Compare commits
47 Commits
e02a4eb990
...
5495551071
Author | SHA1 | Date | |
---|---|---|---|
5495551071 | |||
|
6c3cfa9be7 | ||
|
c20f93aebe | ||
|
0e2097896f | ||
|
df7ab5b5dc | ||
|
b784144edf | ||
|
036f8a463f | ||
1f7d2a9cd2 | |||
4898f50db7 | |||
2ae952cdfa | |||
50b0b324ae | |||
132ece8e53 | |||
c660db82c1 | |||
|
33f2798004 | ||
1fcb55c84d | |||
cd9c3a143b | |||
6affe04fe6 | |||
3ee3ab3ced | |||
013252efde | |||
b63937703c | |||
7450bede5b | |||
07d71eb297 | |||
324d9f871c | |||
9a1a3b350d | |||
c9a0d2eb96 | |||
da4f3900b4 | |||
665c42f595 | |||
49b0f5337c | |||
a3f370b0d8 | |||
a60a1d867c | |||
|
e1d86816a7 | ||
|
ff20c3e25e | ||
|
241a37c889 | ||
1c3e9473fd | |||
64e78d03a1 | |||
94cfcc8750 | |||
|
79973a63dd | ||
7895728308 | |||
8fed5b4b73 | |||
a713d90c3c | |||
0a4782a0da | |||
|
183aed4ba5 | ||
|
84d4e68ab7 | ||
aba173d8a9 | |||
53613a137d | |||
d19aef746c | |||
88f7338d7c |
31
.vscode/tasks.json
vendored
31
.vscode/tasks.json
vendored
@ -1,18 +1,17 @@
|
|||||||
{
|
{
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
// for the documentation about the tasks.json format
|
// for the documentation about the tasks.json format
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"type": "typescript",
|
"type": "typescript",
|
||||||
"tsconfig": "tsconfig.build.json",
|
"tsconfig": "tsconfig.build.json",
|
||||||
"problemMatcher": [
|
"problemMatcher": ["$tsc"],
|
||||||
"$tsc"
|
"group": {
|
||||||
],
|
"kind": "build",
|
||||||
"group": {
|
"isDefault": true
|
||||||
"kind": "build",
|
},
|
||||||
"isDefault": true,
|
"label": "tsc: build - tsconfig.build.json"
|
||||||
},
|
}
|
||||||
}
|
]
|
||||||
]
|
|
||||||
}
|
}
|
@ -1 +1 @@
|
|||||||
Subproject commit 23538677f6c6be2a62f38dc6137ecdd1c76b7b15
|
Subproject commit 001f5dd081fc8729ff8def90c4a1c3f93eb8689a
|
@ -6,6 +6,7 @@ RUN apt-get update -yq && \
|
|||||||
apt-get install -yqq \
|
apt-get install -yqq \
|
||||||
git \
|
git \
|
||||||
python3 \
|
python3 \
|
||||||
|
codespell \
|
||||||
python3-distutils \
|
python3-distutils \
|
||||||
make \
|
make \
|
||||||
zip \
|
zip \
|
||||||
|
@ -42,3 +42,4 @@ ths
|
|||||||
updateing
|
updateing
|
||||||
wan
|
wan
|
||||||
wih
|
wih
|
||||||
|
vie
|
||||||
|
@ -3,4 +3,4 @@ set -exuo pipefail
|
|||||||
|
|
||||||
job_dir=$(dirname "${BASH_SOURCE[0]}")
|
job_dir=$(dirname "${BASH_SOURCE[0]}")
|
||||||
|
|
||||||
codespell -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex"
|
codespell -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex,*/vendor/**"
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"eslint": "^8.29.0",
|
"eslint": "^8.29.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"nx": "15.0.1",
|
"nx": "15.0.1",
|
||||||
"prettier": "^2.8.8"
|
"prettier": "^2.8.8",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"postcss-cli": "^10.1.0",
|
"postcss-cli": "^10.1.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "5.1.3"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"pogen": {
|
"pogen": {
|
||||||
"domain": "aml-backoffice"
|
"domain": "aml-backoffice"
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"target": "ES5",
|
"target": "ES2020",
|
||||||
"module": "ES6",
|
"module": "Node16",
|
||||||
"lib": [
|
"lib": ["DOM", "ES2020"],
|
||||||
"DOM",
|
|
||||||
"ES2017"
|
|
||||||
],
|
|
||||||
"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" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
||||||
@ -45,7 +42,5 @@
|
|||||||
/* Advanced Options */
|
/* Advanced Options */
|
||||||
"skipLibCheck": true /* Skip type checking of declaration files. */
|
"skipLibCheck": true /* Skip type checking of declaration files. */
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*"]
|
||||||
"src/**/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
@ -33,12 +33,12 @@
|
|||||||
"@types/node": "^18.11.17",
|
"@types/node": "^18.11.17",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"typedoc": "^0.24.8",
|
"typedoc": "^0.25.1",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gnu-taler/taler-util": "workspace:*",
|
|
||||||
"@gnu-taler/anastasis-core": "workspace:*",
|
"@gnu-taler/anastasis-core": "workspace:*",
|
||||||
|
"@gnu-taler/taler-util": "workspace:*",
|
||||||
"tslib": "^2.5.3"
|
"tslib": "^2.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
"compileOnSave": true,
|
"compileOnSave": true,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"target": "ES2018",
|
"target": "ES2020",
|
||||||
"module": "ESNext",
|
"module": "Node16",
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "Node16",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"lib": ["es6"],
|
"lib": ["ES2020"],
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "^4.3.3",
|
"ava": "^4.3.3",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@gnu-taler/taler-util": "workspace:*",
|
"@gnu-taler/taler-util": "workspace:*",
|
||||||
|
@ -138,7 +138,6 @@ export * as validators from "./validators.js";
|
|||||||
export * from "./challenge-feedback-types.js";
|
export * from "./challenge-feedback-types.js";
|
||||||
|
|
||||||
const httpLib = createPlatformHttpLib({
|
const httpLib = createPlatformHttpLib({
|
||||||
allowHttp: true,
|
|
||||||
enableThrottling: false,
|
enableThrottling: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"module": "ESNext",
|
"module": "Node16",
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "Node16",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"lib": ["ES2020"],
|
"lib": ["ES2020"],
|
||||||
|
@ -44,6 +44,6 @@
|
|||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
"mocha": "^9.2.0",
|
"mocha": "^9.2.0",
|
||||||
"sass": "1.56.1",
|
"sass": "1.56.1",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"target": "ES5",
|
"target": "ES2020",
|
||||||
"module": "ES6",
|
"module": "Node16",
|
||||||
"lib": [
|
"lib": [
|
||||||
"DOM",
|
"DOM",
|
||||||
"ES2017"
|
"ES2020"
|
||||||
],
|
],
|
||||||
"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. */
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
"po2json": "^0.4.5",
|
"po2json": "^0.4.5",
|
||||||
"preact-render-to-string": "^5.2.6",
|
"preact-render-to-string": "^5.2.6",
|
||||||
"sass": "1.56.1",
|
"sass": "1.56.1",
|
||||||
"typescript": "5.1.3"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"pogen": {
|
"pogen": {
|
||||||
"domain": "bank"
|
"domain": "bank"
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"target": "ES5",
|
"target": "ES2020",
|
||||||
"module": "ES6",
|
"module": "Node16",
|
||||||
"lib": [
|
"lib": ["DOM", "ES2020"],
|
||||||
"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" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
||||||
@ -45,7 +42,5 @@
|
|||||||
/* Advanced Options */
|
/* Advanced Options */
|
||||||
"skipLibCheck": true /* Skip type checking of declaration files. */
|
"skipLibCheck": true /* Skip type checking of declaration files. */
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*"]
|
||||||
"src/**/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
@ -29,7 +29,7 @@
|
|||||||
"ava": "^5.3.1",
|
"ava": "^5.3.1",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.6.0"
|
"tslib": "^2.6.0"
|
||||||
|
@ -1882,7 +1882,7 @@ export class SqliteBackend implements Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearObjectStore(
|
async clearObjectStore(
|
||||||
btx: DatabaseTransaction,
|
btx: DatabaseTransaction,
|
||||||
objectStoreName: string,
|
objectStoreName: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -1906,7 +1906,21 @@ export class SqliteBackend implements Backend {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Method not implemented.");
|
this._prep(sqlClearObjectStore).run({
|
||||||
|
object_store_id: scopeInfo.objectStoreId,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const index of scopeInfo.indexMap.values()) {
|
||||||
|
let stmt: Sqlite3Statement;
|
||||||
|
if (index.unique) {
|
||||||
|
stmt = this._prep(sqlClearUniqueIndexData);
|
||||||
|
} else {
|
||||||
|
stmt = this._prep(sqlClearIndexData);
|
||||||
|
}
|
||||||
|
stmt.run({
|
||||||
|
index_id: index.indexId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1963,6 +1977,15 @@ CREATE TABLE IF NOT EXISTS unique_index_data
|
|||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const sqlClearObjectStore = `
|
||||||
|
DELETE FROM object_data WHERE object_store_id=$object_store_id`;
|
||||||
|
|
||||||
|
const sqlClearIndexData = `
|
||||||
|
DELETE FROM index_data WHERE index_id=$index_id`;
|
||||||
|
|
||||||
|
const sqlClearUniqueIndexData = `
|
||||||
|
DELETE FROM unique_index_data WHERE index_id=$index_id`;
|
||||||
|
|
||||||
const sqlListDatabases = `
|
const sqlListDatabases = `
|
||||||
SELECT name, version FROM databases;
|
SELECT name, version FROM databases;
|
||||||
`;
|
`;
|
||||||
|
@ -144,7 +144,7 @@ export interface IndexMeta {
|
|||||||
unique: boolean;
|
unique: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Instead of refering to an object store by name,
|
// FIXME: Instead of referring to an object store by name,
|
||||||
// maybe refer to it via some internal, numeric ID?
|
// maybe refer to it via some internal, numeric ID?
|
||||||
// This would simplify renaming.
|
// This would simplify renaming.
|
||||||
export interface Backend {
|
export interface Backend {
|
||||||
|
@ -735,7 +735,9 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this._closePending) {
|
if (this._closePending) {
|
||||||
throw new InvalidStateError();
|
throw new InvalidStateError(
|
||||||
|
`tried to start transaction on ${this._name}, but a close is pending`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(storeNames)) {
|
if (!Array.isArray(storeNames)) {
|
||||||
@ -930,6 +932,9 @@ export class BridgeIDBFactory {
|
|||||||
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction
|
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction
|
||||||
|
|
||||||
for (const otherConn of this.connections) {
|
for (const otherConn of this.connections) {
|
||||||
|
if (otherConn._name != db._name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (otherConn._closePending) {
|
if (otherConn._closePending) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"lib": ["es6"],
|
"lib": ["ES2020"],
|
||||||
"module": "ES2020",
|
"module": "Node16",
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "Node16",
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
@ -65,6 +65,6 @@
|
|||||||
"sirv-cli": "^1.0.11",
|
"sirv-cli": "^1.0.11",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tslib": "2.5.3",
|
"tslib": "2.5.3",
|
||||||
"typescript": "5.1.3"
|
"typescript": "5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "@gnu-taler/merchant-backoffice-ui",
|
"name": "@gnu-taler/merchant-backoffice-ui",
|
||||||
"version": "0.0.5",
|
"version": "0.1.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"preact": "10.11.3",
|
"preact": "10.11.3",
|
||||||
"preact-router": "3.2.1",
|
"preact-router": "3.2.1",
|
||||||
"qrcode-generator": "1.4.4",
|
"qrcode-generator": "1.4.4",
|
||||||
"swr": "1.3.0",
|
"swr": "2.2.2",
|
||||||
"yup": "^0.32.9"
|
"yup": "^0.32.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -75,8 +75,8 @@
|
|||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "1.56.1",
|
"sass": "1.56.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"typedoc": "^0.24.8",
|
"typedoc": "^0.25.1",
|
||||||
"typescript": "5.1.3"
|
"typescript": "5.2.2"
|
||||||
},
|
},
|
||||||
"pogen": {
|
"pogen": {
|
||||||
"domain": "taler-merchant-backoffice"
|
"domain": "taler-merchant-backoffice"
|
||||||
|
@ -19,19 +19,20 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
ErrorType,
|
ErrorType,
|
||||||
TranslationProvider,
|
TranslationProvider,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { route } from "preact-router";
|
import { route } from "preact-router";
|
||||||
import { useMemo, useState } from "preact/hooks";
|
import { useMemo } from "preact/hooks";
|
||||||
import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
|
import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
|
||||||
import { Loading } from "./components/exception/loading.js";
|
import { Loading } from "./components/exception/loading.js";
|
||||||
import {
|
import {
|
||||||
NotificationCard,
|
NotConnectedAppMenu,
|
||||||
NotYetReadyAppMenu,
|
NotificationCard
|
||||||
} from "./components/menu/index.js";
|
} from "./components/menu/index.js";
|
||||||
import {
|
import {
|
||||||
BackendContextProvider,
|
BackendContextProvider,
|
||||||
@ -41,23 +42,24 @@ import { ConfigContextProvider } from "./context/config.js";
|
|||||||
import { useBackendConfig } from "./hooks/backend.js";
|
import { useBackendConfig } from "./hooks/backend.js";
|
||||||
import { strings } from "./i18n/strings.js";
|
import { strings } from "./i18n/strings.js";
|
||||||
import LoginPage from "./paths/login/index.js";
|
import LoginPage from "./paths/login/index.js";
|
||||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
|
||||||
import { Settings } from "./paths/settings/index.js";
|
|
||||||
|
|
||||||
export function Application(): VNode {
|
export function Application(): VNode {
|
||||||
return (
|
return (
|
||||||
// <FetchContextProvider>
|
|
||||||
<BackendContextProvider>
|
<BackendContextProvider>
|
||||||
<TranslationProvider source={strings}>
|
<TranslationProvider source={strings}>
|
||||||
<ApplicationStatusRoutes />
|
<ApplicationStatusRoutes />
|
||||||
</TranslationProvider>
|
</TranslationProvider>
|
||||||
</BackendContextProvider>
|
</BackendContextProvider>
|
||||||
// </FetchContextProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check connection testing against /config
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
function ApplicationStatusRoutes(): VNode {
|
function ApplicationStatusRoutes(): VNode {
|
||||||
const { updateLoginStatus, triedToLog } = useBackendContext();
|
const { url, updateLoginStatus, triedToLog } = useBackendContext();
|
||||||
const result = useBackendConfig();
|
const result = useBackendConfig();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
@ -71,19 +73,10 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
: { currency: "unknown", version: "unknown" };
|
: { currency: "unknown", version: "unknown" };
|
||||||
const ctx = useMemo(() => ({ currency, version }), [currency, version]);
|
const ctx = useMemo(() => ({ currency, version }), [currency, version]);
|
||||||
|
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
|
||||||
|
|
||||||
if (showSettings) {
|
|
||||||
return <Fragment>
|
|
||||||
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" />
|
|
||||||
<Settings />
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!triedToLog) {
|
if (!triedToLog) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotYetReadyAppMenu title="Welcome!" onShowSettings={() => setShowSettings(true)} />
|
<NotConnectedAppMenu title="Welcome!" />
|
||||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@ -97,7 +90,7 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotYetReadyAppMenu title="Login" onShowSettings={() => setShowSettings(true)} />
|
<NotConnectedAppMenu title="Login" />
|
||||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@ -108,7 +101,7 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
|
<NotConnectedAppMenu title="Error" />
|
||||||
<NotificationCard
|
<NotificationCard
|
||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Server not found`,
|
message: i18n.str`Server not found`,
|
||||||
@ -122,7 +115,7 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
}
|
}
|
||||||
if (result.type === ErrorType.SERVER) {
|
if (result.type === ErrorType.SERVER) {
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
|
<NotConnectedAppMenu title="Error" />
|
||||||
<NotificationCard
|
<NotificationCard
|
||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Server response with an error code`,
|
message: i18n.str`Server response with an error code`,
|
||||||
@ -135,7 +128,7 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
}
|
}
|
||||||
if (result.type === ErrorType.UNREADABLE) {
|
if (result.type === ErrorType.UNREADABLE) {
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
|
<NotConnectedAppMenu title="Error" />
|
||||||
<NotificationCard
|
<NotificationCard
|
||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
|
message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
|
||||||
@ -148,7 +141,7 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
|
<NotConnectedAppMenu title="Error" />
|
||||||
<NotificationCard
|
<NotificationCard
|
||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Unexpected Error`,
|
message: i18n.str`Unexpected Error`,
|
||||||
@ -161,6 +154,25 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_VERSION = "5:0:1"
|
||||||
|
if (!LibtoolVersion.compare(
|
||||||
|
SUPPORTED_VERSION,
|
||||||
|
result.data.version,
|
||||||
|
)?.compatible) {
|
||||||
|
return <Fragment>
|
||||||
|
<NotConnectedAppMenu title="Error" />
|
||||||
|
<NotificationCard
|
||||||
|
notification={{
|
||||||
|
message: i18n.str`Incompatible version`,
|
||||||
|
type: "ERROR",
|
||||||
|
description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="has-navbar-fixed-top">
|
<div class="has-navbar-fixed-top">
|
||||||
<ConfigContextProvider value={ctx}>
|
<ConfigContextProvider value={ctx}>
|
||||||
|
@ -22,7 +22,7 @@ import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
|
|||||||
import { createHashHistory } from "history";
|
import { createHashHistory } from "history";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { Router, Route, route } from "preact-router";
|
import { Router, Route, route } from "preact-router";
|
||||||
import { useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import {
|
import {
|
||||||
NotificationCard,
|
NotificationCard,
|
||||||
NotYetReadyAppMenu,
|
NotYetReadyAppMenu,
|
||||||
@ -35,52 +35,55 @@ import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
|
|||||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
import { Settings } from "./paths/settings/index.js";
|
import { Settings } from "./paths/settings/index.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if admin against /management/instances
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export function ApplicationReadyRoutes(): VNode {
|
export function ApplicationReadyRoutes(): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const [unauthorized, setUnauthorized] = useState(false)
|
||||||
const {
|
const {
|
||||||
url: backendURL,
|
url: backendURL,
|
||||||
updateLoginStatus,
|
updateLoginStatus: updateLoginStatus2,
|
||||||
clearAllTokens,
|
|
||||||
} = useBackendContext();
|
} = useBackendContext();
|
||||||
|
|
||||||
|
function updateLoginStatus(url: string, token: string | undefined) {
|
||||||
|
console.log("updateing", url, token)
|
||||||
|
updateLoginStatus2(url, token)
|
||||||
|
setUnauthorized(false)
|
||||||
|
}
|
||||||
|
|
||||||
const result = useBackendInstancesTestForAdmin();
|
const result = useBackendInstancesTestForAdmin();
|
||||||
|
|
||||||
const clearTokenAndGoToRoot = () => {
|
const clearTokenAndGoToRoot = () => {
|
||||||
clearAllTokens();
|
|
||||||
route("/");
|
route("/");
|
||||||
};
|
};
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
const [showSettings, setShowSettings] = useState(false)
|
||||||
|
// useEffect(() => {
|
||||||
|
// setUnauthorized(FF)
|
||||||
|
// }, [FF])
|
||||||
|
const unauthorizedAdmin = !result.loading && !result.ok && result.type === ErrorType.CLIENT && result.status === HttpStatusCode.Unauthorized
|
||||||
|
|
||||||
if (showSettings) {
|
if (showSettings) {
|
||||||
return <Fragment>
|
return <Fragment>
|
||||||
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} />
|
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
|
||||||
<Settings/>
|
<Settings />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
if (result.loading) return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." />;
|
|
||||||
|
|
||||||
let admin = true;
|
if (result.loading) {
|
||||||
let instanceNameByBackendURL;
|
return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." isPasswordOk={false} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (!result.ok) {
|
let admin = result.ok || unauthorizedAdmin;
|
||||||
if (
|
let instanceNameByBackendURL: string | undefined;
|
||||||
result.type === ErrorType.CLIENT &&
|
|
||||||
result.status === HttpStatusCode.Unauthorized
|
if (!admin) {
|
||||||
) {
|
// * the testing against admin endpoint failed and it's not
|
||||||
return (
|
// an authorization problem
|
||||||
<Fragment>
|
// * merchant backend will return this SPA under the main
|
||||||
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} />
|
// endpoint or /instance/<id> endpoint
|
||||||
<NotificationCard
|
// => trying to infer the instance id
|
||||||
notification={{
|
|
||||||
message: i18n.str`Access denied`,
|
|
||||||
description: i18n.str`Check your token is valid`,
|
|
||||||
type: "ERROR",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<LoginPage onConfirm={updateLoginStatus} />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const path = new URL(backendURL).pathname;
|
const path = new URL(backendURL).pathname;
|
||||||
const match = INSTANCE_ID_LOOKUP.exec(path);
|
const match = INSTANCE_ID_LOOKUP.exec(path);
|
||||||
if (!match || !match[1]) {
|
if (!match || !match[1]) {
|
||||||
@ -89,7 +92,7 @@ export function ApplicationReadyRoutes(): VNode {
|
|||||||
// does not match our pattern
|
// does not match our pattern
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} />
|
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
|
||||||
<NotificationCard
|
<NotificationCard
|
||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Couldn't access the server.`,
|
message: i18n.str`Couldn't access the server.`,
|
||||||
@ -102,10 +105,24 @@ export function ApplicationReadyRoutes(): VNode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
admin = false;
|
|
||||||
instanceNameByBackendURL = match[1];
|
instanceNameByBackendURL = match[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(unauthorized, unauthorizedAdmin)
|
||||||
|
if (unauthorized || unauthorizedAdmin) {
|
||||||
|
return <Fragment>
|
||||||
|
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
|
||||||
|
<NotificationCard
|
||||||
|
notification={{
|
||||||
|
message: i18n.str`Access denied`,
|
||||||
|
description: i18n.str`Check your token is valid`,
|
||||||
|
type: "ERROR",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<LoginPage onConfirm={updateLoginStatus} />
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
const history = createHashHistory();
|
const history = createHashHistory();
|
||||||
return (
|
return (
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
@ -113,6 +130,11 @@ export function ApplicationReadyRoutes(): VNode {
|
|||||||
default
|
default
|
||||||
component={DefaultMainRoute}
|
component={DefaultMainRoute}
|
||||||
admin={admin}
|
admin={admin}
|
||||||
|
onUnauthorized={() => setUnauthorized(true)}
|
||||||
|
onLoginPass={() => {
|
||||||
|
console.log("ahora si")
|
||||||
|
setUnauthorized(false)
|
||||||
|
}}
|
||||||
instanceNameByBackendURL={instanceNameByBackendURL}
|
instanceNameByBackendURL={instanceNameByBackendURL}
|
||||||
/>
|
/>
|
||||||
</Router>
|
</Router>
|
||||||
@ -122,6 +144,8 @@ export function ApplicationReadyRoutes(): VNode {
|
|||||||
function DefaultMainRoute({
|
function DefaultMainRoute({
|
||||||
instance,
|
instance,
|
||||||
admin,
|
admin,
|
||||||
|
onUnauthorized,
|
||||||
|
onLoginPass,
|
||||||
instanceNameByBackendURL,
|
instanceNameByBackendURL,
|
||||||
url, //from preact-router
|
url, //from preact-router
|
||||||
}: any): VNode {
|
}: any): VNode {
|
||||||
@ -133,6 +157,8 @@ function DefaultMainRoute({
|
|||||||
<InstanceRoutes
|
<InstanceRoutes
|
||||||
admin={admin}
|
admin={admin}
|
||||||
path={url}
|
path={url}
|
||||||
|
onUnauthorized={onUnauthorized}
|
||||||
|
onLoginPass={onLoginPass}
|
||||||
id={instanceName}
|
id={instanceName}
|
||||||
setInstanceName={setInstanceName}
|
setInstanceName={setInstanceName}
|
||||||
/>
|
/>
|
||||||
|
@ -40,6 +40,7 @@ import {
|
|||||||
import { useInstanceKYCDetails } from "./hooks/instance.js";
|
import { useInstanceKYCDetails } from "./hooks/instance.js";
|
||||||
import InstanceCreatePage from "./paths/admin/create/index.js";
|
import InstanceCreatePage from "./paths/admin/create/index.js";
|
||||||
import InstanceListPage from "./paths/admin/list/index.js";
|
import InstanceListPage from "./paths/admin/list/index.js";
|
||||||
|
import TokenPage from "./paths/instance/token/index.js";
|
||||||
import ListKYCPage from "./paths/instance/kyc/list/index.js";
|
import ListKYCPage from "./paths/instance/kyc/list/index.js";
|
||||||
import OrderCreatePage from "./paths/instance/orders/create/index.js";
|
import OrderCreatePage from "./paths/instance/orders/create/index.js";
|
||||||
import OrderDetailsPage from "./paths/instance/orders/details/index.js";
|
import OrderDetailsPage from "./paths/instance/orders/details/index.js";
|
||||||
@ -47,6 +48,9 @@ import OrderListPage from "./paths/instance/orders/list/index.js";
|
|||||||
import ProductCreatePage from "./paths/instance/products/create/index.js";
|
import ProductCreatePage from "./paths/instance/products/create/index.js";
|
||||||
import ProductListPage from "./paths/instance/products/list/index.js";
|
import ProductListPage from "./paths/instance/products/list/index.js";
|
||||||
import ProductUpdatePage from "./paths/instance/products/update/index.js";
|
import ProductUpdatePage from "./paths/instance/products/update/index.js";
|
||||||
|
import BankAccountCreatePage from "./paths/instance/accounts/create/index.js";
|
||||||
|
import BankAccountListPage from "./paths/instance/accounts/list/index.js";
|
||||||
|
import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js";
|
||||||
import ReservesCreatePage from "./paths/instance/reserves/create/index.js";
|
import ReservesCreatePage from "./paths/instance/reserves/create/index.js";
|
||||||
import ReservesDetailsPage from "./paths/instance/reserves/details/index.js";
|
import ReservesDetailsPage from "./paths/instance/reserves/details/index.js";
|
||||||
import ReservesListPage from "./paths/instance/reserves/list/index.js";
|
import ReservesListPage from "./paths/instance/reserves/list/index.js";
|
||||||
@ -58,6 +62,9 @@ import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
|
|||||||
import WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
|
import WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
|
||||||
import WebhookListPage from "./paths/instance/webhooks/list/index.js";
|
import WebhookListPage from "./paths/instance/webhooks/list/index.js";
|
||||||
import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js";
|
import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js";
|
||||||
|
import ValidatorCreatePage from "./paths/instance/validators/create/index.js";
|
||||||
|
import ValidatorListPage from "./paths/instance/validators/list/index.js";
|
||||||
|
import ValidatorUpdatePage from "./paths/instance/validators/update/index.js";
|
||||||
import TransferCreatePage from "./paths/instance/transfers/create/index.js";
|
import TransferCreatePage from "./paths/instance/transfers/create/index.js";
|
||||||
import TransferListPage from "./paths/instance/transfers/list/index.js";
|
import TransferListPage from "./paths/instance/transfers/list/index.js";
|
||||||
import InstanceUpdatePage, {
|
import InstanceUpdatePage, {
|
||||||
@ -69,11 +76,16 @@ import NotFoundPage from "./paths/notfound/index.js";
|
|||||||
import { Notification } from "./utils/types.js";
|
import { Notification } from "./utils/types.js";
|
||||||
import { MerchantBackend } from "./declaration.js";
|
import { MerchantBackend } from "./declaration.js";
|
||||||
import { Settings } from "./paths/settings/index.js";
|
import { Settings } from "./paths/settings/index.js";
|
||||||
|
import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
|
||||||
|
|
||||||
export enum InstancePaths {
|
export enum InstancePaths {
|
||||||
// details = '/',
|
|
||||||
error = "/error",
|
error = "/error",
|
||||||
update = "/update",
|
server = "/server",
|
||||||
|
token = "/token",
|
||||||
|
|
||||||
|
bank_list = "/bank",
|
||||||
|
bank_update = "/bank/:bid/update",
|
||||||
|
bank_new = "/bank/new",
|
||||||
|
|
||||||
product_list = "/products",
|
product_list = "/products",
|
||||||
product_update = "/product/:pid/update",
|
product_update = "/product/:pid/update",
|
||||||
@ -102,11 +114,15 @@ export enum InstancePaths {
|
|||||||
webhooks_update = "/webhooks/:tid/update",
|
webhooks_update = "/webhooks/:tid/update",
|
||||||
webhooks_new = "/webhooks/new",
|
webhooks_new = "/webhooks/new",
|
||||||
|
|
||||||
settings = "/settings",
|
validators_list = "/validators",
|
||||||
|
validators_update = "/validators/:vid/update",
|
||||||
|
validators_new = "/validators/new",
|
||||||
|
|
||||||
|
settings = "/interface",
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
const noop = () => {};
|
const noop = () => { };
|
||||||
|
|
||||||
export enum AdminPaths {
|
export enum AdminPaths {
|
||||||
list_instances = "/instances",
|
list_instances = "/instances",
|
||||||
@ -118,6 +134,8 @@ export interface Props {
|
|||||||
id: string;
|
id: string;
|
||||||
admin?: boolean;
|
admin?: boolean;
|
||||||
path: string;
|
path: string;
|
||||||
|
onUnauthorized: () => void;
|
||||||
|
onLoginPass: () => void;
|
||||||
setInstanceName: (s: string) => void;
|
setInstanceName: (s: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,40 +143,29 @@ export function InstanceRoutes({
|
|||||||
id,
|
id,
|
||||||
admin,
|
admin,
|
||||||
path,
|
path,
|
||||||
|
onUnauthorized,
|
||||||
|
onLoginPass,
|
||||||
setInstanceName,
|
setInstanceName,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const [_, updateDefaultToken] = useBackendDefaultToken();
|
const [defaultToken, updateDefaultToken] = useBackendDefaultToken();
|
||||||
const [token, updateToken] = useBackendInstanceToken(id);
|
const [token, updateToken] = useBackendInstanceToken(id);
|
||||||
const {
|
|
||||||
updateLoginStatus: changeBackend,
|
|
||||||
addTokenCleaner,
|
|
||||||
clearAllTokens,
|
|
||||||
} = useBackendContext();
|
|
||||||
const cleaner = useCallback(() => {
|
|
||||||
updateToken(undefined);
|
|
||||||
}, [id]);
|
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
type GlobalNotifState = (Notification & { to: string }) | undefined;
|
type GlobalNotifState = (Notification & { to: string }) | undefined;
|
||||||
const [globalNotification, setGlobalNotification] =
|
const [globalNotification, setGlobalNotification] =
|
||||||
useState<GlobalNotifState>(undefined);
|
useState<GlobalNotifState>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
addTokenCleaner(cleaner);
|
|
||||||
}, [addTokenCleaner, cleaner]);
|
|
||||||
|
|
||||||
const changeToken = (token?: string) => {
|
const changeToken = (token?: string) => {
|
||||||
if (admin) {
|
if (admin) {
|
||||||
updateToken(token);
|
updateToken(token);
|
||||||
} else {
|
} else {
|
||||||
updateDefaultToken(token);
|
updateDefaultToken(token);
|
||||||
}
|
}
|
||||||
|
onLoginPass()
|
||||||
};
|
};
|
||||||
const updateLoginStatus = (url: string, token?: string) => {
|
// const updateLoginStatus = (url: string, token?: string) => {
|
||||||
changeBackend(url);
|
// changeToken(token);
|
||||||
if (!token) return;
|
// };
|
||||||
changeToken(token);
|
|
||||||
};
|
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({ id, token, admin, changeToken }),
|
() => ({ id, token, admin, changeToken }),
|
||||||
@ -192,18 +199,17 @@ export function InstanceRoutes({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoginPageAccessDenied = () => (
|
// const LoginPageAccessDeniend = onUnauthorized
|
||||||
<Fragment>
|
const LoginPageAccessDenied = () => {
|
||||||
<NotificationCard
|
onUnauthorized()
|
||||||
notification={{
|
return <NotificationCard
|
||||||
message: i18n.str`Access denied`,
|
notification={{
|
||||||
description: i18n.str`The access token provided is invalid.`,
|
message: i18n.str`Access denied`,
|
||||||
type: "ERROR",
|
description: i18n.str`Redirecting to login page.`,
|
||||||
}}
|
type: "ERROR",
|
||||||
/>
|
}}
|
||||||
<LoginPage onConfirm={updateLoginStatus} />
|
/>
|
||||||
</Fragment>
|
}
|
||||||
);
|
|
||||||
|
|
||||||
function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
|
function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
|
||||||
return function IfAdminCreateDefaultOrImpl(props?: T) {
|
return function IfAdminCreateDefaultOrImpl(props?: T) {
|
||||||
@ -234,8 +240,10 @@ export function InstanceRoutes({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clearTokenAndGoToRoot = () => {
|
const clearTokenAndGoToRoot = () => {
|
||||||
clearAllTokens();
|
|
||||||
route("/");
|
route("/");
|
||||||
|
// clear all tokens
|
||||||
|
updateToken(undefined)
|
||||||
|
updateDefaultToken(undefined)
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -244,11 +252,12 @@ export function InstanceRoutes({
|
|||||||
instance={id}
|
instance={id}
|
||||||
admin={admin}
|
admin={admin}
|
||||||
onShowSettings={() => {
|
onShowSettings={() => {
|
||||||
route("/settings")
|
route(InstancePaths.settings)
|
||||||
}}
|
}}
|
||||||
path={path}
|
path={path}
|
||||||
onLogout={clearTokenAndGoToRoot}
|
onLogout={clearTokenAndGoToRoot}
|
||||||
setInstanceName={setInstanceName}
|
setInstanceName={setInstanceName}
|
||||||
|
isPasswordOk={defaultToken !== undefined}
|
||||||
/>
|
/>
|
||||||
<KycBanner />
|
<KycBanner />
|
||||||
<NotificationCard notification={globalNotification} />
|
<NotificationCard notification={globalNotification} />
|
||||||
@ -308,7 +317,7 @@ export function InstanceRoutes({
|
|||||||
* Update instance page
|
* Update instance page
|
||||||
*/}
|
*/}
|
||||||
<Route
|
<Route
|
||||||
path={InstancePaths.update}
|
path={InstancePaths.server}
|
||||||
component={InstanceUpdatePage}
|
component={InstanceUpdatePage}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
route(`/`);
|
route(`/`);
|
||||||
@ -321,6 +330,19 @@ export function InstanceRoutes({
|
|||||||
onUnauthorized={LoginPageAccessDenied}
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
|
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
|
||||||
/>
|
/>
|
||||||
|
{/**
|
||||||
|
* Update instance page
|
||||||
|
*/}
|
||||||
|
<Route
|
||||||
|
path={InstancePaths.token}
|
||||||
|
component={TokenPage}
|
||||||
|
onChange={() => {
|
||||||
|
route(`/`);
|
||||||
|
}}
|
||||||
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
|
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
|
||||||
|
/>
|
||||||
{/**
|
{/**
|
||||||
* Product pages
|
* Product pages
|
||||||
*/}
|
*/}
|
||||||
@ -328,7 +350,7 @@ export function InstanceRoutes({
|
|||||||
path={InstancePaths.product_list}
|
path={InstancePaths.product_list}
|
||||||
component={ProductListPage}
|
component={ProductListPage}
|
||||||
onUnauthorized={LoginPageAccessDenied}
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
|
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
|
||||||
onCreate={() => {
|
onCreate={() => {
|
||||||
route(InstancePaths.product_new);
|
route(InstancePaths.product_new);
|
||||||
}}
|
}}
|
||||||
@ -360,6 +382,45 @@ export function InstanceRoutes({
|
|||||||
route(InstancePaths.product_list);
|
route(InstancePaths.product_list);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/**
|
||||||
|
* Bank pages
|
||||||
|
*/}
|
||||||
|
<Route
|
||||||
|
path={InstancePaths.bank_list}
|
||||||
|
component={BankAccountListPage}
|
||||||
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
|
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
|
||||||
|
onCreate={() => {
|
||||||
|
route(InstancePaths.bank_new);
|
||||||
|
}}
|
||||||
|
onSelect={(id: string) => {
|
||||||
|
route(InstancePaths.bank_update.replace(":bid", id));
|
||||||
|
}}
|
||||||
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={InstancePaths.bank_update}
|
||||||
|
component={BankAccountUpdatePage}
|
||||||
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
|
onLoadError={ServerErrorRedirectTo(InstancePaths.product_list)}
|
||||||
|
onConfirm={() => {
|
||||||
|
route(InstancePaths.bank_list);
|
||||||
|
}}
|
||||||
|
onBack={() => {
|
||||||
|
route(InstancePaths.bank_list);
|
||||||
|
}}
|
||||||
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={InstancePaths.bank_new}
|
||||||
|
component={BankAccountCreatePage}
|
||||||
|
onConfirm={() => {
|
||||||
|
route(InstancePaths.bank_list);
|
||||||
|
}}
|
||||||
|
onBack={() => {
|
||||||
|
route(InstancePaths.bank_list);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/**
|
{/**
|
||||||
* Order pages
|
* Order pages
|
||||||
*/}
|
*/}
|
||||||
@ -373,7 +434,7 @@ export function InstanceRoutes({
|
|||||||
route(InstancePaths.order_details.replace(":oid", id));
|
route(InstancePaths.order_details.replace(":oid", id));
|
||||||
}}
|
}}
|
||||||
onUnauthorized={LoginPageAccessDenied}
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
|
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
|
||||||
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
@ -389,8 +450,8 @@ export function InstanceRoutes({
|
|||||||
<Route
|
<Route
|
||||||
path={InstancePaths.order_new}
|
path={InstancePaths.order_new}
|
||||||
component={OrderCreatePage}
|
component={OrderCreatePage}
|
||||||
onConfirm={() => {
|
onConfirm={(orderId: string) => {
|
||||||
route(InstancePaths.order_list);
|
route(InstancePaths.order_details.replace(":oid", orderId));
|
||||||
}}
|
}}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
route(InstancePaths.order_list);
|
route(InstancePaths.order_list);
|
||||||
@ -404,7 +465,7 @@ export function InstanceRoutes({
|
|||||||
component={TransferListPage}
|
component={TransferListPage}
|
||||||
onUnauthorized={LoginPageAccessDenied}
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
|
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
|
||||||
onCreate={() => {
|
onCreate={() => {
|
||||||
route(InstancePaths.transfers_new);
|
route(InstancePaths.transfers_new);
|
||||||
}}
|
}}
|
||||||
@ -427,7 +488,7 @@ export function InstanceRoutes({
|
|||||||
component={WebhookListPage}
|
component={WebhookListPage}
|
||||||
onUnauthorized={LoginPageAccessDenied}
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
|
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
|
||||||
onCreate={() => {
|
onCreate={() => {
|
||||||
route(InstancePaths.webhooks_new);
|
route(InstancePaths.webhooks_new);
|
||||||
}}
|
}}
|
||||||
@ -458,6 +519,45 @@ export function InstanceRoutes({
|
|||||||
route(InstancePaths.webhooks_list);
|
route(InstancePaths.webhooks_list);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/**
|
||||||
|
* Validator pages
|
||||||
|
*/}
|
||||||
|
<Route
|
||||||
|
path={InstancePaths.validators_list}
|
||||||
|
component={ValidatorListPage}
|
||||||
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
|
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
|
||||||
|
onCreate={() => {
|
||||||
|
route(InstancePaths.validators_new);
|
||||||
|
}}
|
||||||
|
onSelect={(id: string) => {
|
||||||
|
route(InstancePaths.validators_update.replace(":vid", id));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={InstancePaths.validators_update}
|
||||||
|
component={ValidatorUpdatePage}
|
||||||
|
onConfirm={() => {
|
||||||
|
route(InstancePaths.validators_list);
|
||||||
|
}}
|
||||||
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
|
onLoadError={ServerErrorRedirectTo(InstancePaths.validators_list)}
|
||||||
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
|
onBack={() => {
|
||||||
|
route(InstancePaths.validators_list);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={InstancePaths.validators_new}
|
||||||
|
component={ValidatorCreatePage}
|
||||||
|
onConfirm={() => {
|
||||||
|
route(InstancePaths.validators_list);
|
||||||
|
}}
|
||||||
|
onBack={() => {
|
||||||
|
route(InstancePaths.validators_list);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/**
|
{/**
|
||||||
* Templates pages
|
* Templates pages
|
||||||
*/}
|
*/}
|
||||||
@ -466,7 +566,7 @@ export function InstanceRoutes({
|
|||||||
component={TemplateListPage}
|
component={TemplateListPage}
|
||||||
onUnauthorized={LoginPageAccessDenied}
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
|
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
|
||||||
onCreate={() => {
|
onCreate={() => {
|
||||||
route(InstancePaths.templates_new);
|
route(InstancePaths.templates_new);
|
||||||
}}
|
}}
|
||||||
@ -535,7 +635,7 @@ export function InstanceRoutes({
|
|||||||
component={ReservesListPage}
|
component={ReservesListPage}
|
||||||
onUnauthorized={LoginPageAccessDenied}
|
onUnauthorized={LoginPageAccessDenied}
|
||||||
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
|
||||||
onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
|
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
|
||||||
onSelect={(id: string) => {
|
onSelect={(id: string) => {
|
||||||
route(InstancePaths.reserves_details.replace(":rid", id));
|
route(InstancePaths.reserves_details.replace(":rid", id));
|
||||||
}}
|
}}
|
||||||
@ -590,7 +690,7 @@ function AdminInstanceUpdatePage({
|
|||||||
const { updateLoginStatus: changeBackend } = useBackendContext();
|
const { updateLoginStatus: changeBackend } = useBackendContext();
|
||||||
const updateLoginStatus = (url: string, token?: string): void => {
|
const updateLoginStatus = (url: string, token?: string): void => {
|
||||||
changeBackend(url);
|
changeBackend(url);
|
||||||
if (token) changeToken(token);
|
changeToken(token);
|
||||||
};
|
};
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({ id, token, admin: true, changeToken }),
|
() => ({ id, token, admin: true, changeToken }),
|
||||||
@ -607,20 +707,20 @@ function AdminInstanceUpdatePage({
|
|||||||
const notif =
|
const notif =
|
||||||
error.type === ErrorType.TIMEOUT
|
error.type === ErrorType.TIMEOUT
|
||||||
? {
|
? {
|
||||||
message: i18n.str`The request to the backend take too long and was cancelled`,
|
message: i18n.str`The request to the backend take too long and was cancelled`,
|
||||||
description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
|
description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
|
||||||
type: "ERROR" as const,
|
type: "ERROR" as const,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
|
message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
|
||||||
description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
|
description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`,
|
||||||
details:
|
details:
|
||||||
error.type === ErrorType.CLIENT ||
|
error.type === ErrorType.CLIENT ||
|
||||||
error.type === ErrorType.SERVER
|
error.type === ErrorType.SERVER
|
||||||
? error.payload.detail
|
? error.payload.detail
|
||||||
: undefined,
|
: undefined,
|
||||||
type: "ERROR" as const,
|
type: "ERROR" as const,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotificationCard notification={notif} />
|
<NotificationCard notification={notif} />
|
||||||
@ -650,7 +750,8 @@ function AdminInstanceUpdatePage({
|
|||||||
function KycBanner(): VNode {
|
function KycBanner(): VNode {
|
||||||
const kycStatus = useInstanceKYCDetails();
|
const kycStatus = useInstanceKYCDetails();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const today = format(new Date(), "yyyy-MM-dd");
|
const [settings] = useSettings();
|
||||||
|
const today = format(new Date(), dateFormatForSettings(settings));
|
||||||
const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide");
|
const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide");
|
||||||
const hasBeenHidden = today === lastHide;
|
const hasBeenHidden = today === lastHide;
|
||||||
const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
|
const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
|
||||||
|
@ -93,7 +93,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
|
|||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={"set new access token"}
|
placeholder={"current access token"}
|
||||||
name="token"
|
name="token"
|
||||||
onKeyPress={(e) =>
|
onKeyPress={(e) =>
|
||||||
e.keyCode === 13
|
e.keyCode === 13
|
||||||
@ -186,7 +186,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
|
|||||||
<input
|
<input
|
||||||
class="input"
|
class="input"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder={"set new access token"}
|
placeholder={"current access token"}
|
||||||
name="token"
|
name="token"
|
||||||
onKeyPress={(e) =>
|
onKeyPress={(e) =>
|
||||||
e.keyCode === 13
|
e.keyCode === 13
|
||||||
|
@ -20,16 +20,18 @@
|
|||||||
*/
|
*/
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { h, VNode } from "preact";
|
import { ComponentChildren, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { DatePicker } from "../picker/DatePicker.js";
|
import { DatePicker } from "../picker/DatePicker.js";
|
||||||
import { InputProps, useField } from "./useField.js";
|
import { InputProps, useField } from "./useField.js";
|
||||||
|
import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js";
|
||||||
|
|
||||||
export interface Props<T> extends InputProps<T> {
|
export interface Props<T> extends InputProps<T> {
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
expand?: boolean;
|
expand?: boolean;
|
||||||
//FIXME: create separated components InputDate and InputTimestamp
|
//FIXME: create separated components InputDate and InputTimestamp
|
||||||
withTimestampSupport?: boolean;
|
withTimestampSupport?: boolean;
|
||||||
|
side?: ComponentChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputDate<T>({
|
export function InputDate<T>({
|
||||||
@ -41,9 +43,11 @@ export function InputDate<T>({
|
|||||||
tooltip,
|
tooltip,
|
||||||
expand,
|
expand,
|
||||||
withTimestampSupport,
|
withTimestampSupport,
|
||||||
|
side,
|
||||||
}: Props<keyof T>): VNode {
|
}: Props<keyof T>): VNode {
|
||||||
const [opened, setOpened] = useState(false);
|
const [opened, setOpened] = useState(false);
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings] = useSettings()
|
||||||
|
|
||||||
const { error, required, value, onChange } = useField<T>(name);
|
const { error, required, value, onChange } = useField<T>(name);
|
||||||
|
|
||||||
@ -51,14 +55,14 @@ export function InputDate<T>({
|
|||||||
if (!value) {
|
if (!value) {
|
||||||
strValue = withTimestampSupport ? "unknown" : "";
|
strValue = withTimestampSupport ? "unknown" : "";
|
||||||
} else if (value instanceof Date) {
|
} else if (value instanceof Date) {
|
||||||
strValue = format(value, "yyyy/MM/dd");
|
strValue = format(value, dateFormatForSettings(settings));
|
||||||
} else if (value.t_s) {
|
} else if (value.t_s) {
|
||||||
strValue =
|
strValue =
|
||||||
value.t_s === "never"
|
value.t_s === "never"
|
||||||
? withTimestampSupport
|
? withTimestampSupport
|
||||||
? "never"
|
? "never"
|
||||||
: ""
|
: ""
|
||||||
: format(new Date(value.t_s * 1000), "yyyy/MM/dd");
|
: format(new Date(value.t_s * 1000), dateFormatForSettings(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -142,6 +146,7 @@ export function InputDate<T>({
|
|||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{side}
|
||||||
</div>
|
</div>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
opened={opened}
|
opened={opened}
|
||||||
|
@ -18,9 +18,9 @@
|
|||||||
*
|
*
|
||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useCallback, useState } from "preact/hooks";
|
|
||||||
import { COUNTRY_TABLE } from "../../utils/constants.js";
|
import { COUNTRY_TABLE } from "../../utils/constants.js";
|
||||||
import { undefinedIfEmpty } from "../../utils/table.js";
|
import { undefinedIfEmpty } from "../../utils/table.js";
|
||||||
import { FormErrors, FormProvider } from "./FormProvider.js";
|
import { FormErrors, FormProvider } from "./FormProvider.js";
|
||||||
@ -28,23 +28,23 @@ import { Input } from "./Input.js";
|
|||||||
import { InputGroup } from "./InputGroup.js";
|
import { InputGroup } from "./InputGroup.js";
|
||||||
import { InputSelector } from "./InputSelector.js";
|
import { InputSelector } from "./InputSelector.js";
|
||||||
import { InputProps, useField } from "./useField.js";
|
import { InputProps, useField } from "./useField.js";
|
||||||
import { InputWithAddon } from "./InputWithAddon.js";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { MerchantBackend } from "../../declaration.js";
|
|
||||||
|
|
||||||
export interface Props<T> extends InputProps<T> {
|
export interface Props<T> extends InputProps<T> {
|
||||||
isValid?: (e: any) => boolean;
|
isValid?: (e: any) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type Entity = PaytoUriGeneric
|
||||||
// https://datatracker.ietf.org/doc/html/rfc8905
|
// https://datatracker.ietf.org/doc/html/rfc8905
|
||||||
type Entity = {
|
type Entity = {
|
||||||
// iban, bitcoin, x-taler-bank. it defined the format
|
// iban, bitcoin, x-taler-bank. it defined the format
|
||||||
target: string;
|
target: string;
|
||||||
// path1 if the first field to be used
|
// path1 if the first field to be used
|
||||||
path1: string;
|
path1?: string;
|
||||||
// path2 if the second field to be used, optional
|
// path2 if the second field to be used, optional
|
||||||
path2?: string;
|
path2?: string;
|
||||||
// options of the payto uri
|
// params of the payto uri
|
||||||
options: {
|
params: {
|
||||||
"receiver-name"?: string;
|
"receiver-name"?: string;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
@ -52,13 +52,6 @@ type Entity = {
|
|||||||
instruction?: string;
|
instruction?: string;
|
||||||
[name: string]: string | undefined;
|
[name: string]: string | undefined;
|
||||||
};
|
};
|
||||||
auth: {
|
|
||||||
type: "unset" | "basic" | "none";
|
|
||||||
url?: string;
|
|
||||||
username?: string;
|
|
||||||
password?: string;
|
|
||||||
repeat?: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function isEthereumAddress(address: string) {
|
function isEthereumAddress(address: string) {
|
||||||
@ -171,14 +164,10 @@ const targets = [
|
|||||||
"bitcoin",
|
"bitcoin",
|
||||||
"ethereum",
|
"ethereum",
|
||||||
];
|
];
|
||||||
const accountAuthType = ["none", "basic"];
|
|
||||||
const noTargetValue = targets[0];
|
const noTargetValue = targets[0];
|
||||||
const defaultTarget: Partial<Entity> = {
|
const defaultTarget: Entity = {
|
||||||
target: noTargetValue,
|
target: noTargetValue,
|
||||||
options: {},
|
params: {},
|
||||||
auth: {
|
|
||||||
type: "unset" as const,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function InputPaytoForm<T>({
|
export function InputPaytoForm<T>({
|
||||||
@ -187,110 +176,91 @@ export function InputPaytoForm<T>({
|
|||||||
label,
|
label,
|
||||||
tooltip,
|
tooltip,
|
||||||
}: Props<keyof T>): VNode {
|
}: Props<keyof T>): VNode {
|
||||||
const { value: paytos, onChange, required } = useField<T>(name);
|
const { value: initialValueStr, onChange } = useField<T>(name);
|
||||||
|
|
||||||
const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget);
|
const initialPayto = parsePaytoUri(initialValueStr ?? "")
|
||||||
|
const paths = !initialPayto ? [] : initialPayto.targetPath.split("/")
|
||||||
let payToPath;
|
const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
|
||||||
if (value.target === "iban" && value.path1) {
|
const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
|
||||||
payToPath = `/${value.path1.toUpperCase()}`;
|
const initial: Entity = initialPayto === undefined ? defaultTarget : {
|
||||||
} else if (value.path1) {
|
target: initialPayto.targetType,
|
||||||
if (value.path2) {
|
params: initialPayto.params,
|
||||||
payToPath = `/${value.path1}/${value.path2}`;
|
path1: initialPath1,
|
||||||
} else {
|
path2: initialPath2,
|
||||||
payToPath = `/${value.path1}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const [value, setValue] = useState<Partial<Entity>>(initial)
|
||||||
|
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
const ops = value.options ?? {};
|
|
||||||
const url = tryUrl(`payto://${value.target}${payToPath}`);
|
|
||||||
if (url) {
|
|
||||||
Object.keys(ops).forEach((opt_key) => {
|
|
||||||
const opt_value = ops[opt_key];
|
|
||||||
if (opt_value) url.searchParams.set(opt_key, opt_value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const paytoURL = !url ? "" : url.href;
|
|
||||||
|
|
||||||
const errors: FormErrors<Entity> = {
|
const errors: FormErrors<Entity> = {
|
||||||
target:
|
target:
|
||||||
value.target === noTargetValue && !paytos.length
|
value.target === noTargetValue
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: undefined,
|
: undefined,
|
||||||
path1: !value.path1
|
path1: !value.path1
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: value.target === "iban"
|
: value.target === "iban"
|
||||||
? validateIBAN(value.path1, i18n)
|
? validateIBAN(value.path1, i18n)
|
||||||
: value.target === "bitcoin"
|
: value.target === "bitcoin"
|
||||||
? validateBitcoin(value.path1, i18n)
|
? validateBitcoin(value.path1, i18n)
|
||||||
: value.target === "ethereum"
|
: value.target === "ethereum"
|
||||||
? validateEthereum(value.path1, i18n)
|
? validateEthereum(value.path1, i18n)
|
||||||
: undefined,
|
: undefined,
|
||||||
path2:
|
path2:
|
||||||
value.target === "x-taler-bank"
|
value.target === "x-taler-bank"
|
||||||
? !value.path2
|
? !value.path2
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: undefined
|
: undefined
|
||||||
: undefined,
|
: undefined,
|
||||||
options: undefinedIfEmpty({
|
params: undefinedIfEmpty({
|
||||||
"receiver-name": !value.options?.["receiver-name"]
|
"receiver-name": !value.params?.["receiver-name"]
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: undefined,
|
: undefined,
|
||||||
}),
|
}),
|
||||||
auth: !value.auth
|
|
||||||
? undefined
|
|
||||||
: undefinedIfEmpty({
|
|
||||||
username:
|
|
||||||
value.auth.type === "basic" && !value.auth.username
|
|
||||||
? i18n.str`required`
|
|
||||||
: undefined,
|
|
||||||
password:
|
|
||||||
value.auth.type === "basic" && !value.auth.password
|
|
||||||
? i18n.str`required`
|
|
||||||
: undefined,
|
|
||||||
repeat:
|
|
||||||
value.auth.type === "basic" && !value.auth.repeat
|
|
||||||
? i18n.str`required`
|
|
||||||
: value.auth.repeat !== value.auth.password
|
|
||||||
? i18n.str`is not the same`
|
|
||||||
: undefined,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasErrors = Object.keys(errors).some(
|
const hasErrors = Object.keys(errors).some(
|
||||||
(k) => (errors as any)[k] !== undefined,
|
(k) => (errors as any)[k] !== undefined,
|
||||||
);
|
);
|
||||||
|
const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({
|
||||||
|
targetType: value.target,
|
||||||
|
targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""),
|
||||||
|
params: value.params ?? {} as any,
|
||||||
|
isKnown: false,
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
onChange(str as any)
|
||||||
|
}, [str])
|
||||||
|
|
||||||
const submit = useCallback((): void => {
|
// const submit = useCallback((): void => {
|
||||||
const accounts: MerchantBackend.Instances.MerchantBankAccount[] = paytos;
|
// // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos;
|
||||||
const alreadyExists =
|
// // const alreadyExists =
|
||||||
accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
|
// // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
|
||||||
if (!alreadyExists) {
|
// // if (!alreadyExists) {
|
||||||
const newValue: MerchantBackend.Instances.MerchantBankAccount = {
|
// const newValue: MerchantBackend.BankAccounts.AccountAddDetails = {
|
||||||
payto_uri: paytoURL,
|
// payto_uri: paytoURL,
|
||||||
};
|
// };
|
||||||
if (value.auth) {
|
// if (value.auth) {
|
||||||
if (value.auth.url) {
|
// if (value.auth.url) {
|
||||||
newValue.credit_facade_url = value.auth.url;
|
// newValue.credit_facade_url = value.auth.url;
|
||||||
}
|
// }
|
||||||
if (value.auth.type === "none") {
|
// if (value.auth.type === "none") {
|
||||||
newValue.credit_facade_credentials = {
|
// newValue.credit_facade_credentials = {
|
||||||
type: "none",
|
// type: "none",
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
if (value.auth.type === "basic") {
|
// if (value.auth.type === "basic") {
|
||||||
newValue.credit_facade_credentials = {
|
// newValue.credit_facade_credentials = {
|
||||||
type: "basic",
|
// type: "basic",
|
||||||
username: value.auth.username ?? "",
|
// username: value.auth.username ?? "",
|
||||||
password: value.auth.password ?? "",
|
// password: value.auth.password ?? "",
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
onChange([newValue, ...accounts] as any);
|
// onChange(newValue as any);
|
||||||
}
|
// // }
|
||||||
valueHandler(defaultTarget);
|
// // valueHandler(defaultTarget);
|
||||||
}, [value]);
|
// }, [value]);
|
||||||
|
|
||||||
//FIXME: translating plural singular
|
//FIXME: translating plural singular
|
||||||
return (
|
return (
|
||||||
@ -299,11 +269,11 @@ export function InputPaytoForm<T>({
|
|||||||
name="tax"
|
name="tax"
|
||||||
errors={errors}
|
errors={errors}
|
||||||
object={value}
|
object={value}
|
||||||
valueHandler={valueHandler}
|
valueHandler={setValue}
|
||||||
>
|
>
|
||||||
<InputSelector<Entity>
|
<InputSelector<Entity>
|
||||||
name="target"
|
name="target"
|
||||||
label={i18n.str`Target type`}
|
label={i18n.str`Account type`}
|
||||||
tooltip={i18n.str`Method to use for wire transfer`}
|
tooltip={i18n.str`Method to use for wire transfer`}
|
||||||
values={targets}
|
values={targets}
|
||||||
toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)}
|
toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)}
|
||||||
@ -400,150 +370,15 @@ export function InputPaytoForm<T>({
|
|||||||
{value.target !== noTargetValue && (
|
{value.target !== noTargetValue && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Input
|
<Input
|
||||||
name="options.receiver-name"
|
name="params.receiver-name"
|
||||||
label={i18n.str`Name`}
|
label={i18n.str`Name`}
|
||||||
tooltip={i18n.str`Bank account owner's name.`}
|
tooltip={i18n.str`Bank account owner's name.`}
|
||||||
/>
|
/>
|
||||||
<InputWithAddon
|
|
||||||
name="auth.url"
|
|
||||||
label={i18n.str`Account info URL`}
|
|
||||||
help="https://bank.com"
|
|
||||||
expand
|
|
||||||
tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
|
|
||||||
/>
|
|
||||||
<InputSelector
|
|
||||||
name="auth.type"
|
|
||||||
label={i18n.str`Auth type`}
|
|
||||||
tooltip={i18n.str`Choose the authentication type for the account info URL`}
|
|
||||||
values={accountAuthType}
|
|
||||||
toStr={(str) => {
|
|
||||||
// if (str === "unset") {
|
|
||||||
// return "Without change";
|
|
||||||
// }
|
|
||||||
if (str === "none") return "Without authentication";
|
|
||||||
return "Username and password";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{value.auth?.type === "basic" ? (
|
|
||||||
<Fragment>
|
|
||||||
<Input
|
|
||||||
name="auth.username"
|
|
||||||
label={i18n.str`Username`}
|
|
||||||
tooltip={i18n.str`Username to access the account information.`}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="auth.password"
|
|
||||||
inputType="password"
|
|
||||||
label={i18n.str`Password`}
|
|
||||||
tooltip={i18n.str`Password to access the account information.`}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="auth.repeat"
|
|
||||||
inputType="password"
|
|
||||||
label={i18n.str`Repeat password`}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{/* <InputWithAddon
|
|
||||||
name="options.credit_credentials"
|
|
||||||
label={i18n.str`Account info`}
|
|
||||||
inputType={showKey ? "text" : "password"}
|
|
||||||
help="From where the merchant can download information about incoming wire transfers to this account"
|
|
||||||
expand
|
|
||||||
tooltip={i18n.str`Useful to validate the purchase`}
|
|
||||||
fromStr={(v) => v.toUpperCase()}
|
|
||||||
addonAfter={
|
|
||||||
<span class="icon">
|
|
||||||
{showKey ? (
|
|
||||||
<i class="mdi mdi-eye" />
|
|
||||||
) : (
|
|
||||||
<i class="mdi mdi-eye-off" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
side={
|
|
||||||
<span style={{ display: "flex" }}>
|
|
||||||
<button
|
|
||||||
data-tooltip={
|
|
||||||
showKey
|
|
||||||
? i18n.str`show secret key`
|
|
||||||
: i18n.str`hide secret key`
|
|
||||||
}
|
|
||||||
class="button is-info mr-3"
|
|
||||||
onClick={(e) => {
|
|
||||||
setShowKey(!showKey);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showKey ? (
|
|
||||||
<i18n.Translate>hide</i18n.Translate>
|
|
||||||
) : (
|
|
||||||
<i18n.Translate>show</i18n.Translate>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/> */}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
{/**
|
|
||||||
* Show the values in the list
|
|
||||||
*/}
|
|
||||||
<div class="field is-horizontal">
|
|
||||||
<div class="field-label is-normal" />
|
|
||||||
<div class="field-body" style={{ display: "block" }}>
|
|
||||||
{paytos.map(
|
|
||||||
(v: MerchantBackend.Instances.MerchantBankAccount, i: number) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
class="tags has-addons mt-3 mb-0 mr-3"
|
|
||||||
style={{ flexWrap: "nowrap" }}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="tag is-medium is-info mb-0"
|
|
||||||
style={{ maxWidth: "90%" }}
|
|
||||||
>
|
|
||||||
{v.payto_uri}
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
class="tag is-medium is-danger is-delete mb-0"
|
|
||||||
onClick={() => {
|
|
||||||
onChange(paytos.filter((f: any) => f !== v) as any);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
{!paytos.length && i18n.str`No accounts yet.`}
|
|
||||||
{required && (
|
|
||||||
<span class="icon has-text-danger is-right">
|
|
||||||
<i class="mdi mdi-alert" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{value.target !== noTargetValue && (
|
|
||||||
<div class="buttons is-right mt-5">
|
|
||||||
<button
|
|
||||||
class="button is-info"
|
|
||||||
data-tooltip={i18n.str`add tax to the tax list`}
|
|
||||||
disabled={hasErrors}
|
|
||||||
onClick={submit}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Add</i18n.Translate>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryUrl(s: string): URL | undefined {
|
|
||||||
try {
|
|
||||||
return new URL(s);
|
|
||||||
} catch (e) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -22,32 +22,41 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
|||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import emptyImage from "../../assets/empty.png";
|
import emptyImage from "../../assets/empty.png";
|
||||||
import { MerchantBackend, WithId } from "../../declaration.js";
|
|
||||||
import { FormErrors, FormProvider } from "./FormProvider.js";
|
import { FormErrors, FormProvider } from "./FormProvider.js";
|
||||||
import { InputWithAddon } from "./InputWithAddon.js";
|
import { InputWithAddon } from "./InputWithAddon.js";
|
||||||
|
import { TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Products.ProductDetail & WithId;
|
type Entity = {
|
||||||
|
id: string,
|
||||||
|
description: string;
|
||||||
|
image?: string;
|
||||||
|
extra?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface Props {
|
export interface Props<T extends Entity> {
|
||||||
selected?: Entity;
|
selected?: T;
|
||||||
onChange: (p?: Entity) => void;
|
onChange: (p?: T) => void;
|
||||||
products: (MerchantBackend.Products.ProductDetail & WithId)[];
|
label: TranslatedString;
|
||||||
|
list: T[];
|
||||||
|
withImage?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductSearch {
|
interface Search {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputSearchProduct({
|
export function InputSearchOnList<T extends Entity>({
|
||||||
selected,
|
selected,
|
||||||
onChange,
|
onChange,
|
||||||
products,
|
label,
|
||||||
}: Props): VNode {
|
list,
|
||||||
const [prodForm, setProdName] = useState<Partial<ProductSearch>>({
|
withImage,
|
||||||
|
}: Props<T>): VNode {
|
||||||
|
const [nameForm, setNameForm] = useState<Partial<Search>>({
|
||||||
name: "",
|
name: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const errors: FormErrors<ProductSearch> = {
|
const errors: FormErrors<Search> = {
|
||||||
name: undefined,
|
name: undefined,
|
||||||
};
|
};
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
@ -55,15 +64,17 @@ export function InputSearchProduct({
|
|||||||
if (selected) {
|
if (selected) {
|
||||||
return (
|
return (
|
||||||
<article class="media">
|
<article class="media">
|
||||||
<figure class="media-left">
|
{withImage &&
|
||||||
<p class="image is-128x128">
|
<figure class="media-left">
|
||||||
<img src={selected.image ? selected.image : emptyImage} />
|
<p class="image is-128x128">
|
||||||
</p>
|
<img src={selected.image ? selected.image : emptyImage} />
|
||||||
</figure>
|
</p>
|
||||||
|
</figure>
|
||||||
|
}
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p class="media-meta">
|
<p class="media-meta">
|
||||||
<i18n.Translate>Product id</i18n.Translate>: <b>{selected.id}</b>
|
<i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<i18n.Translate>Description</i18n.Translate>:{" "}
|
<i18n.Translate>Description</i18n.Translate>:{" "}
|
||||||
@ -84,15 +95,15 @@ export function InputSearchProduct({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider<ProductSearch>
|
<FormProvider<Search>
|
||||||
errors={errors}
|
errors={errors}
|
||||||
object={prodForm}
|
object={nameForm}
|
||||||
valueHandler={setProdName}
|
valueHandler={setNameForm}
|
||||||
>
|
>
|
||||||
<InputWithAddon<ProductSearch>
|
<InputWithAddon<Search>
|
||||||
name="name"
|
name="name"
|
||||||
label={i18n.str`Product`}
|
label={label}
|
||||||
tooltip={i18n.str`search products by it's description or id`}
|
tooltip={i18n.str`enter description or id`}
|
||||||
addonAfter={
|
addonAfter={
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="mdi mdi-magnify" />
|
<i class="mdi mdi-magnify" />
|
||||||
@ -100,13 +111,14 @@ export function InputSearchProduct({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<ProductList
|
<DropdownList
|
||||||
name={prodForm.name}
|
name={nameForm.name}
|
||||||
list={products}
|
list={list}
|
||||||
onSelect={(p) => {
|
onSelect={(p) => {
|
||||||
setProdName({ name: "" });
|
setNameForm({ name: "" });
|
||||||
onChange(p);
|
onChange(p);
|
||||||
}}
|
}}
|
||||||
|
withImage={!!withImage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</InputWithAddon>
|
</InputWithAddon>
|
||||||
@ -114,13 +126,14 @@ export function InputSearchProduct({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductListProps {
|
interface DropdownListProps<T extends Entity> {
|
||||||
name?: string;
|
name?: string;
|
||||||
onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void;
|
onSelect: (p: T) => void;
|
||||||
list: (MerchantBackend.Products.ProductDetail & WithId)[];
|
list: T[];
|
||||||
|
withImage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProductList({ name, onSelect, list }: ProductListProps) {
|
function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
/* FIXME
|
/* FIXME
|
||||||
@ -149,7 +162,7 @@ function ProductList({ name, onSelect, list }: ProductListProps) {
|
|||||||
{!filtered.length ? (
|
{!filtered.length ? (
|
||||||
<div class="dropdown-item">
|
<div class="dropdown-item">
|
||||||
<i18n.Translate>
|
<i18n.Translate>
|
||||||
no products found with that description
|
no match found with that description or id
|
||||||
</i18n.Translate>
|
</i18n.Translate>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -161,18 +174,20 @@ function ProductList({ name, onSelect, list }: ProductListProps) {
|
|||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<article class="media">
|
<article class="media">
|
||||||
<div class="media-left">
|
{withImage &&
|
||||||
<div class="image" style={{ minWidth: 64 }}>
|
<div class="media-left">
|
||||||
<img
|
<div class="image" style={{ minWidth: 64 }}>
|
||||||
src={p.image ? p.image : emptyImage}
|
<img
|
||||||
style={{ width: 64, height: 64 }}
|
src={p.image ? p.image : emptyImage}
|
||||||
/>
|
style={{ width: 64, height: 64 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>
|
<p>
|
||||||
<strong>{p.id}</strong> <small>{p.price}</small>
|
<strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined}
|
||||||
<br />
|
<br />
|
||||||
{p.description}
|
{p.description}
|
||||||
</p>
|
</p>
|
@ -56,7 +56,7 @@ export function InputToggle<T>({
|
|||||||
return (
|
return (
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-label is-normal">
|
<div class="field-label is-normal">
|
||||||
<label class="label" style={{ width: 200 }}>
|
<label class="label" >
|
||||||
{label}
|
{label}
|
||||||
{tooltip && (
|
{tooltip && (
|
||||||
<span class="icon has-tooltip-right" data-tooltip={tooltip}>
|
<span class="icon has-tooltip-right" data-tooltip={tooltip}>
|
||||||
@ -65,7 +65,7 @@ export function InputToggle<T>({
|
|||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-body is-flex-grow-1">
|
<div class="field-body is-flex-grow-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<p class={expand ? "control is-expanded" : "control"}>
|
<p class={expand ? "control is-expanded" : "control"}>
|
||||||
<label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
|
<label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
|
||||||
|
@ -24,14 +24,13 @@ import { Fragment, h, VNode } from "preact";
|
|||||||
import { useBackendContext } from "../../context/backend.js";
|
import { useBackendContext } from "../../context/backend.js";
|
||||||
import { Entity } from "../../paths/admin/create/CreatePage.js";
|
import { Entity } from "../../paths/admin/create/CreatePage.js";
|
||||||
import { Input } from "../form/Input.js";
|
import { Input } from "../form/Input.js";
|
||||||
import { InputCurrency } from "../form/InputCurrency.js";
|
|
||||||
import { InputDuration } from "../form/InputDuration.js";
|
import { InputDuration } from "../form/InputDuration.js";
|
||||||
import { InputGroup } from "../form/InputGroup.js";
|
import { InputGroup } from "../form/InputGroup.js";
|
||||||
import { InputImage } from "../form/InputImage.js";
|
import { InputImage } from "../form/InputImage.js";
|
||||||
import { InputLocation } from "../form/InputLocation.js";
|
import { InputLocation } from "../form/InputLocation.js";
|
||||||
import { InputPaytoForm } from "../form/InputPaytoForm.js";
|
|
||||||
import { InputWithAddon } from "../form/InputWithAddon.js";
|
|
||||||
import { InputSelector } from "../form/InputSelector.js";
|
import { InputSelector } from "../form/InputSelector.js";
|
||||||
|
import { InputToggle } from "../form/InputToggle.js";
|
||||||
|
import { InputWithAddon } from "../form/InputWithAddon.js";
|
||||||
|
|
||||||
export function DefaultInstanceFormFields({
|
export function DefaultInstanceFormFields({
|
||||||
readonlyId,
|
readonlyId,
|
||||||
@ -85,28 +84,10 @@ export function DefaultInstanceFormFields({
|
|||||||
tooltip={i18n.str`Logo image.`}
|
tooltip={i18n.str`Logo image.`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputPaytoForm<Entity>
|
<InputToggle<Entity>
|
||||||
name="accounts"
|
name="use_stefan"
|
||||||
label={i18n.str`Bank account`}
|
label={i18n.str`Pay transaction fee`}
|
||||||
tooltip={i18n.str`URI specifying bank account for crediting revenue.`}
|
tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`}
|
||||||
/>
|
|
||||||
|
|
||||||
<InputCurrency<Entity>
|
|
||||||
name="default_max_deposit_fee"
|
|
||||||
label={i18n.str`Default max deposit fee`}
|
|
||||||
tooltip={i18n.str`Maximum deposit fees this merchant is willing to pay per order by default.`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InputCurrency<Entity>
|
|
||||||
name="default_max_wire_fee"
|
|
||||||
label={i18n.str`Default max wire fee`}
|
|
||||||
tooltip={i18n.str`Maximum wire fees this merchant is willing to pay per wire transfer by default.`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input<Entity>
|
|
||||||
name="default_wire_fee_amortization"
|
|
||||||
label={i18n.str`Default wire fee amortization`}
|
|
||||||
tooltip={i18n.str`Number of orders excess wire transfer fees will be divided by to compute per order surcharge.`}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputGroup
|
<InputGroup
|
||||||
|
@ -25,6 +25,7 @@ import { useBackendContext } from "../../context/backend.js";
|
|||||||
import { useConfigContext } from "../../context/config.js";
|
import { useConfigContext } from "../../context/config.js";
|
||||||
import { useInstanceKYCDetails } from "../../hooks/instance.js";
|
import { useInstanceKYCDetails } from "../../hooks/instance.js";
|
||||||
import { LangSelector } from "./LangSelector.js";
|
import { LangSelector } from "./LangSelector.js";
|
||||||
|
import { useCredentialsChecker } from "../../hooks/backend.js";
|
||||||
|
|
||||||
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
|
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
|
||||||
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
|
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
|
||||||
@ -36,6 +37,7 @@ interface Props {
|
|||||||
instance: string;
|
instance: string;
|
||||||
admin?: boolean;
|
admin?: boolean;
|
||||||
mimic?: boolean;
|
mimic?: boolean;
|
||||||
|
isPasswordOk: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({
|
export function Sidebar({
|
||||||
@ -45,6 +47,7 @@ export function Sidebar({
|
|||||||
onLogout,
|
onLogout,
|
||||||
admin,
|
admin,
|
||||||
mimic,
|
mimic,
|
||||||
|
isPasswordOk
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const config = useConfigContext();
|
const config = useConfigContext();
|
||||||
const backend = useBackendContext();
|
const backend = useBackendContext();
|
||||||
@ -53,7 +56,7 @@ export function Sidebar({
|
|||||||
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
|
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside class="aside is-placed-left is-expanded">
|
<aside class="aside is-placed-left is-expanded" style={{ overflowY: "scroll" }}>
|
||||||
{mobile && (
|
{mobile && (
|
||||||
<div
|
<div
|
||||||
class="footer"
|
class="footer"
|
||||||
@ -78,10 +81,10 @@ export function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu is-menu-main">
|
<div class="menu is-menu-main">
|
||||||
{instance ? (
|
{isPasswordOk && instance ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
<a href={"/orders"} class="has-icon">
|
<a href={"/orders"} class="has-icon">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="mdi mdi-cash-register" />
|
<i class="mdi mdi-cash-register" />
|
||||||
@ -104,7 +107,7 @@ export function Sidebar({
|
|||||||
<li>
|
<li>
|
||||||
<a href={"/transfers"} class="has-icon">
|
<a href={"/transfers"} class="has-icon">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="mdi mdi-bank" />
|
<i class="mdi mdi-arrow-left-right" />
|
||||||
</span>
|
</span>
|
||||||
<span class="menu-item-label">
|
<span class="menu-item-label">
|
||||||
<i18n.Translate>Transfers</i18n.Translate>
|
<i18n.Translate>Transfers</i18n.Translate>
|
||||||
@ -137,12 +140,22 @@ export function Sidebar({
|
|||||||
</p>
|
</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li>
|
<li>
|
||||||
<a href={"/update"} class="has-icon">
|
<a href={"/bank"} class="has-icon">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="mdi mdi-square-edit-outline" />
|
<i class="mdi mdi-bank" />
|
||||||
</span>
|
</span>
|
||||||
<span class="menu-item-label">
|
<span class="menu-item-label">
|
||||||
<i18n.Translate>Account</i18n.Translate>
|
<i18n.Translate>Bank account</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={"/validators"} class="has-icon">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="mdi mdi-lock" />
|
||||||
|
</span>
|
||||||
|
<span class="menu-item-label">
|
||||||
|
<i18n.Translate>Validators</i18n.Translate>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -164,6 +177,26 @@ export function Sidebar({
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={"/server"} class="has-icon">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="mdi mdi-square-edit-outline" />
|
||||||
|
</span>
|
||||||
|
<span class="menu-item-label">
|
||||||
|
<i18n.Translate>Server</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={"/token"} class="has-icon">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="mdi mdi-security" />
|
||||||
|
</span>
|
||||||
|
<span class="menu-item-label">
|
||||||
|
<i18n.Translate>Access token</i18n.Translate>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
@ -174,12 +207,12 @@ export function Sidebar({
|
|||||||
<li>
|
<li>
|
||||||
<a class="has-icon is-state-info is-hoverable"
|
<a class="has-icon is-state-info is-hoverable"
|
||||||
onClick={(): void => onShowSettings()}
|
onClick={(): void => onShowSettings()}
|
||||||
>
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="mdi mdi-newspaper" />
|
<i class="mdi mdi-newspaper" />
|
||||||
</span>
|
</span>
|
||||||
<span class="menu-item-label">
|
<span class="menu-item-label">
|
||||||
<i18n.Translate>Settings</i18n.Translate>
|
<i18n.Translate>Interface</i18n.Translate>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -211,7 +244,7 @@ export function Sidebar({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{admin && !mimic && (
|
{isPasswordOk && admin && !mimic && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<p class="menu-label">
|
<p class="menu-label">
|
||||||
<i18n.Translate>Instances</i18n.Translate>
|
<i18n.Translate>Instances</i18n.Translate>
|
||||||
@ -238,19 +271,21 @@ export function Sidebar({
|
|||||||
</li>
|
</li>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
<li>
|
{isPasswordOk &&
|
||||||
<a
|
<li>
|
||||||
class="has-icon is-state-info is-hoverable"
|
<a
|
||||||
onClick={(): void => onLogout()}
|
class="has-icon is-state-info is-hoverable"
|
||||||
>
|
onClick={(): void => onLogout()}
|
||||||
<span class="icon">
|
>
|
||||||
<i class="mdi mdi-logout default" />
|
<span class="icon">
|
||||||
</span>
|
<i class="mdi mdi-logout default" />
|
||||||
<span class="menu-item-label">
|
</span>
|
||||||
<i18n.Translate>Log out</i18n.Translate>
|
<span class="menu-item-label">
|
||||||
</span>
|
<i18n.Translate>Log out</i18n.Translate>
|
||||||
</a>
|
</span>
|
||||||
</li>
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -24,7 +24,7 @@ import { Sidebar } from "./SideBar.js";
|
|||||||
|
|
||||||
function getInstanceTitle(path: string, id: string): string {
|
function getInstanceTitle(path: string, id: string): string {
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case InstancePaths.update:
|
case InstancePaths.server:
|
||||||
return `${id}: Settings`;
|
return `${id}: Settings`;
|
||||||
case InstancePaths.order_list:
|
case InstancePaths.order_list:
|
||||||
return `${id}: Orders`;
|
return `${id}: Orders`;
|
||||||
@ -50,6 +50,12 @@ function getInstanceTitle(path: string, id: string): string {
|
|||||||
return `${id}: New webhook`;
|
return `${id}: New webhook`;
|
||||||
case InstancePaths.webhooks_update:
|
case InstancePaths.webhooks_update:
|
||||||
return `${id}: Update webhook`;
|
return `${id}: Update webhook`;
|
||||||
|
case InstancePaths.validators_list:
|
||||||
|
return `${id}: Validators`;
|
||||||
|
case InstancePaths.validators_new:
|
||||||
|
return `${id}: New validator`;
|
||||||
|
case InstancePaths.validators_update:
|
||||||
|
return `${id}: Update validators`;
|
||||||
case InstancePaths.templates_new:
|
case InstancePaths.templates_new:
|
||||||
return `${id}: New template`;
|
return `${id}: New template`;
|
||||||
case InstancePaths.templates_update:
|
case InstancePaths.templates_update:
|
||||||
@ -58,6 +64,10 @@ function getInstanceTitle(path: string, id: string): string {
|
|||||||
return `${id}: Templates`;
|
return `${id}: Templates`;
|
||||||
case InstancePaths.templates_use:
|
case InstancePaths.templates_use:
|
||||||
return `${id}: Use template`;
|
return `${id}: Use template`;
|
||||||
|
case InstancePaths.settings:
|
||||||
|
return `${id}: Interface`;
|
||||||
|
case InstancePaths.settings:
|
||||||
|
return `${id}: Interface`;
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -77,6 +87,7 @@ interface MenuProps {
|
|||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
onShowSettings: () => void;
|
onShowSettings: () => void;
|
||||||
setInstanceName: (s: string) => void;
|
setInstanceName: (s: string) => void;
|
||||||
|
isPasswordOk: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WithTitle({
|
function WithTitle({
|
||||||
@ -100,14 +111,15 @@ export function Menu({
|
|||||||
path,
|
path,
|
||||||
admin,
|
admin,
|
||||||
setInstanceName,
|
setInstanceName,
|
||||||
|
isPasswordOk
|
||||||
}: MenuProps): VNode {
|
}: MenuProps): VNode {
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
const titleWithSubtitle = title
|
const titleWithSubtitle = title
|
||||||
? title
|
? title
|
||||||
: !admin
|
: !admin
|
||||||
? getInstanceTitle(path, instance)
|
? getInstanceTitle(path, instance)
|
||||||
: getAdminTitle(path, instance);
|
: getAdminTitle(path, instance);
|
||||||
const adminInstance = instance === "default";
|
const adminInstance = instance === "default";
|
||||||
const mimic = admin && !adminInstance;
|
const mimic = admin && !adminInstance;
|
||||||
return (
|
return (
|
||||||
@ -129,14 +141,15 @@ export function Menu({
|
|||||||
mimic={mimic}
|
mimic={mimic}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
mobile={mobileOpen}
|
mobile={mobileOpen}
|
||||||
|
isPasswordOk={isPasswordOk}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mimic && (
|
{mimic && (
|
||||||
<nav class="level" style={{
|
<nav class="level" style={{
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
position:"fixed",
|
position: "fixed",
|
||||||
width:"50%",
|
width: "50%",
|
||||||
marginLeft: "20%"
|
marginLeft: "20%"
|
||||||
}}>
|
}}>
|
||||||
<div class="level-item has-text-centered has-background-warning">
|
<div class="level-item has-text-centered has-background-warning">
|
||||||
@ -161,8 +174,9 @@ export function Menu({
|
|||||||
|
|
||||||
interface NotYetReadyAppMenuProps {
|
interface NotYetReadyAppMenuProps {
|
||||||
title: string;
|
title: string;
|
||||||
onLogout?: () => void;
|
|
||||||
onShowSettings: () => void;
|
onShowSettings: () => void;
|
||||||
|
onLogout?: () => void;
|
||||||
|
isPasswordOk: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotifProps {
|
interface NotifProps {
|
||||||
@ -181,8 +195,8 @@ export function NotificationCard({
|
|||||||
n.type === "ERROR"
|
n.type === "ERROR"
|
||||||
? "message is-danger"
|
? "message is-danger"
|
||||||
: n.type === "WARN"
|
: n.type === "WARN"
|
||||||
? "message is-warning"
|
? "message is-warning"
|
||||||
: "message is-info"
|
: "message is-info"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
@ -201,10 +215,36 @@ export function NotificationCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NotConnectedAppMenuProps {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
export function NotConnectedAppMenu({
|
||||||
|
title,
|
||||||
|
}: NotConnectedAppMenuProps): VNode {
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = `Taler Backoffice: ${title}`;
|
||||||
|
}, [title]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={mobileOpen ? "has-aside-mobile-expanded" : ""}
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
|
>
|
||||||
|
<NavigationBar
|
||||||
|
onMobileMenu={() => setMobileOpen(!mobileOpen)}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function NotYetReadyAppMenu({
|
export function NotYetReadyAppMenu({
|
||||||
onLogout,
|
onLogout,
|
||||||
onShowSettings,
|
onShowSettings,
|
||||||
title,
|
title,
|
||||||
|
isPasswordOk
|
||||||
}: NotYetReadyAppMenuProps): VNode {
|
}: NotYetReadyAppMenuProps): VNode {
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
@ -222,7 +262,7 @@ export function NotYetReadyAppMenu({
|
|||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
{onLogout && (
|
{onLogout && (
|
||||||
<Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} />
|
<Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -20,7 +20,7 @@ import { MerchantBackend, WithId } from "../../declaration.js";
|
|||||||
import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js";
|
import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js";
|
||||||
import { FormErrors, FormProvider } from "../form/FormProvider.js";
|
import { FormErrors, FormProvider } from "../form/FormProvider.js";
|
||||||
import { InputNumber } from "../form/InputNumber.js";
|
import { InputNumber } from "../form/InputNumber.js";
|
||||||
import { InputSearchProduct } from "../form/InputSearchProduct.js";
|
import { InputSearchOnList } from "../form/InputSearchOnList.js";
|
||||||
|
|
||||||
type Form = {
|
type Form = {
|
||||||
product: MerchantBackend.Products.ProductDetail & WithId;
|
product: MerchantBackend.Products.ProductDetail & WithId;
|
||||||
@ -95,10 +95,12 @@ export function InventoryProductForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider<Form> errors={errors} object={state} valueHandler={setState}>
|
<FormProvider<Form> errors={errors} object={state} valueHandler={setState}>
|
||||||
<InputSearchProduct
|
<InputSearchOnList
|
||||||
|
label={i18n.str`Search product`}
|
||||||
selected={state.product}
|
selected={state.product}
|
||||||
onChange={(p) => setState((v) => ({ ...v, product: p }))}
|
onChange={(p) => setState((v) => ({ ...v, product: p }))}
|
||||||
products={inventory}
|
list={inventory}
|
||||||
|
withImage
|
||||||
/>
|
/>
|
||||||
{state.product && (
|
{state.product && (
|
||||||
<div class="columns mt-5">
|
<div class="columns mt-5">
|
||||||
|
@ -58,12 +58,12 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
|
|||||||
!initial || initial.total_stock === -1
|
!initial || initial.total_stock === -1
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
current: initial.total_stock || 0,
|
current: initial.total_stock || 0,
|
||||||
lost: initial.total_lost || 0,
|
lost: initial.total_lost || 0,
|
||||||
sold: initial.total_sold || 0,
|
sold: initial.total_sold || 0,
|
||||||
address: initial.address,
|
address: initial.address,
|
||||||
nextRestock: initial.next_restock,
|
nextRestock: initial.next_restock,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
let errors: FormErrors<Entity> = {};
|
let errors: FormErrors<Entity> = {};
|
||||||
|
|
||||||
@ -148,15 +148,17 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
|
|||||||
name="minimum_age"
|
name="minimum_age"
|
||||||
label={i18n.str`Age restricted`}
|
label={i18n.str`Age restricted`}
|
||||||
tooltip={i18n.str`is this product restricted for customer below certain age?`}
|
tooltip={i18n.str`is this product restricted for customer below certain age?`}
|
||||||
|
help={i18n.str`can be overridden by the order configuration`}
|
||||||
/>
|
/>
|
||||||
<Input<Entity>
|
<Input<Entity>
|
||||||
name="unit"
|
name="unit"
|
||||||
label={i18n.str`Unit`}
|
label={i18n.str`Unit name`}
|
||||||
tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`}
|
tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`}
|
||||||
|
help={i18n.str`exajmple: kg, items or liters`}
|
||||||
/>
|
/>
|
||||||
<InputCurrency<Entity>
|
<InputCurrency<Entity>
|
||||||
name="price"
|
name="price"
|
||||||
label={i18n.str`Price`}
|
label={i18n.str`Price per unit`}
|
||||||
tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`}
|
tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`}
|
||||||
/>
|
/>
|
||||||
<InputStock
|
<InputStock
|
||||||
|
@ -28,8 +28,8 @@ interface BackendContextType {
|
|||||||
token?: string;
|
token?: string;
|
||||||
triedToLog: boolean;
|
triedToLog: boolean;
|
||||||
resetBackend: () => void;
|
resetBackend: () => void;
|
||||||
clearAllTokens: () => void;
|
// clearAllTokens: () => void;
|
||||||
addTokenCleaner: (c: () => void) => void;
|
// addTokenCleaner: (c: () => void) => void;
|
||||||
updateLoginStatus: (url: string, token?: string) => void;
|
updateLoginStatus: (url: string, token?: string) => void;
|
||||||
updateToken: (token?: string) => void;
|
updateToken: (token?: string) => void;
|
||||||
}
|
}
|
||||||
@ -39,8 +39,8 @@ const BackendContext = createContext<BackendContextType>({
|
|||||||
token: undefined,
|
token: undefined,
|
||||||
triedToLog: false,
|
triedToLog: false,
|
||||||
resetBackend: () => null,
|
resetBackend: () => null,
|
||||||
clearAllTokens: () => null,
|
// clearAllTokens: () => null,
|
||||||
addTokenCleaner: () => null,
|
// addTokenCleaner: () => null,
|
||||||
updateLoginStatus: () => null,
|
updateLoginStatus: () => null,
|
||||||
updateToken: () => null,
|
updateToken: () => null,
|
||||||
});
|
});
|
||||||
@ -56,30 +56,30 @@ function useBackendContextState(
|
|||||||
_updateToken(t);
|
_updateToken(t);
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenCleaner = useCallback(() => {
|
// const tokenCleaner = useCallback(() => {
|
||||||
updateToken(undefined);
|
// updateToken(undefined);
|
||||||
}, []);
|
// }, []);
|
||||||
const [cleaners, setCleaners] = useState([tokenCleaner]);
|
// const [cleaners, setCleaners] = useState([tokenCleaner]);
|
||||||
const addTokenCleaner = (c: () => void) => setCleaners((cs) => [...cs, c]);
|
// const addTokenCleaner = (c: () => void) => setCleaners((cs) => [...cs, c]);
|
||||||
const addTokenCleanerMemo = useCallback(
|
// const addTokenCleanerMemo = useCallback(
|
||||||
(c: () => void) => {
|
// (c: () => void) => {
|
||||||
addTokenCleaner(c);
|
// addTokenCleaner(c);
|
||||||
},
|
// },
|
||||||
[tokenCleaner],
|
// [tokenCleaner],
|
||||||
);
|
// );
|
||||||
|
|
||||||
const clearAllTokens = () => {
|
// const clearAllTokens = () => {
|
||||||
cleaners.forEach((c) => c());
|
// cleaners.forEach((c) => c());
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
// for (let i = 0; i < localStorage.length; i++) {
|
||||||
const k = localStorage.key(i);
|
// const k = localStorage.key(i);
|
||||||
if (k && /^backend-token/.test(k)) localStorage.removeItem(k);
|
// if (k && /^backend-token/.test(k)) localStorage.removeItem(k);
|
||||||
}
|
// }
|
||||||
resetBackend();
|
// resetBackend();
|
||||||
};
|
// };
|
||||||
|
|
||||||
const updateLoginStatus = (url: string, token?: string) => {
|
const updateLoginStatus = (url: string, token?: string) => {
|
||||||
changeBackend(url);
|
changeBackend(url);
|
||||||
if (token) updateToken(token);
|
updateToken(token);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -88,9 +88,9 @@ function useBackendContextState(
|
|||||||
triedToLog,
|
triedToLog,
|
||||||
updateLoginStatus,
|
updateLoginStatus,
|
||||||
resetBackend,
|
resetBackend,
|
||||||
clearAllTokens,
|
// clearAllTokens,
|
||||||
updateToken,
|
updateToken,
|
||||||
addTokenCleaner: addTokenCleanerMemo,
|
// addTokenCleaner: addTokenCleanerMemo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
428
packages/merchant-backoffice-ui/src/declaration.d.ts
vendored
428
packages/merchant-backoffice-ui/src/declaration.d.ts
vendored
@ -25,6 +25,8 @@ type EddsaSignature = string;
|
|||||||
type WireTransferIdentifierRawP = string;
|
type WireTransferIdentifierRawP = string;
|
||||||
type RelativeTime = Duration;
|
type RelativeTime = Duration;
|
||||||
type ImageDataUrl = string;
|
type ImageDataUrl = string;
|
||||||
|
type MerchantUserType = "business" | "individual";
|
||||||
|
|
||||||
|
|
||||||
export interface WithId {
|
export interface WithId {
|
||||||
id: string;
|
id: string;
|
||||||
@ -312,46 +314,8 @@ export namespace MerchantBackend {
|
|||||||
// header.
|
// header.
|
||||||
token?: string;
|
token?: string;
|
||||||
}
|
}
|
||||||
type FacadeCredentials = NoFacadeCredentials | BasicAuthFacadeCredentials;
|
|
||||||
|
|
||||||
interface NoFacadeCredentials {
|
|
||||||
type: "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BasicAuthFacadeCredentials {
|
|
||||||
type: "basic";
|
|
||||||
|
|
||||||
// Username to use to authenticate
|
|
||||||
username: string;
|
|
||||||
|
|
||||||
// Password to use to authenticate
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MerchantBankAccount {
|
|
||||||
// The payto:// URI where the wallet will send coins.
|
|
||||||
payto_uri: string;
|
|
||||||
|
|
||||||
// Optional base URL for a facade where the
|
|
||||||
// merchant backend can see incoming wire
|
|
||||||
// transfers to reconcile its accounting
|
|
||||||
// with that of the exchange. Used by
|
|
||||||
// taler-merchant-wirewatch.
|
|
||||||
credit_facade_url?: string;
|
|
||||||
|
|
||||||
// Credentials for accessing the credit facade.
|
|
||||||
credit_facade_credentials?: FacadeCredentials;
|
|
||||||
}
|
|
||||||
//POST /private/instances
|
//POST /private/instances
|
||||||
interface InstanceConfigurationMessage {
|
interface InstanceConfigurationMessage {
|
||||||
// Bank accounts of the merchant. A merchant may have
|
|
||||||
// multiple accounts, thus this is an array. Note that by
|
|
||||||
// removing accounts from this list the respective account is set to
|
|
||||||
// inactive and thus unavailable for new contracts, but preserved
|
|
||||||
// in the database as existing offers and contracts may still refer
|
|
||||||
// to it.
|
|
||||||
accounts: MerchantBankAccount[];
|
|
||||||
|
|
||||||
// Name of the merchant instance to create (will become $INSTANCE).
|
// Name of the merchant instance to create (will become $INSTANCE).
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@ -361,12 +325,16 @@ export namespace MerchantBackend {
|
|||||||
// Type of the user (business or individual).
|
// Type of the user (business or individual).
|
||||||
// Defaults to 'business'. Should become mandatory field
|
// Defaults to 'business'. Should become mandatory field
|
||||||
// in the future, left as optional for API compatibility for now.
|
// in the future, left as optional for API compatibility for now.
|
||||||
user_type?: string;
|
user_type?: MerchantUserType;
|
||||||
|
|
||||||
email: string;
|
// Merchant email for customer contact.
|
||||||
website: string;
|
email?: string;
|
||||||
// An optional base64-encoded logo image
|
|
||||||
logo: ImageDataUrl;
|
// Merchant public website.
|
||||||
|
website?: string;
|
||||||
|
|
||||||
|
// Merchant logo.
|
||||||
|
logo?: ImageDataUrl;
|
||||||
|
|
||||||
// "Authentication" header required to authorize management access the instance.
|
// "Authentication" header required to authorize management access the instance.
|
||||||
// Optional, if not given authentication will be disabled for
|
// Optional, if not given authentication will be disabled for
|
||||||
@ -381,17 +349,10 @@ export namespace MerchantBackend {
|
|||||||
// (to be put into contracts).
|
// (to be put into contracts).
|
||||||
jurisdiction: Location;
|
jurisdiction: Location;
|
||||||
|
|
||||||
// Maximum wire fee this instance is willing to pay.
|
// Use STEFAN curves to determine default fees?
|
||||||
// Can be overridden by the frontend on a per-order basis.
|
// If false, no fees are allowed by default.
|
||||||
default_max_wire_fee: Amount;
|
// Can always be overridden by the frontend on a per-order basis.
|
||||||
|
use_stefan: boolean;
|
||||||
// Default factor for wire fee amortization calculations.
|
|
||||||
// Can be overridden by the frontend on a per-order basis.
|
|
||||||
default_wire_fee_amortization: Integer;
|
|
||||||
|
|
||||||
// Maximum deposit fee (sum over all coins) this instance is willing to pay.
|
|
||||||
// Can be overridden by the frontend on a per-order basis.
|
|
||||||
default_max_deposit_fee: Amount;
|
|
||||||
|
|
||||||
// If the frontend does NOT specify an execution date, how long should
|
// If the frontend does NOT specify an execution date, how long should
|
||||||
// we tell the exchange to wait to aggregate transactions before
|
// we tell the exchange to wait to aggregate transactions before
|
||||||
@ -406,11 +367,6 @@ export namespace MerchantBackend {
|
|||||||
|
|
||||||
// PATCH /private/instances/$INSTANCE
|
// PATCH /private/instances/$INSTANCE
|
||||||
interface InstanceReconfigurationMessage {
|
interface InstanceReconfigurationMessage {
|
||||||
// Bank accounts of the merchant. A merchant may have
|
|
||||||
// multiple accounts, thus this is an array. Note that removing
|
|
||||||
// URIs from this list deactivates the specified accounts
|
|
||||||
// (they will no longer be used for future contracts).
|
|
||||||
accounts: MerchantBankAccount[];
|
|
||||||
|
|
||||||
// Merchant name corresponding to this instance.
|
// Merchant name corresponding to this instance.
|
||||||
name: string;
|
name: string;
|
||||||
@ -418,7 +374,16 @@ export namespace MerchantBackend {
|
|||||||
// Type of the user (business or individual).
|
// Type of the user (business or individual).
|
||||||
// Defaults to 'business'. Should become mandatory field
|
// Defaults to 'business'. Should become mandatory field
|
||||||
// in the future, left as optional for API compatibility for now.
|
// in the future, left as optional for API compatibility for now.
|
||||||
user_type?: string;
|
user_type?: MerchantUserType;
|
||||||
|
|
||||||
|
// Merchant email for customer contact.
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
// Merchant public website.
|
||||||
|
website?: string;
|
||||||
|
|
||||||
|
// Merchant logo.
|
||||||
|
logo?: ImageDataUrl;
|
||||||
|
|
||||||
// The merchant's physical address (to be put into contracts).
|
// The merchant's physical address (to be put into contracts).
|
||||||
address: Location;
|
address: Location;
|
||||||
@ -427,17 +392,10 @@ export namespace MerchantBackend {
|
|||||||
// (to be put into contracts).
|
// (to be put into contracts).
|
||||||
jurisdiction: Location;
|
jurisdiction: Location;
|
||||||
|
|
||||||
// Maximum wire fee this instance is willing to pay.
|
// Use STEFAN curves to determine default fees?
|
||||||
// Can be overridden by the frontend on a per-order basis.
|
// If false, no fees are allowed by default.
|
||||||
default_max_wire_fee: Amount;
|
// Can always be overridden by the frontend on a per-order basis.
|
||||||
|
use_stefan: boolean;
|
||||||
// Default factor for wire fee amortization calculations.
|
|
||||||
// Can be overridden by the frontend on a per-order basis.
|
|
||||||
default_wire_fee_amortization: Integer;
|
|
||||||
|
|
||||||
// Maximum deposit fee (sum over all coins) this instance is willing to pay.
|
|
||||||
// Can be overridden by the frontend on a per-order basis.
|
|
||||||
default_max_deposit_fee: Amount;
|
|
||||||
|
|
||||||
// If the frontend does NOT specify an execution date, how long should
|
// If the frontend does NOT specify an execution date, how long should
|
||||||
// we tell the exchange to wait to aggregate transactions before
|
// we tell the exchange to wait to aggregate transactions before
|
||||||
@ -460,7 +418,14 @@ export namespace MerchantBackend {
|
|||||||
// Merchant name corresponding to this instance.
|
// Merchant name corresponding to this instance.
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
deleted?: boolean;
|
// Type of the user ("business" or "individual").
|
||||||
|
user_type: MerchantUserType;
|
||||||
|
|
||||||
|
// Merchant public website.
|
||||||
|
website?: string;
|
||||||
|
|
||||||
|
// Merchant logo.
|
||||||
|
logo?: ImageDataUrl;
|
||||||
|
|
||||||
// Merchant instance this response is about ($INSTANCE)
|
// Merchant instance this response is about ($INSTANCE)
|
||||||
id: string;
|
id: string;
|
||||||
@ -472,8 +437,63 @@ export namespace MerchantBackend {
|
|||||||
// specify the desired payment target in /order requests. Note that
|
// specify the desired payment target in /order requests. Note that
|
||||||
// front-ends do not have to support wallets selecting payment targets.
|
// front-ends do not have to support wallets selecting payment targets.
|
||||||
payment_targets: string[];
|
payment_targets: string[];
|
||||||
|
|
||||||
|
// Has this instance been deleted (but not purged)?
|
||||||
|
deleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//GET /private/instances/$INSTANCE
|
||||||
|
interface QueryInstancesResponse {
|
||||||
|
|
||||||
|
// Merchant name corresponding to this instance.
|
||||||
|
name: string;
|
||||||
|
// Type of the user ("business" or "individual").
|
||||||
|
user_type: MerchantUserType;
|
||||||
|
|
||||||
|
// Merchant email for customer contact.
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
// Merchant public website.
|
||||||
|
website?: string;
|
||||||
|
|
||||||
|
// Merchant logo.
|
||||||
|
logo?: ImageDataUrl;
|
||||||
|
|
||||||
|
// Public key of the merchant/instance, in Crockford Base32 encoding.
|
||||||
|
merchant_pub: EddsaPublicKey;
|
||||||
|
|
||||||
|
// The merchant's physical address (to be put into contracts).
|
||||||
|
address: Location;
|
||||||
|
|
||||||
|
// The jurisdiction under which the merchant conducts its business
|
||||||
|
// (to be put into contracts).
|
||||||
|
jurisdiction: Location;
|
||||||
|
|
||||||
|
// Use STEFAN curves to determine default fees?
|
||||||
|
// If false, no fees are allowed by default.
|
||||||
|
// Can always be overridden by the frontend on a per-order basis.
|
||||||
|
use_stefan: boolean;
|
||||||
|
|
||||||
|
// If the frontend does NOT specify an execution date, how long should
|
||||||
|
// we tell the exchange to wait to aggregate transactions before
|
||||||
|
// executing the wire transfer? This delay is added to the current
|
||||||
|
// time when we generate the advisory execution time for the exchange.
|
||||||
|
default_wire_transfer_delay: RelativeTime;
|
||||||
|
|
||||||
|
// If the frontend does NOT specify a payment deadline, how long should
|
||||||
|
// offers we make be valid by default?
|
||||||
|
default_pay_delay: RelativeTime;
|
||||||
|
|
||||||
|
// Authentication configuration.
|
||||||
|
// Does not contain the token when token auth is configured.
|
||||||
|
auth: {
|
||||||
|
method: "external" | "token";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// DELETE /private/instances/$INSTANCE
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace KYC {
|
||||||
//GET /private/instances/$INSTANCE/kyc
|
//GET /private/instances/$INSTANCE/kyc
|
||||||
interface AccountKycRedirects {
|
interface AccountKycRedirects {
|
||||||
// Array of pending KYCs.
|
// Array of pending KYCs.
|
||||||
@ -513,56 +533,76 @@ export namespace MerchantBackend {
|
|||||||
exchange_http_status: number;
|
exchange_http_status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
//GET /private/instances/$INSTANCE
|
}
|
||||||
interface QueryInstancesResponse {
|
|
||||||
// The URI where the wallet will send coins. A merchant may have
|
|
||||||
// multiple accounts, thus this is an array.
|
|
||||||
accounts: MerchantAccount[];
|
|
||||||
|
|
||||||
// Merchant name corresponding to this instance.
|
namespace BankAccounts {
|
||||||
name: string;
|
|
||||||
|
|
||||||
// Public key of the merchant/instance, in Crockford Base32 encoding.
|
interface AccountAddDetails {
|
||||||
merchant_pub: EddsaPublicKey;
|
|
||||||
|
|
||||||
// The merchant's physical address (to be put into contracts).
|
// payto:// URI of the account.
|
||||||
address: Location;
|
payto_uri: string;
|
||||||
|
|
||||||
// The jurisdiction under which the merchant conducts its business
|
// URL from where the merchant can download information
|
||||||
// (to be put into contracts).
|
// about incoming wire transfers to this account.
|
||||||
jurisdiction: Location;
|
credit_facade_url?: string;
|
||||||
|
|
||||||
// Maximum wire fee this instance is willing to pay.
|
// Credentials to use when accessing the credit facade.
|
||||||
// Can be overridden by the frontend on a per-order basis.
|
// Never returned on a GET (as this may be somewhat
|
||||||
default_max_wire_fee: Amount;
|
// sensitive data). Can be set in POST
|
||||||
|
// or PATCH requests to update (or delete) credentials.
|
||||||
|
// To really delete credentials, set them to the type: "none".
|
||||||
|
credit_facade_credentials?: FacadeCredentials;
|
||||||
|
|
||||||
// Default factor for wire fee amortization calculations.
|
|
||||||
// Can be overridden by the frontend on a per-order basis.
|
|
||||||
default_wire_fee_amortization: Integer;
|
|
||||||
|
|
||||||
// Maximum deposit fee (sum over all coins) this instance is willing to pay.
|
|
||||||
// Can be overridden by the frontend on a per-order basis.
|
|
||||||
default_max_deposit_fee: Amount;
|
|
||||||
|
|
||||||
// If the frontend does NOT specify an execution date, how long should
|
|
||||||
// we tell the exchange to wait to aggregate transactions before
|
|
||||||
// executing the wire transfer? This delay is added to the current
|
|
||||||
// time when we generate the advisory execution time for the exchange.
|
|
||||||
default_wire_transfer_delay: RelativeTime;
|
|
||||||
|
|
||||||
// If the frontend does NOT specify a payment deadline, how long should
|
|
||||||
// offers we make be valid by default?
|
|
||||||
default_pay_delay: RelativeTime;
|
|
||||||
|
|
||||||
// Authentication configuration.
|
|
||||||
// Does not contain the token when token auth is configured.
|
|
||||||
auth: {
|
|
||||||
method: "external" | "token";
|
|
||||||
token?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MerchantAccount {
|
type FacadeCredentials =
|
||||||
|
| NoFacadeCredentials
|
||||||
|
| BasicAuthFacadeCredentials;
|
||||||
|
|
||||||
|
interface NoFacadeCredentials {
|
||||||
|
type: "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BasicAuthFacadeCredentials {
|
||||||
|
type: "basic";
|
||||||
|
|
||||||
|
// Username to use to authenticate
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
// Password to use to authenticate
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountAddResponse {
|
||||||
|
// Hash over the wire details (including over the salt).
|
||||||
|
h_wire: HashCode;
|
||||||
|
|
||||||
|
// Salt used to compute h_wire.
|
||||||
|
salt: HashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountPatchDetails {
|
||||||
|
|
||||||
|
// URL from where the merchant can download information
|
||||||
|
// about incoming wire transfers to this account.
|
||||||
|
credit_facade_url?: string;
|
||||||
|
|
||||||
|
// Credentials to use when accessing the credit facade.
|
||||||
|
// Never returned on a GET (as this may be somewhat
|
||||||
|
// sensitive data). Can be set in POST
|
||||||
|
// or PATCH requests to update (or delete) credentials.
|
||||||
|
// To really delete credentials, set them to the type: "none".
|
||||||
|
credit_facade_credentials?: FacadeCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface AccountsSummaryResponse {
|
||||||
|
|
||||||
|
// List of accounts that are known for the instance.
|
||||||
|
accounts: BankAccountEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BankAccountEntry {
|
||||||
// payto:// URI of the account.
|
// payto:// URI of the account.
|
||||||
payto_uri: string;
|
payto_uri: string;
|
||||||
|
|
||||||
@ -587,7 +627,6 @@ export namespace MerchantBackend {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE /private/instances/$INSTANCE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Products {
|
namespace Products {
|
||||||
@ -957,6 +996,10 @@ export namespace MerchantBackend {
|
|||||||
// high entropy to prevent adversarial claims (like it is
|
// high entropy to prevent adversarial claims (like it is
|
||||||
// if the backend auto-generates one). Default is 'true'.
|
// if the backend auto-generates one). Default is 'true'.
|
||||||
create_token?: boolean;
|
create_token?: boolean;
|
||||||
|
|
||||||
|
// OTP device ID to associate with the order.
|
||||||
|
// This parameter is optional.
|
||||||
|
otp_id?: string;
|
||||||
}
|
}
|
||||||
type Order = MinimalOrderDetail | ContractTerms;
|
type Order = MinimalOrderDetail | ContractTerms;
|
||||||
|
|
||||||
@ -1031,9 +1074,9 @@ export namespace MerchantBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Tips {
|
namespace Rewards {
|
||||||
// GET /private/reserves
|
// GET /private/reserves
|
||||||
interface TippingReserveStatus {
|
interface RewardReserveStatus {
|
||||||
// Array of all known reserves (possibly empty!)
|
// Array of all known reserves (possibly empty!)
|
||||||
reserves: ReserveStatusEntry[];
|
reserves: ReserveStatusEntry[];
|
||||||
}
|
}
|
||||||
@ -1057,7 +1100,7 @@ export namespace MerchantBackend {
|
|||||||
// Amount picked up so far.
|
// Amount picked up so far.
|
||||||
pickup_amount: Amount;
|
pickup_amount: Amount;
|
||||||
|
|
||||||
// Amount approved for tips that exceeds the pickup_amount.
|
// Amount approved for rewards that exceeds the pickup_amount.
|
||||||
committed_amount: Amount;
|
committed_amount: Amount;
|
||||||
|
|
||||||
// Is this reserve active (false if it was deleted but not purged)
|
// Is this reserve active (false if it was deleted but not purged)
|
||||||
@ -1068,7 +1111,7 @@ export namespace MerchantBackend {
|
|||||||
// Amount that the merchant promises to put into the reserve
|
// Amount that the merchant promises to put into the reserve
|
||||||
initial_balance: Amount;
|
initial_balance: Amount;
|
||||||
|
|
||||||
// Exchange the merchant intends to use for tipping
|
// Exchange the merchant intends to use for reward
|
||||||
exchange_url: string;
|
exchange_url: string;
|
||||||
|
|
||||||
// Desired wire method, for example "iban" or "x-taler-bank"
|
// Desired wire method, for example "iban" or "x-taler-bank"
|
||||||
@ -1081,30 +1124,30 @@ export namespace MerchantBackend {
|
|||||||
// Wire accounts of the exchange where to transfer the funds.
|
// Wire accounts of the exchange where to transfer the funds.
|
||||||
accounts: WireAccount[];
|
accounts: WireAccount[];
|
||||||
}
|
}
|
||||||
interface TipCreateRequest {
|
interface RewardCreateRequest {
|
||||||
// Amount that the customer should be tipped
|
// Amount that the customer should be reward
|
||||||
amount: Amount;
|
amount: Amount;
|
||||||
|
|
||||||
// Justification for giving the tip
|
// Justification for giving the reward
|
||||||
justification: string;
|
justification: string;
|
||||||
|
|
||||||
// URL that the user should be directed to after tipping,
|
// URL that the user should be directed to after rewarding,
|
||||||
// will be included in the tip_token.
|
// will be included in the reward_token.
|
||||||
next_url: string;
|
next_url: string;
|
||||||
}
|
}
|
||||||
interface TipCreateConfirmation {
|
interface RewardCreateConfirmation {
|
||||||
// Unique tip identifier for the tip that was created.
|
// Unique reward identifier for the reward that was created.
|
||||||
tip_id: HashCode;
|
reward_id: HashCode;
|
||||||
|
|
||||||
// taler://tip URI for the tip
|
// taler://reward URI for the reward
|
||||||
taler_tip_uri: string;
|
taler_reward_uri: string;
|
||||||
|
|
||||||
// URL that will directly trigger processing
|
// URL that will directly trigger processing
|
||||||
// the tip when the browser is redirected to it
|
// the reward when the browser is redirected to it
|
||||||
tip_status_url: string;
|
reward_status_url: string;
|
||||||
|
|
||||||
// when does the tip expire
|
// when does the reward expire
|
||||||
tip_expiration: Timestamp;
|
reward_expiration: Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReserveDetail {
|
interface ReserveDetail {
|
||||||
@ -1124,12 +1167,12 @@ export namespace MerchantBackend {
|
|||||||
// Amount picked up so far.
|
// Amount picked up so far.
|
||||||
pickup_amount: Amount;
|
pickup_amount: Amount;
|
||||||
|
|
||||||
// Amount approved for tips that exceeds the pickup_amount.
|
// Amount approved for rewards that exceeds the pickup_amount.
|
||||||
committed_amount: Amount;
|
committed_amount: Amount;
|
||||||
|
|
||||||
// Array of all tips created by this reserves (possibly empty!).
|
// Array of all rewards created by this reserves (possibly empty!).
|
||||||
// Only present if asked for explicitly.
|
// Only present if asked for explicitly.
|
||||||
tips?: TipStatusEntry[];
|
rewards?: RewardStatusEntry[];
|
||||||
|
|
||||||
// Is this reserve active (false if it was deleted but not purged)?
|
// Is this reserve active (false if it was deleted but not purged)?
|
||||||
active: boolean;
|
active: boolean;
|
||||||
@ -1144,31 +1187,31 @@ export namespace MerchantBackend {
|
|||||||
exchange_url: string;
|
exchange_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TipStatusEntry {
|
interface RewardStatusEntry {
|
||||||
// Unique identifier for the tip.
|
// Unique identifier for the reward.
|
||||||
tip_id: HashCode;
|
reward_id: HashCode;
|
||||||
|
|
||||||
// Total amount of the tip that can be withdrawn.
|
// Total amount of the reward that can be withdrawn.
|
||||||
total_amount: Amount;
|
total_amount: Amount;
|
||||||
|
|
||||||
// Human-readable reason for why the tip was granted.
|
// Human-readable reason for why the reward was granted.
|
||||||
reason: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TipDetails {
|
interface RewardDetails {
|
||||||
// Amount that we authorized for this tip.
|
// Amount that we authorized for this reward.
|
||||||
total_authorized: Amount;
|
total_authorized: Amount;
|
||||||
|
|
||||||
// Amount that was picked up by the user already.
|
// Amount that was picked up by the user already.
|
||||||
total_picked_up: Amount;
|
total_picked_up: Amount;
|
||||||
|
|
||||||
// Human-readable reason given when authorizing the tip.
|
// Human-readable reason given when authorizing the reward.
|
||||||
reason: string;
|
reason: string;
|
||||||
|
|
||||||
// Timestamp indicating when the tip is set to expire (may be in the past).
|
// Timestamp indicating when the reward is set to expire (may be in the past).
|
||||||
expiration: Timestamp;
|
expiration: Timestamp;
|
||||||
|
|
||||||
// Reserve public key from which the tip is funded.
|
// Reserve public key from which the reward is funded.
|
||||||
reserve_pub: EddsaPublicKey;
|
reserve_pub: EddsaPublicKey;
|
||||||
|
|
||||||
// Array showing the pickup operations of the wallet (possibly empty!).
|
// Array showing the pickup operations of the wallet (possibly empty!).
|
||||||
@ -1239,6 +1282,63 @@ export namespace MerchantBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace OTP {
|
||||||
|
interface OtpDeviceAddDetails {
|
||||||
|
// Device ID to use.
|
||||||
|
otp_device_id: string;
|
||||||
|
|
||||||
|
// Human-readable description for the device.
|
||||||
|
otp_description: string;
|
||||||
|
|
||||||
|
// A base64-encoded key
|
||||||
|
otp_key: string;
|
||||||
|
|
||||||
|
// Algorithm for computing the POS confirmation.
|
||||||
|
otp_algorithm: Integer;
|
||||||
|
|
||||||
|
// Counter for counter-based OTP devices.
|
||||||
|
otp_ctr?: Integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OtpDevicePatchDetails {
|
||||||
|
// Human-readable description for the device.
|
||||||
|
otp_description: string;
|
||||||
|
|
||||||
|
// A base64-encoded key
|
||||||
|
otp_key: string | undefined;
|
||||||
|
|
||||||
|
// Algorithm for computing the POS confirmation.
|
||||||
|
otp_algorithm: Integer;
|
||||||
|
|
||||||
|
// Counter for counter-based OTP devices.
|
||||||
|
otp_ctr?: Integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OtpDeviceSummaryResponse {
|
||||||
|
// Array of devices that are present in our backend.
|
||||||
|
otp_devices: OtpDeviceEntry[];
|
||||||
|
}
|
||||||
|
interface OtpDeviceEntry {
|
||||||
|
// Device identifier.
|
||||||
|
otp_device_id: string;
|
||||||
|
|
||||||
|
// Human-readable description for the device.
|
||||||
|
device_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OtpDeviceDetails {
|
||||||
|
// Human-readable description for the device.
|
||||||
|
device_description: string;
|
||||||
|
|
||||||
|
// Algorithm for computing the POS confirmation.
|
||||||
|
otp_algorithm: Integer;
|
||||||
|
|
||||||
|
// Counter for counter-based OTP devices.
|
||||||
|
otp_ctr?: Integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
namespace Template {
|
namespace Template {
|
||||||
interface TemplateAddDetails {
|
interface TemplateAddDetails {
|
||||||
// Template ID to use.
|
// Template ID to use.
|
||||||
@ -1247,12 +1347,9 @@ export namespace MerchantBackend {
|
|||||||
// Human-readable description for the template.
|
// Human-readable description for the template.
|
||||||
template_description: string;
|
template_description: string;
|
||||||
|
|
||||||
// A base64-encoded key of the point-of-sale.
|
// OTP device ID.
|
||||||
// This parameter is optional.
|
// This parameter is optional.
|
||||||
pos_key?: string;
|
otp_id?: string;
|
||||||
|
|
||||||
// Algorithm for computing the POS confirmation, 0 for none.
|
|
||||||
pos_algorithm?: number;
|
|
||||||
|
|
||||||
// Additional information in a separate template.
|
// Additional information in a separate template.
|
||||||
template_contract: TemplateContractDetails;
|
template_contract: TemplateContractDetails;
|
||||||
@ -1276,12 +1373,9 @@ export namespace MerchantBackend {
|
|||||||
// Human-readable description for the template.
|
// Human-readable description for the template.
|
||||||
template_description: string;
|
template_description: string;
|
||||||
|
|
||||||
// A base64-encoded key of the point-of-sale.
|
// OTP device ID.
|
||||||
// This parameter is optional.
|
// This parameter is optional.
|
||||||
pos_key?: string;
|
otp_id?: string;
|
||||||
|
|
||||||
// Algorithm for computing the POS confirmation, 0 for none.
|
|
||||||
pos_algorithm?: Integer;
|
|
||||||
|
|
||||||
// Additional information in a separate template.
|
// Additional information in a separate template.
|
||||||
template_contract: TemplateContractDetails;
|
template_contract: TemplateContractDetails;
|
||||||
@ -1304,12 +1398,9 @@ export namespace MerchantBackend {
|
|||||||
// Human-readable description for the template.
|
// Human-readable description for the template.
|
||||||
template_description: string;
|
template_description: string;
|
||||||
|
|
||||||
// A base64-encoded key of the point-of-sale.
|
// OTP device ID.
|
||||||
// This parameter is optional.
|
// This parameter is optional.
|
||||||
pos_key?: string;
|
otp_id?: string;
|
||||||
|
|
||||||
// Algorithm for computing the POS confirmation, 0 for none.
|
|
||||||
pos_algorithm?: Integer;
|
|
||||||
|
|
||||||
// Additional information in a separate template.
|
// Additional information in a separate template.
|
||||||
template_contract: TemplateContractDetails;
|
template_contract: TemplateContractDetails;
|
||||||
@ -1424,21 +1515,6 @@ export namespace MerchantBackend {
|
|||||||
// Maximum total deposit fee accepted by the merchant for this contract
|
// Maximum total deposit fee accepted by the merchant for this contract
|
||||||
max_fee: Amount;
|
max_fee: Amount;
|
||||||
|
|
||||||
// Maximum wire fee accepted by the merchant (customer share to be
|
|
||||||
// divided by the 'wire_fee_amortization' factor, and further reduced
|
|
||||||
// if deposit fees are below 'max_fee'). Default if missing is zero.
|
|
||||||
max_wire_fee: Amount;
|
|
||||||
|
|
||||||
// Over how many customer transactions does the merchant expect to
|
|
||||||
// amortize wire fees on average? If the exchange's wire fee is
|
|
||||||
// above 'max_wire_fee', the difference is divided by this number
|
|
||||||
// to compute the expected customer's contribution to the wire fee.
|
|
||||||
// The customer's contribution may further be reduced by the difference
|
|
||||||
// between the 'max_fee' and the sum of the actual deposit fees.
|
|
||||||
// Optional, default value if missing is 1. 0 and negative values are
|
|
||||||
// invalid and also interpreted as 1.
|
|
||||||
wire_fee_amortization: number;
|
|
||||||
|
|
||||||
// List of products that are part of the purchase (see Product).
|
// List of products that are part of the purchase (see Product).
|
||||||
products: Product[];
|
products: Product[];
|
||||||
|
|
||||||
|
@ -33,8 +33,9 @@ import {
|
|||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { useApiContext } from "@gnu-taler/web-util/browser";
|
import { useApiContext } from "@gnu-taler/web-util/browser";
|
||||||
|
|
||||||
|
|
||||||
export function useMatchMutate(): (
|
export function useMatchMutate(): (
|
||||||
re: RegExp,
|
re?: RegExp,
|
||||||
value?: unknown,
|
value?: unknown,
|
||||||
) => Promise<any> {
|
) => Promise<any> {
|
||||||
const { cache, mutate } = useSWRConfig();
|
const { cache, mutate } = useSWRConfig();
|
||||||
@ -45,13 +46,19 @@ export function useMatchMutate(): (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return function matchRegexMutate(re: RegExp, value?: unknown) {
|
return function matchRegexMutate(re?: RegExp) {
|
||||||
const allKeys = Array.from(cache.keys());
|
return mutate((key) => {
|
||||||
const keys = allKeys.filter((key) => re.test(key));
|
// evict if no key or regex === all
|
||||||
const mutations = keys.map((key) => {
|
if (!key || !re) return true
|
||||||
return mutate(key, value, true);
|
// match string
|
||||||
|
if (typeof key === 'string' && re.test(key)) return true
|
||||||
|
// record or object have the path at [0]
|
||||||
|
if (typeof key === 'object' && re.test(key[0])) return true
|
||||||
|
//key didn't match regex
|
||||||
|
return false
|
||||||
|
}, undefined, {
|
||||||
|
revalidate: true,
|
||||||
});
|
});
|
||||||
return Promise.all(mutations);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,32 +113,32 @@ interface useBackendInstanceRequestType {
|
|||||||
) => Promise<HttpResponseOk<T>>;
|
) => Promise<HttpResponseOk<T>>;
|
||||||
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
||||||
reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
||||||
tipsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
||||||
multiFetcher: <T>(url: string[]) => Promise<HttpResponseOk<T>[]>;
|
multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>;
|
||||||
orderFetcher: <T>(
|
orderFetcher: <T>(
|
||||||
endpoint: string,
|
params: [endpoint: string,
|
||||||
paid?: YesOrNo,
|
paid?: YesOrNo,
|
||||||
refunded?: YesOrNo,
|
refunded?: YesOrNo,
|
||||||
wired?: YesOrNo,
|
wired?: YesOrNo,
|
||||||
searchDate?: Date,
|
searchDate?: Date,
|
||||||
delta?: number,
|
delta?: number,]
|
||||||
) => Promise<HttpResponseOk<T>>;
|
) => Promise<HttpResponseOk<T>>;
|
||||||
transferFetcher: <T>(
|
transferFetcher: <T>(
|
||||||
endpoint: string,
|
params: [endpoint: string,
|
||||||
payto_uri?: string,
|
payto_uri?: string,
|
||||||
verified?: string,
|
verified?: string,
|
||||||
position?: string,
|
position?: string,
|
||||||
delta?: number,
|
delta?: number,]
|
||||||
) => Promise<HttpResponseOk<T>>;
|
) => Promise<HttpResponseOk<T>>;
|
||||||
templateFetcher: <T>(
|
templateFetcher: <T>(
|
||||||
endpoint: string,
|
params: [endpoint: string,
|
||||||
position?: string,
|
position?: string,
|
||||||
delta?: number,
|
delta?: number]
|
||||||
) => Promise<HttpResponseOk<T>>;
|
) => Promise<HttpResponseOk<T>>;
|
||||||
webhookFetcher: <T>(
|
webhookFetcher: <T>(
|
||||||
endpoint: string,
|
params: [endpoint: string,
|
||||||
position?: string,
|
position?: string,
|
||||||
delta?: number,
|
delta?: number]
|
||||||
) => Promise<HttpResponseOk<T>>;
|
) => Promise<HttpResponseOk<T>>;
|
||||||
}
|
}
|
||||||
interface useBackendBaseRequestType {
|
interface useBackendBaseRequestType {
|
||||||
@ -147,7 +154,7 @@ export function useCredentialsChecker() {
|
|||||||
const { request } = useApiContext();
|
const { request } = useApiContext();
|
||||||
//check against instance details endpoint
|
//check against instance details endpoint
|
||||||
//while merchant backend doesn't have a login endpoint
|
//while merchant backend doesn't have a login endpoint
|
||||||
return async function testLogin(
|
async function testLogin(
|
||||||
instance: string,
|
instance: string,
|
||||||
token: string,
|
token: string,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@ -167,6 +174,7 @@ export function useCredentialsChecker() {
|
|||||||
return { valid: false, cause: ErrorType.UNEXPECTED };
|
return { valid: false, cause: ErrorType.UNEXPECTED };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
return testLogin
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -212,8 +220,9 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
|||||||
|
|
||||||
const multiFetcher = useCallback(
|
const multiFetcher = useCallback(
|
||||||
function multiFetcherImpl<T>(
|
function multiFetcherImpl<T>(
|
||||||
endpoints: string[],
|
args: [endpoints: string[]],
|
||||||
): Promise<HttpResponseOk<T>[]> {
|
): Promise<HttpResponseOk<T>[]> {
|
||||||
|
const [endpoints] = args
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
endpoints.map((endpoint) =>
|
endpoints.map((endpoint) =>
|
||||||
requestHandler<T>(baseUrl, endpoint, { token }),
|
requestHandler<T>(baseUrl, endpoint, { token }),
|
||||||
@ -232,13 +241,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
|||||||
|
|
||||||
const orderFetcher = useCallback(
|
const orderFetcher = useCallback(
|
||||||
function orderFetcherImpl<T>(
|
function orderFetcherImpl<T>(
|
||||||
endpoint: string,
|
args: [endpoint: string,
|
||||||
paid?: YesOrNo,
|
paid?: YesOrNo,
|
||||||
refunded?: YesOrNo,
|
refunded?: YesOrNo,
|
||||||
wired?: YesOrNo,
|
wired?: YesOrNo,
|
||||||
searchDate?: Date,
|
searchDate?: Date,
|
||||||
delta?: number,
|
delta?: number,]
|
||||||
): Promise<HttpResponseOk<T>> {
|
): Promise<HttpResponseOk<T>> {
|
||||||
|
const [endpoint, paid, refunded, wired, searchDate, delta] = args
|
||||||
const date_s =
|
const date_s =
|
||||||
delta && delta < 0 && searchDate
|
delta && delta < 0 && searchDate
|
||||||
? (searchDate.getTime() / 1000) + 1
|
? (searchDate.getTime() / 1000) + 1
|
||||||
@ -260,7 +270,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
|||||||
): Promise<HttpResponseOk<T>> {
|
): Promise<HttpResponseOk<T>> {
|
||||||
return requestHandler<T>(baseUrl, endpoint, {
|
return requestHandler<T>(baseUrl, endpoint, {
|
||||||
params: {
|
params: {
|
||||||
tips: "yes",
|
rewards: "yes",
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
@ -268,8 +278,8 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
|||||||
[baseUrl, token],
|
[baseUrl, token],
|
||||||
);
|
);
|
||||||
|
|
||||||
const tipsDetailFetcher = useCallback(
|
const rewardsDetailFetcher = useCallback(
|
||||||
function tipsDetailFetcherImpl<T>(
|
function rewardsDetailFetcherImpl<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
): Promise<HttpResponseOk<T>> {
|
): Promise<HttpResponseOk<T>> {
|
||||||
return requestHandler<T>(baseUrl, endpoint, {
|
return requestHandler<T>(baseUrl, endpoint, {
|
||||||
@ -284,12 +294,13 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
|||||||
|
|
||||||
const transferFetcher = useCallback(
|
const transferFetcher = useCallback(
|
||||||
function transferFetcherImpl<T>(
|
function transferFetcherImpl<T>(
|
||||||
endpoint: string,
|
args: [endpoint: string,
|
||||||
payto_uri?: string,
|
payto_uri?: string,
|
||||||
verified?: string,
|
verified?: string,
|
||||||
position?: string,
|
position?: string,
|
||||||
delta?: number,
|
delta?: number,]
|
||||||
): Promise<HttpResponseOk<T>> {
|
): Promise<HttpResponseOk<T>> {
|
||||||
|
const [endpoint, payto_uri, verified, position, delta] = args
|
||||||
const params: any = {};
|
const params: any = {};
|
||||||
if (payto_uri !== undefined) params.payto_uri = payto_uri;
|
if (payto_uri !== undefined) params.payto_uri = payto_uri;
|
||||||
if (verified !== undefined) params.verified = verified;
|
if (verified !== undefined) params.verified = verified;
|
||||||
@ -305,10 +316,11 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
|||||||
|
|
||||||
const templateFetcher = useCallback(
|
const templateFetcher = useCallback(
|
||||||
function templateFetcherImpl<T>(
|
function templateFetcherImpl<T>(
|
||||||
endpoint: string,
|
args: [endpoint: string,
|
||||||
position?: string,
|
position?: string,
|
||||||
delta?: number,
|
delta?: number,]
|
||||||
): Promise<HttpResponseOk<T>> {
|
): Promise<HttpResponseOk<T>> {
|
||||||
|
const [endpoint, position, delta] = args
|
||||||
const params: any = {};
|
const params: any = {};
|
||||||
if (delta !== undefined) {
|
if (delta !== undefined) {
|
||||||
params.limit = delta;
|
params.limit = delta;
|
||||||
@ -322,10 +334,11 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
|||||||
|
|
||||||
const webhookFetcher = useCallback(
|
const webhookFetcher = useCallback(
|
||||||
function webhookFetcherImpl<T>(
|
function webhookFetcherImpl<T>(
|
||||||
endpoint: string,
|
args: [endpoint: string,
|
||||||
position?: string,
|
position?: string,
|
||||||
delta?: number,
|
delta?: number,]
|
||||||
): Promise<HttpResponseOk<T>> {
|
): Promise<HttpResponseOk<T>> {
|
||||||
|
const [endpoint, position, delta] = args
|
||||||
const params: any = {};
|
const params: any = {};
|
||||||
if (delta !== undefined) {
|
if (delta !== undefined) {
|
||||||
params.limit = delta;
|
params.limit = delta;
|
||||||
@ -343,7 +356,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
|||||||
multiFetcher,
|
multiFetcher,
|
||||||
orderFetcher,
|
orderFetcher,
|
||||||
reserveDetailFetcher,
|
reserveDetailFetcher,
|
||||||
tipsDetailFetcher,
|
rewardsDetailFetcher,
|
||||||
transferFetcher,
|
transferFetcher,
|
||||||
templateFetcher,
|
templateFetcher,
|
||||||
webhookFetcher,
|
webhookFetcher,
|
||||||
|
217
packages/merchant-backoffice-ui/src/hooks/bank.ts
Normal file
217
packages/merchant-backoffice-ui/src/hooks/bank.ts
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 {
|
||||||
|
HttpResponse,
|
||||||
|
HttpResponseOk,
|
||||||
|
HttpResponsePaginated,
|
||||||
|
RequestError,
|
||||||
|
} from "@gnu-taler/web-util/browser";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { MerchantBackend } from "../declaration.js";
|
||||||
|
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
|
||||||
|
import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
|
||||||
|
|
||||||
|
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
||||||
|
import _useSWR, { SWRHook } from "swr";
|
||||||
|
const useSWR = _useSWR as unknown as SWRHook;
|
||||||
|
|
||||||
|
// const MOCKED_ACCOUNTS: Record<string, MerchantBackend.BankAccounts.AccountAddDetails> = {
|
||||||
|
// "hwire1": {
|
||||||
|
// h_wire: "hwire1",
|
||||||
|
// payto_uri: "payto://fake/iban/123",
|
||||||
|
// salt: "qwe",
|
||||||
|
// },
|
||||||
|
// "hwire2": {
|
||||||
|
// h_wire: "hwire2",
|
||||||
|
// payto_uri: "payto://fake/iban/123",
|
||||||
|
// salt: "qwe2",
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function useBankAccountAPI(): BankAccountAPI {
|
||||||
|
const mutateAll = useMatchMutate();
|
||||||
|
const { request } = useBackendInstanceRequest();
|
||||||
|
|
||||||
|
const createBankAccount = async (
|
||||||
|
data: MerchantBackend.BankAccounts.AccountAddDetails,
|
||||||
|
): Promise<HttpResponseOk<void>> => {
|
||||||
|
// MOCKED_ACCOUNTS[data.h_wire] = data
|
||||||
|
// return Promise.resolve({ ok: true, data: undefined });
|
||||||
|
const res = await request<void>(`/private/accounts`, {
|
||||||
|
method: "POST",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
await mutateAll(/.*private\/accounts.*/);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBankAccount = async (
|
||||||
|
h_wire: string,
|
||||||
|
data: MerchantBackend.BankAccounts.AccountPatchDetails,
|
||||||
|
): Promise<HttpResponseOk<void>> => {
|
||||||
|
// MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials
|
||||||
|
// MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url
|
||||||
|
// return Promise.resolve({ ok: true, data: undefined });
|
||||||
|
const res = await request<void>(`/private/accounts/${h_wire}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
await mutateAll(/.*private\/accounts.*/);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBankAccount = async (
|
||||||
|
h_wire: string,
|
||||||
|
): Promise<HttpResponseOk<void>> => {
|
||||||
|
// delete MOCKED_ACCOUNTS[h_wire]
|
||||||
|
// return Promise.resolve({ ok: true, data: undefined });
|
||||||
|
const res = await request<void>(`/private/accounts/${h_wire}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
await mutateAll(/.*private\/accounts.*/);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createBankAccount,
|
||||||
|
updateBankAccount,
|
||||||
|
deleteBankAccount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BankAccountAPI {
|
||||||
|
createBankAccount: (
|
||||||
|
data: MerchantBackend.BankAccounts.AccountAddDetails,
|
||||||
|
) => Promise<HttpResponseOk<void>>;
|
||||||
|
updateBankAccount: (
|
||||||
|
id: string,
|
||||||
|
data: MerchantBackend.BankAccounts.AccountPatchDetails,
|
||||||
|
) => Promise<HttpResponseOk<void>>;
|
||||||
|
deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceBankAccountFilter {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useInstanceBankAccounts(
|
||||||
|
args?: InstanceBankAccountFilter,
|
||||||
|
updatePosition?: (id: string) => void,
|
||||||
|
): HttpResponsePaginated<
|
||||||
|
MerchantBackend.BankAccounts.AccountsSummaryResponse,
|
||||||
|
MerchantBackend.ErrorDetail
|
||||||
|
> {
|
||||||
|
// return {
|
||||||
|
// ok: true,
|
||||||
|
// loadMore() { },
|
||||||
|
// loadMorePrev() { },
|
||||||
|
// data: {
|
||||||
|
// accounts: Object.values(MOCKED_ACCOUNTS).map(e => ({
|
||||||
|
// ...e,
|
||||||
|
// active: true,
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
const { fetcher } = useBackendInstanceRequest();
|
||||||
|
|
||||||
|
const [pageAfter, setPageAfter] = useState(1);
|
||||||
|
|
||||||
|
const totalAfter = pageAfter * PAGE_SIZE;
|
||||||
|
const {
|
||||||
|
data: afterData,
|
||||||
|
error: afterError,
|
||||||
|
isValidating: loadingAfter,
|
||||||
|
} = useSWR<
|
||||||
|
HttpResponseOk<MerchantBackend.BankAccounts.AccountsSummaryResponse>,
|
||||||
|
RequestError<MerchantBackend.ErrorDetail>
|
||||||
|
>([`/private/accounts`], fetcher);
|
||||||
|
|
||||||
|
const [lastAfter, setLastAfter] = useState<
|
||||||
|
HttpResponse<
|
||||||
|
MerchantBackend.BankAccounts.AccountsSummaryResponse,
|
||||||
|
MerchantBackend.ErrorDetail
|
||||||
|
>
|
||||||
|
>({ loading: true });
|
||||||
|
useEffect(() => {
|
||||||
|
if (afterData) setLastAfter(afterData);
|
||||||
|
}, [afterData /*, beforeData*/]);
|
||||||
|
|
||||||
|
if (afterError) return afterError.cause;
|
||||||
|
|
||||||
|
// if the query returns less that we ask, then we have reach the end or beginning
|
||||||
|
const isReachingEnd =
|
||||||
|
afterData && afterData.data.accounts.length < totalAfter;
|
||||||
|
const isReachingStart = false;
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
isReachingEnd,
|
||||||
|
isReachingStart,
|
||||||
|
loadMore: () => {
|
||||||
|
if (!afterData || isReachingEnd) return;
|
||||||
|
if (afterData.data.accounts.length < MAX_RESULT_SIZE) {
|
||||||
|
setPageAfter(pageAfter + 1);
|
||||||
|
} else {
|
||||||
|
const from = `${afterData.data.accounts[afterData.data.accounts.length - 1]
|
||||||
|
.h_wire
|
||||||
|
}`;
|
||||||
|
if (from && updatePosition) updatePosition(from);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadMorePrev: () => {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts;
|
||||||
|
if (loadingAfter /* || loadingBefore */)
|
||||||
|
return { loading: true, data: { accounts } };
|
||||||
|
if (/*beforeData &&*/ afterData) {
|
||||||
|
return { ok: true, data: { accounts }, ...pagination };
|
||||||
|
}
|
||||||
|
return { loading: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBankAccountDetails(
|
||||||
|
h_wire: string,
|
||||||
|
): HttpResponse<
|
||||||
|
MerchantBackend.BankAccounts.BankAccountEntry,
|
||||||
|
MerchantBackend.ErrorDetail
|
||||||
|
> {
|
||||||
|
// return {
|
||||||
|
// ok: true,
|
||||||
|
// data: {
|
||||||
|
// ...MOCKED_ACCOUNTS[h_wire],
|
||||||
|
// active: true,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
const { fetcher } = useBackendInstanceRequest();
|
||||||
|
|
||||||
|
const { data, error, isValidating } = useSWR<
|
||||||
|
HttpResponseOk<MerchantBackend.BankAccounts.BankAccountEntry>,
|
||||||
|
RequestError<MerchantBackend.ErrorDetail>
|
||||||
|
>([`/private/accounts/${h_wire}`], fetcher, {
|
||||||
|
refreshInterval: 0,
|
||||||
|
refreshWhenHidden: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
refreshWhenOffline: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isValidating) return { loading: true, data: data?.data };
|
||||||
|
if (data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (error) return error.cause;
|
||||||
|
return { loading: true };
|
||||||
|
}
|
@ -19,9 +19,10 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { StateUpdater, useCallback, useState } from "preact/hooks";
|
import { StateUpdater, useCallback, useEffect, useState } from "preact/hooks";
|
||||||
import { ValueOrFunction } from "../utils/types.js";
|
import { ValueOrFunction } from "../utils/types.js";
|
||||||
import { useMemoryStorage } from "@gnu-taler/web-util/browser";
|
import { useMemoryStorage } from "@gnu-taler/web-util/browser";
|
||||||
|
import { useMatchMutate } from "./backend.js";
|
||||||
|
|
||||||
const calculateRootPath = () => {
|
const calculateRootPath = () => {
|
||||||
const rootPath =
|
const rootPath =
|
||||||
@ -56,8 +57,22 @@ export function useBackendDefaultToken(
|
|||||||
): [string | undefined, ((d: string | undefined) => void)] {
|
): [string | undefined, ((d: string | undefined) => void)] {
|
||||||
// uncomment for testing
|
// uncomment for testing
|
||||||
initialValue = "secret-token:secret" as string | undefined
|
initialValue = "secret-token:secret" as string | undefined
|
||||||
const { update, value } = useMemoryStorage(`backend-token`, initialValue)
|
const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token`, initialValue)
|
||||||
return [value, update];
|
const clearCache = useMatchMutate()
|
||||||
|
useEffect(() => {
|
||||||
|
clearCache()
|
||||||
|
}, [token])
|
||||||
|
|
||||||
|
function updateToken(
|
||||||
|
value: (string | undefined)
|
||||||
|
): void {
|
||||||
|
if (value === undefined) {
|
||||||
|
reset()
|
||||||
|
} else {
|
||||||
|
setToken(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [token, updateToken];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBackendInstanceToken(
|
export function useBackendInstanceToken(
|
||||||
@ -73,14 +88,12 @@ export function useBackendInstanceToken(
|
|||||||
function updateToken(
|
function updateToken(
|
||||||
value: (string | undefined)
|
value: (string | undefined)
|
||||||
): void {
|
): void {
|
||||||
console.log("seeting token", value)
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
reset()
|
reset()
|
||||||
} else {
|
} else {
|
||||||
setToken(value)
|
setToken(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("token", token)
|
|
||||||
|
|
||||||
return [token, updateToken];
|
return [token, updateToken];
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ describe("instance api interaction with details", () => {
|
|||||||
name: "instance_name",
|
name: "instance_name",
|
||||||
auth: {
|
auth: {
|
||||||
method: "token",
|
method: "token",
|
||||||
token: "not-secret",
|
// token: "not-secret",
|
||||||
},
|
},
|
||||||
} as MerchantBackend.Instances.QueryInstancesResponse,
|
} as MerchantBackend.Instances.QueryInstancesResponse,
|
||||||
});
|
});
|
||||||
@ -154,7 +154,7 @@ describe("instance api interaction with details", () => {
|
|||||||
name: "instance_name",
|
name: "instance_name",
|
||||||
auth: {
|
auth: {
|
||||||
method: "token",
|
method: "token",
|
||||||
token: "secret",
|
// token: "secret",
|
||||||
},
|
},
|
||||||
} as MerchantBackend.Instances.QueryInstancesResponse,
|
} as MerchantBackend.Instances.QueryInstancesResponse,
|
||||||
});
|
});
|
||||||
@ -190,7 +190,7 @@ describe("instance api interaction with details", () => {
|
|||||||
name: "instance_name",
|
name: "instance_name",
|
||||||
auth: {
|
auth: {
|
||||||
method: "token",
|
method: "token",
|
||||||
token: "not-secret",
|
// token: "not-secret",
|
||||||
},
|
},
|
||||||
} as MerchantBackend.Instances.QueryInstancesResponse,
|
} as MerchantBackend.Instances.QueryInstancesResponse,
|
||||||
});
|
});
|
||||||
|
@ -198,6 +198,7 @@ export function useInstanceDetails(): HttpResponse<
|
|||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateOnReconnect: false,
|
revalidateOnReconnect: false,
|
||||||
refreshWhenOffline: false,
|
refreshWhenOffline: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
errorRetryCount: 0,
|
errorRetryCount: 0,
|
||||||
errorRetryInterval: 1,
|
errorRetryInterval: 1,
|
||||||
shouldRetryOnError: false,
|
shouldRetryOnError: false,
|
||||||
@ -211,7 +212,7 @@ export function useInstanceDetails(): HttpResponse<
|
|||||||
|
|
||||||
type KYCStatus =
|
type KYCStatus =
|
||||||
| { type: "ok" }
|
| { type: "ok" }
|
||||||
| { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects };
|
| { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects };
|
||||||
|
|
||||||
export function useInstanceKYCDetails(): HttpResponse<
|
export function useInstanceKYCDetails(): HttpResponse<
|
||||||
KYCStatus,
|
KYCStatus,
|
||||||
@ -220,7 +221,7 @@ export function useInstanceKYCDetails(): HttpResponse<
|
|||||||
const { fetcher } = useBackendInstanceRequest();
|
const { fetcher } = useBackendInstanceRequest();
|
||||||
|
|
||||||
const { data, error } = useSWR<
|
const { data, error } = useSWR<
|
||||||
HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>,
|
HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>,
|
||||||
RequestError<MerchantBackend.ErrorDetail>
|
RequestError<MerchantBackend.ErrorDetail>
|
||||||
>([`/private/kyc`], fetcher, {
|
>([`/private/kyc`], fetcher, {
|
||||||
refreshInterval: 60 * 1000,
|
refreshInterval: 60 * 1000,
|
||||||
|
223
packages/merchant-backoffice-ui/src/hooks/otp.ts
Normal file
223
packages/merchant-backoffice-ui/src/hooks/otp.ts
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 {
|
||||||
|
HttpResponse,
|
||||||
|
HttpResponseOk,
|
||||||
|
HttpResponsePaginated,
|
||||||
|
RequestError,
|
||||||
|
} from "@gnu-taler/web-util/browser";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { MerchantBackend } from "../declaration.js";
|
||||||
|
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
|
||||||
|
import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
|
||||||
|
|
||||||
|
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
|
||||||
|
import _useSWR, { SWRHook } from "swr";
|
||||||
|
const useSWR = _useSWR as unknown as SWRHook;
|
||||||
|
|
||||||
|
const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = {
|
||||||
|
"1": {
|
||||||
|
otp_description: "first device",
|
||||||
|
otp_algorithm: 1,
|
||||||
|
otp_device_id: "1",
|
||||||
|
otp_key: "123",
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
otp_description: "second device",
|
||||||
|
otp_algorithm: 0,
|
||||||
|
otp_device_id: "2",
|
||||||
|
otp_key: "456",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOtpDeviceAPI(): OtpDeviceAPI {
|
||||||
|
const mutateAll = useMatchMutate();
|
||||||
|
const { request } = useBackendInstanceRequest();
|
||||||
|
|
||||||
|
const createOtpDevice = async (
|
||||||
|
data: MerchantBackend.OTP.OtpDeviceAddDetails,
|
||||||
|
): Promise<HttpResponseOk<void>> => {
|
||||||
|
// MOCKED_DEVICES[data.otp_device_id] = data
|
||||||
|
// return Promise.resolve({ ok: true, data: undefined });
|
||||||
|
const res = await request<void>(`/private/otp-devices`, {
|
||||||
|
method: "POST",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
await mutateAll(/.*private\/otp-devices.*/);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOtpDevice = async (
|
||||||
|
deviceId: string,
|
||||||
|
data: MerchantBackend.OTP.OtpDevicePatchDetails,
|
||||||
|
): Promise<HttpResponseOk<void>> => {
|
||||||
|
// MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm
|
||||||
|
// MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr
|
||||||
|
// MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description
|
||||||
|
// MOCKED_DEVICES[deviceId].otp_key = data.otp_key
|
||||||
|
// return Promise.resolve({ ok: true, data: undefined });
|
||||||
|
const res = await request<void>(`/private/otp-devices/${deviceId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
await mutateAll(/.*private\/otp-devices.*/);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteOtpDevice = async (
|
||||||
|
deviceId: string,
|
||||||
|
): Promise<HttpResponseOk<void>> => {
|
||||||
|
// delete MOCKED_DEVICES[deviceId]
|
||||||
|
// return Promise.resolve({ ok: true, data: undefined });
|
||||||
|
const res = await request<void>(`/private/otp-devices/${deviceId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
await mutateAll(/.*private\/otp-devices.*/);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createOtpDevice,
|
||||||
|
updateOtpDevice,
|
||||||
|
deleteOtpDevice,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OtpDeviceAPI {
|
||||||
|
createOtpDevice: (
|
||||||
|
data: MerchantBackend.OTP.OtpDeviceAddDetails,
|
||||||
|
) => Promise<HttpResponseOk<void>>;
|
||||||
|
updateOtpDevice: (
|
||||||
|
id: string,
|
||||||
|
data: MerchantBackend.OTP.OtpDevicePatchDetails,
|
||||||
|
) => Promise<HttpResponseOk<void>>;
|
||||||
|
deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceOtpDeviceFilter {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useInstanceOtpDevices(
|
||||||
|
args?: InstanceOtpDeviceFilter,
|
||||||
|
updatePosition?: (id: string) => void,
|
||||||
|
): HttpResponsePaginated<
|
||||||
|
MerchantBackend.OTP.OtpDeviceSummaryResponse,
|
||||||
|
MerchantBackend.ErrorDetail
|
||||||
|
> {
|
||||||
|
// return {
|
||||||
|
// ok: true,
|
||||||
|
// loadMore: () => { },
|
||||||
|
// loadMorePrev: () => { },
|
||||||
|
// data: {
|
||||||
|
// otp_devices: Object.values(MOCKED_DEVICES).map(d => ({
|
||||||
|
// device_description: d.otp_device_description,
|
||||||
|
// otp_device_id: d.otp_device_id
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const { fetcher } = useBackendInstanceRequest();
|
||||||
|
|
||||||
|
const [pageAfter, setPageAfter] = useState(1);
|
||||||
|
|
||||||
|
const totalAfter = pageAfter * PAGE_SIZE;
|
||||||
|
const {
|
||||||
|
data: afterData,
|
||||||
|
error: afterError,
|
||||||
|
isValidating: loadingAfter,
|
||||||
|
} = useSWR<
|
||||||
|
HttpResponseOk<MerchantBackend.OTP.OtpDeviceSummaryResponse>,
|
||||||
|
RequestError<MerchantBackend.ErrorDetail>
|
||||||
|
>([`/private/otp-devices`], fetcher);
|
||||||
|
|
||||||
|
const [lastAfter, setLastAfter] = useState<
|
||||||
|
HttpResponse<
|
||||||
|
MerchantBackend.OTP.OtpDeviceSummaryResponse,
|
||||||
|
MerchantBackend.ErrorDetail
|
||||||
|
>
|
||||||
|
>({ loading: true });
|
||||||
|
useEffect(() => {
|
||||||
|
if (afterData) setLastAfter(afterData);
|
||||||
|
}, [afterData /*, beforeData*/]);
|
||||||
|
|
||||||
|
if (afterError) return afterError.cause;
|
||||||
|
|
||||||
|
// if the query returns less that we ask, then we have reach the end or beginning
|
||||||
|
const isReachingEnd =
|
||||||
|
afterData && afterData.data.otp_devices.length < totalAfter;
|
||||||
|
const isReachingStart = false;
|
||||||
|
|
||||||
|
const pagination = {
|
||||||
|
isReachingEnd,
|
||||||
|
isReachingStart,
|
||||||
|
loadMore: () => {
|
||||||
|
if (!afterData || isReachingEnd) return;
|
||||||
|
if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) {
|
||||||
|
setPageAfter(pageAfter + 1);
|
||||||
|
} else {
|
||||||
|
const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1]
|
||||||
|
.otp_device_id
|
||||||
|
}`;
|
||||||
|
if (from && updatePosition) updatePosition(from);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadMorePrev: () => {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices;
|
||||||
|
if (loadingAfter /* || loadingBefore */)
|
||||||
|
return { loading: true, data: { otp_devices } };
|
||||||
|
if (/*beforeData &&*/ afterData) {
|
||||||
|
return { ok: true, data: { otp_devices }, ...pagination };
|
||||||
|
}
|
||||||
|
return { loading: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOtpDeviceDetails(
|
||||||
|
deviceId: string,
|
||||||
|
): HttpResponse<
|
||||||
|
MerchantBackend.OTP.OtpDeviceDetails,
|
||||||
|
MerchantBackend.ErrorDetail
|
||||||
|
> {
|
||||||
|
// return {
|
||||||
|
// ok: true,
|
||||||
|
// data: {
|
||||||
|
// device_description: MOCKED_DEVICES[deviceId].otp_device_description,
|
||||||
|
// otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm,
|
||||||
|
// otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
const { fetcher } = useBackendInstanceRequest();
|
||||||
|
|
||||||
|
const { data, error, isValidating } = useSWR<
|
||||||
|
HttpResponseOk<MerchantBackend.OTP.OtpDeviceDetails>,
|
||||||
|
RequestError<MerchantBackend.ErrorDetail>
|
||||||
|
>([`/private/otp-devices/${deviceId}`], fetcher, {
|
||||||
|
refreshInterval: 0,
|
||||||
|
refreshWhenHidden: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
refreshWhenOffline: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isValidating) return { loading: true, data: data?.data };
|
||||||
|
if (data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (error) return error.cause;
|
||||||
|
return { loading: true };
|
||||||
|
}
|
@ -25,16 +25,16 @@ import {
|
|||||||
useInstanceReserves,
|
useInstanceReserves,
|
||||||
useReserveDetails,
|
useReserveDetails,
|
||||||
useReservesAPI,
|
useReservesAPI,
|
||||||
useTipDetails,
|
useRewardDetails,
|
||||||
} from "./reserves.js";
|
} from "./reserves.js";
|
||||||
import { ApiMockEnvironment } from "./testing.js";
|
import { ApiMockEnvironment } from "./testing.js";
|
||||||
import {
|
import {
|
||||||
API_AUTHORIZE_TIP,
|
API_AUTHORIZE_REWARD,
|
||||||
API_AUTHORIZE_TIP_FOR_RESERVE,
|
API_AUTHORIZE_REWARD_FOR_RESERVE,
|
||||||
API_CREATE_RESERVE,
|
API_CREATE_RESERVE,
|
||||||
API_DELETE_RESERVE,
|
API_DELETE_RESERVE,
|
||||||
API_GET_RESERVE_BY_ID,
|
API_GET_RESERVE_BY_ID,
|
||||||
API_GET_TIP_BY_ID,
|
API_GET_REWARD_BY_ID,
|
||||||
API_LIST_RESERVES,
|
API_LIST_RESERVES,
|
||||||
} from "./urls.js";
|
} from "./urls.js";
|
||||||
import * as tests from "@gnu-taler/web-util/testing";
|
import * as tests from "@gnu-taler/web-util/testing";
|
||||||
@ -48,7 +48,7 @@ describe("reserve api interaction with listing", () => {
|
|||||||
reserves: [
|
reserves: [
|
||||||
{
|
{
|
||||||
reserve_pub: "11",
|
reserve_pub: "11",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -89,10 +89,10 @@ describe("reserve api interaction with listing", () => {
|
|||||||
reserves: [
|
reserves: [
|
||||||
{
|
{
|
||||||
reserve_pub: "11",
|
reserve_pub: "11",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
{
|
{
|
||||||
reserve_pub: "22",
|
reserve_pub: "22",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -115,10 +115,10 @@ describe("reserve api interaction with listing", () => {
|
|||||||
reserves: [
|
reserves: [
|
||||||
{
|
{
|
||||||
reserve_pub: "11",
|
reserve_pub: "11",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
{
|
{
|
||||||
reserve_pub: "22",
|
reserve_pub: "22",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -138,13 +138,13 @@ describe("reserve api interaction with listing", () => {
|
|||||||
reserves: [
|
reserves: [
|
||||||
{
|
{
|
||||||
reserve_pub: "11",
|
reserve_pub: "11",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
{
|
{
|
||||||
reserve_pub: "22",
|
reserve_pub: "22",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
{
|
{
|
||||||
reserve_pub: "33",
|
reserve_pub: "33",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -182,10 +182,10 @@ describe("reserve api interaction with listing", () => {
|
|||||||
reserves: [
|
reserves: [
|
||||||
{
|
{
|
||||||
reserve_pub: "22",
|
reserve_pub: "22",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
{
|
{
|
||||||
reserve_pub: "33",
|
reserve_pub: "33",
|
||||||
} as MerchantBackend.Tips.ReserveStatusEntry,
|
} as MerchantBackend.Rewards.ReserveStatusEntry,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -213,16 +213,16 @@ describe("reserve api interaction with listing", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("reserve api interaction with details", () => {
|
describe("reserve api interaction with details", () => {
|
||||||
it("should evict cache when adding a tip for a specific reserve", async () => {
|
it("should evict cache when adding a reward for a specific reserve", async () => {
|
||||||
const env = new ApiMockEnvironment();
|
const env = new ApiMockEnvironment();
|
||||||
|
|
||||||
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
|
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
|
||||||
response: {
|
response: {
|
||||||
accounts: [{ payto_uri: "payto://here" }],
|
accounts: [{ payto_uri: "payto://here" }],
|
||||||
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
|
rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
|
||||||
} as MerchantBackend.Tips.ReserveDetail,
|
} as MerchantBackend.Rewards.ReserveDetail,
|
||||||
qparam: {
|
qparam: {
|
||||||
tips: "yes",
|
rewards: "yes",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -246,37 +246,37 @@ describe("reserve api interaction with details", () => {
|
|||||||
if (!query.ok) return;
|
if (!query.ok) return;
|
||||||
expect(query.data).deep.equals({
|
expect(query.data).deep.equals({
|
||||||
accounts: [{ payto_uri: "payto://here" }],
|
accounts: [{ payto_uri: "payto://here" }],
|
||||||
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
|
rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), {
|
env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), {
|
||||||
request: {
|
request: {
|
||||||
amount: "USD:12",
|
amount: "USD:12",
|
||||||
justification: "not",
|
justification: "not",
|
||||||
next_url: "http://taler.net",
|
next_url: "http://taler.net",
|
||||||
},
|
},
|
||||||
response: {
|
response: {
|
||||||
tip_id: "id2",
|
reward_id: "id2",
|
||||||
taler_tip_uri: "uri",
|
taler_reward_uri: "uri",
|
||||||
tip_expiration: { t_s: 1 },
|
reward_expiration: { t_s: 1 },
|
||||||
tip_status_url: "url",
|
reward_status_url: "url",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
|
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
|
||||||
response: {
|
response: {
|
||||||
accounts: [{ payto_uri: "payto://here" }],
|
accounts: [{ payto_uri: "payto://here" }],
|
||||||
tips: [
|
rewards: [
|
||||||
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
|
{ reason: "why?", reward_id: "id1", total_amount: "USD:10" },
|
||||||
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
|
{ reason: "not", reward_id: "id2", total_amount: "USD:12" },
|
||||||
],
|
],
|
||||||
} as MerchantBackend.Tips.ReserveDetail,
|
} as MerchantBackend.Rewards.ReserveDetail,
|
||||||
qparam: {
|
qparam: {
|
||||||
tips: "yes",
|
rewards: "yes",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
api.authorizeTipReserve("11", {
|
api.authorizeRewardReserve("11", {
|
||||||
amount: "USD:12",
|
amount: "USD:12",
|
||||||
justification: "not",
|
justification: "not",
|
||||||
next_url: "http://taler.net",
|
next_url: "http://taler.net",
|
||||||
@ -294,9 +294,9 @@ describe("reserve api interaction with details", () => {
|
|||||||
|
|
||||||
expect(query.data).deep.equals({
|
expect(query.data).deep.equals({
|
||||||
accounts: [{ payto_uri: "payto://here" }],
|
accounts: [{ payto_uri: "payto://here" }],
|
||||||
tips: [
|
rewards: [
|
||||||
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
|
{ reason: "why?", reward_id: "id1", total_amount: "USD:10" },
|
||||||
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
|
{ reason: "not", reward_id: "id2", total_amount: "USD:12" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -308,16 +308,16 @@ describe("reserve api interaction with details", () => {
|
|||||||
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
|
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should evict cache when adding a tip for a random reserve", async () => {
|
it("should evict cache when adding a reward for a random reserve", async () => {
|
||||||
const env = new ApiMockEnvironment();
|
const env = new ApiMockEnvironment();
|
||||||
|
|
||||||
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
|
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
|
||||||
response: {
|
response: {
|
||||||
accounts: [{ payto_uri: "payto://here" }],
|
accounts: [{ payto_uri: "payto://here" }],
|
||||||
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
|
rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
|
||||||
} as MerchantBackend.Tips.ReserveDetail,
|
} as MerchantBackend.Rewards.ReserveDetail,
|
||||||
qparam: {
|
qparam: {
|
||||||
tips: "yes",
|
rewards: "yes",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -341,37 +341,37 @@ describe("reserve api interaction with details", () => {
|
|||||||
if (!query.ok) return;
|
if (!query.ok) return;
|
||||||
expect(query.data).deep.equals({
|
expect(query.data).deep.equals({
|
||||||
accounts: [{ payto_uri: "payto://here" }],
|
accounts: [{ payto_uri: "payto://here" }],
|
||||||
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
|
rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
env.addRequestExpectation(API_AUTHORIZE_TIP, {
|
env.addRequestExpectation(API_AUTHORIZE_REWARD, {
|
||||||
request: {
|
request: {
|
||||||
amount: "USD:12",
|
amount: "USD:12",
|
||||||
justification: "not",
|
justification: "not",
|
||||||
next_url: "http://taler.net",
|
next_url: "http://taler.net",
|
||||||
},
|
},
|
||||||
response: {
|
response: {
|
||||||
tip_id: "id2",
|
reward_id: "id2",
|
||||||
taler_tip_uri: "uri",
|
taler_reward_uri: "uri",
|
||||||
tip_expiration: { t_s: 1 },
|
reward_expiration: { t_s: 1 },
|
||||||
tip_status_url: "url",
|
reward_status_url: "url",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
|
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
|
||||||
response: {
|
response: {
|
||||||
accounts: [{ payto_uri: "payto://here" }],
|
accounts: [{ payto_uri: "payto://here" }],
|
||||||
tips: [
|
rewards: [
|
||||||
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
|
{ reason: "why?", reward_id: "id1", total_amount: "USD:10" },
|
||||||
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
|
{ reason: "not", reward_id: "id2", total_amount: "USD:12" },
|
||||||
],
|
],
|
||||||
} as MerchantBackend.Tips.ReserveDetail,
|
} as MerchantBackend.Rewards.ReserveDetail,
|
||||||
qparam: {
|
qparam: {
|
||||||
tips: "yes",
|
rewards: "yes",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
api.authorizeTip({
|
api.authorizeReward({
|
||||||
amount: "USD:12",
|
amount: "USD:12",
|
||||||
justification: "not",
|
justification: "not",
|
||||||
next_url: "http://taler.net",
|
next_url: "http://taler.net",
|
||||||
@ -387,9 +387,9 @@ describe("reserve api interaction with details", () => {
|
|||||||
|
|
||||||
expect(query.data).deep.equals({
|
expect(query.data).deep.equals({
|
||||||
accounts: [{ payto_uri: "payto://here" }],
|
accounts: [{ payto_uri: "payto://here" }],
|
||||||
tips: [
|
rewards: [
|
||||||
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
|
{ reason: "why?", reward_id: "id1", total_amount: "USD:10" },
|
||||||
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
|
{ reason: "not", reward_id: "id2", total_amount: "USD:12" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -402,15 +402,15 @@ describe("reserve api interaction with details", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("reserve api interaction with tip details", () => {
|
describe("reserve api interaction with reward details", () => {
|
||||||
it("should list tips", async () => {
|
it("should list rewards", async () => {
|
||||||
const env = new ApiMockEnvironment();
|
const env = new ApiMockEnvironment();
|
||||||
|
|
||||||
env.addRequestExpectation(API_GET_TIP_BY_ID("11"), {
|
env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), {
|
||||||
response: {
|
response: {
|
||||||
total_picked_up: "USD:12",
|
total_picked_up: "USD:12",
|
||||||
reason: "not",
|
reason: "not",
|
||||||
} as MerchantBackend.Tips.TipDetails,
|
} as MerchantBackend.Rewards.RewardDetails,
|
||||||
qparam: {
|
qparam: {
|
||||||
pickups: "yes",
|
pickups: "yes",
|
||||||
},
|
},
|
||||||
@ -418,7 +418,7 @@ describe("reserve api interaction with tip details", () => {
|
|||||||
|
|
||||||
const hookBehavior = await tests.hookBehaveLikeThis(
|
const hookBehavior = await tests.hookBehaveLikeThis(
|
||||||
() => {
|
() => {
|
||||||
const query = useTipDetails("11");
|
const query = useRewardDetails("11");
|
||||||
return { query };
|
return { query };
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
|
@ -31,11 +31,11 @@ export function useReservesAPI(): ReserveMutateAPI {
|
|||||||
const { request } = useBackendInstanceRequest();
|
const { request } = useBackendInstanceRequest();
|
||||||
|
|
||||||
const createReserve = async (
|
const createReserve = async (
|
||||||
data: MerchantBackend.Tips.ReserveCreateRequest,
|
data: MerchantBackend.Rewards.ReserveCreateRequest,
|
||||||
): Promise<
|
): Promise<
|
||||||
HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>
|
HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>
|
||||||
> => {
|
> => {
|
||||||
const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>(
|
const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>(
|
||||||
`/private/reserves`,
|
`/private/reserves`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -49,12 +49,12 @@ export function useReservesAPI(): ReserveMutateAPI {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizeTipReserve = async (
|
const authorizeRewardReserve = async (
|
||||||
pub: string,
|
pub: string,
|
||||||
data: MerchantBackend.Tips.TipCreateRequest,
|
data: MerchantBackend.Rewards.RewardCreateRequest,
|
||||||
): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
|
): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => {
|
||||||
const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(
|
const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>(
|
||||||
`/private/reserves/${pub}/authorize-tip`,
|
`/private/reserves/${pub}/authorize-reward`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data,
|
data,
|
||||||
@ -67,11 +67,11 @@ export function useReservesAPI(): ReserveMutateAPI {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
const authorizeTip = async (
|
const authorizeReward = async (
|
||||||
data: MerchantBackend.Tips.TipCreateRequest,
|
data: MerchantBackend.Rewards.RewardCreateRequest,
|
||||||
): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
|
): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => {
|
||||||
const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(
|
const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>(
|
||||||
`/private/tips`,
|
`/private/rewards`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data,
|
data,
|
||||||
@ -97,33 +97,33 @@ export function useReservesAPI(): ReserveMutateAPI {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve };
|
return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReserveMutateAPI {
|
export interface ReserveMutateAPI {
|
||||||
createReserve: (
|
createReserve: (
|
||||||
data: MerchantBackend.Tips.ReserveCreateRequest,
|
data: MerchantBackend.Rewards.ReserveCreateRequest,
|
||||||
) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>;
|
) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>;
|
||||||
authorizeTipReserve: (
|
authorizeRewardReserve: (
|
||||||
id: string,
|
id: string,
|
||||||
data: MerchantBackend.Tips.TipCreateRequest,
|
data: MerchantBackend.Rewards.RewardCreateRequest,
|
||||||
) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
|
) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>;
|
||||||
authorizeTip: (
|
authorizeReward: (
|
||||||
data: MerchantBackend.Tips.TipCreateRequest,
|
data: MerchantBackend.Rewards.RewardCreateRequest,
|
||||||
) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
|
) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>;
|
||||||
deleteReserve: (
|
deleteReserve: (
|
||||||
id: string,
|
id: string,
|
||||||
) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>;
|
) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInstanceReserves(): HttpResponse<
|
export function useInstanceReserves(): HttpResponse<
|
||||||
MerchantBackend.Tips.TippingReserveStatus,
|
MerchantBackend.Rewards.RewardReserveStatus,
|
||||||
MerchantBackend.ErrorDetail
|
MerchantBackend.ErrorDetail
|
||||||
> {
|
> {
|
||||||
const { fetcher } = useBackendInstanceRequest();
|
const { fetcher } = useBackendInstanceRequest();
|
||||||
|
|
||||||
const { data, error, isValidating } = useSWR<
|
const { data, error, isValidating } = useSWR<
|
||||||
HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>,
|
HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>,
|
||||||
RequestError<MerchantBackend.ErrorDetail>
|
RequestError<MerchantBackend.ErrorDetail>
|
||||||
>([`/private/reserves`], fetcher);
|
>([`/private/reserves`], fetcher);
|
||||||
|
|
||||||
@ -136,13 +136,13 @@ export function useInstanceReserves(): HttpResponse<
|
|||||||
export function useReserveDetails(
|
export function useReserveDetails(
|
||||||
reserveId: string,
|
reserveId: string,
|
||||||
): HttpResponse<
|
): HttpResponse<
|
||||||
MerchantBackend.Tips.ReserveDetail,
|
MerchantBackend.Rewards.ReserveDetail,
|
||||||
MerchantBackend.ErrorDetail
|
MerchantBackend.ErrorDetail
|
||||||
> {
|
> {
|
||||||
const { reserveDetailFetcher } = useBackendInstanceRequest();
|
const { reserveDetailFetcher } = useBackendInstanceRequest();
|
||||||
|
|
||||||
const { data, error, isValidating } = useSWR<
|
const { data, error, isValidating } = useSWR<
|
||||||
HttpResponseOk<MerchantBackend.Tips.ReserveDetail>,
|
HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>,
|
||||||
RequestError<MerchantBackend.ErrorDetail>
|
RequestError<MerchantBackend.ErrorDetail>
|
||||||
>([`/private/reserves/${reserveId}`], reserveDetailFetcher, {
|
>([`/private/reserves/${reserveId}`], reserveDetailFetcher, {
|
||||||
refreshInterval: 0,
|
refreshInterval: 0,
|
||||||
@ -158,15 +158,15 @@ export function useReserveDetails(
|
|||||||
return { loading: true };
|
return { loading: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTipDetails(
|
export function useRewardDetails(
|
||||||
tipId: string,
|
rewardId: string,
|
||||||
): HttpResponse<MerchantBackend.Tips.TipDetails, MerchantBackend.ErrorDetail> {
|
): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> {
|
||||||
const { tipsDetailFetcher } = useBackendInstanceRequest();
|
const { rewardsDetailFetcher } = useBackendInstanceRequest();
|
||||||
|
|
||||||
const { data, error, isValidating } = useSWR<
|
const { data, error, isValidating } = useSWR<
|
||||||
HttpResponseOk<MerchantBackend.Tips.TipDetails>,
|
HttpResponseOk<MerchantBackend.Rewards.RewardDetails>,
|
||||||
RequestError<MerchantBackend.ErrorDetail>
|
RequestError<MerchantBackend.ErrorDetail>
|
||||||
>([`/private/tips/${tipId}`], tipsDetailFetcher, {
|
>([`/private/rewards/${rewardId}`], rewardsDetailFetcher, {
|
||||||
refreshInterval: 0,
|
refreshInterval: 0,
|
||||||
refreshWhenHidden: false,
|
refreshWhenHidden: false,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
|
@ -139,15 +139,15 @@ export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({
|
|||||||
////////////////////
|
////////////////////
|
||||||
|
|
||||||
export const API_CREATE_RESERVE: Query<
|
export const API_CREATE_RESERVE: Query<
|
||||||
MerchantBackend.Tips.ReserveCreateRequest,
|
MerchantBackend.Rewards.ReserveCreateRequest,
|
||||||
MerchantBackend.Tips.ReserveCreateConfirmation
|
MerchantBackend.Rewards.ReserveCreateConfirmation
|
||||||
> = {
|
> = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "http://backend/instances/default/private/reserves",
|
url: "http://backend/instances/default/private/reserves",
|
||||||
};
|
};
|
||||||
export const API_LIST_RESERVES: Query<
|
export const API_LIST_RESERVES: Query<
|
||||||
unknown,
|
unknown,
|
||||||
MerchantBackend.Tips.TippingReserveStatus
|
MerchantBackend.Rewards.RewardReserveStatus
|
||||||
> = {
|
> = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "http://backend/instances/default/private/reserves",
|
url: "http://backend/instances/default/private/reserves",
|
||||||
@ -155,34 +155,34 @@ export const API_LIST_RESERVES: Query<
|
|||||||
|
|
||||||
export const API_GET_RESERVE_BY_ID = (
|
export const API_GET_RESERVE_BY_ID = (
|
||||||
pub: string,
|
pub: string,
|
||||||
): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({
|
): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `http://backend/instances/default/private/reserves/${pub}`,
|
url: `http://backend/instances/default/private/reserves/${pub}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const API_GET_TIP_BY_ID = (
|
export const API_GET_REWARD_BY_ID = (
|
||||||
pub: string,
|
pub: string,
|
||||||
): Query<unknown, MerchantBackend.Tips.TipDetails> => ({
|
): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `http://backend/instances/default/private/tips/${pub}`,
|
url: `http://backend/instances/default/private/rewards/${pub}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const API_AUTHORIZE_TIP_FOR_RESERVE = (
|
export const API_AUTHORIZE_REWARD_FOR_RESERVE = (
|
||||||
pub: string,
|
pub: string,
|
||||||
): Query<
|
): Query<
|
||||||
MerchantBackend.Tips.TipCreateRequest,
|
MerchantBackend.Rewards.RewardCreateRequest,
|
||||||
MerchantBackend.Tips.TipCreateConfirmation
|
MerchantBackend.Rewards.RewardCreateConfirmation
|
||||||
> => ({
|
> => ({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`,
|
url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const API_AUTHORIZE_TIP: Query<
|
export const API_AUTHORIZE_REWARD: Query<
|
||||||
MerchantBackend.Tips.TipCreateRequest,
|
MerchantBackend.Rewards.RewardCreateRequest,
|
||||||
MerchantBackend.Tips.TipCreateConfirmation
|
MerchantBackend.Rewards.RewardCreateConfirmation
|
||||||
> = {
|
> = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `http://backend/instances/default/private/tips`,
|
url: `http://backend/instances/default/private/rewards`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({
|
export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({
|
||||||
@ -211,7 +211,7 @@ export const API_GET_INSTANCE_BY_ID = (
|
|||||||
|
|
||||||
export const API_GET_INSTANCE_KYC_BY_ID = (
|
export const API_GET_INSTANCE_KYC_BY_ID = (
|
||||||
id: string,
|
id: string,
|
||||||
): Query<unknown, MerchantBackend.Instances.AccountKycRedirects> => ({
|
): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `http://backend/management/instances/${id}/kyc`,
|
url: `http://backend/management/instances/${id}/kyc`,
|
||||||
});
|
});
|
||||||
@ -263,7 +263,7 @@ export const API_GET_CURRENT_INSTANCE: Query<
|
|||||||
|
|
||||||
export const API_GET_CURRENT_INSTANCE_KYC: Query<
|
export const API_GET_CURRENT_INSTANCE_KYC: Query<
|
||||||
unknown,
|
unknown,
|
||||||
MerchantBackend.Instances.AccountKycRedirects
|
MerchantBackend.KYC.AccountKycRedirects
|
||||||
> = {
|
> = {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `http://backend/instances/default/private/kyc`,
|
url: `http://backend/instances/default/private/kyc`,
|
||||||
|
@ -19,6 +19,9 @@ import {
|
|||||||
Codec,
|
Codec,
|
||||||
buildCodecForObject,
|
buildCodecForObject,
|
||||||
codecForBoolean,
|
codecForBoolean,
|
||||||
|
codecForConstString,
|
||||||
|
codecForEither,
|
||||||
|
codecForString,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
|
function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
|
||||||
@ -31,29 +34,49 @@ function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
advanceOrderMode: boolean
|
advanceOrderMode: boolean;
|
||||||
|
dateFormat: "ymd" | "dmy" | "mdy";
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSettings: Settings = {
|
const defaultSettings: Settings = {
|
||||||
advanceOrderMode: false,
|
advanceOrderMode: false,
|
||||||
|
dateFormat: "ymd",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForSettings = (): Codec<Settings> =>
|
export const codecForSettings = (): Codec<Settings> =>
|
||||||
buildCodecForObject<Settings>()
|
buildCodecForObject<Settings>()
|
||||||
.property("advanceOrderMode", codecForBoolean())
|
.property("advanceOrderMode", codecForBoolean())
|
||||||
|
.property("dateFormat", codecForEither(
|
||||||
|
codecForConstString("ymd"),
|
||||||
|
codecForConstString("dmy"),
|
||||||
|
codecForConstString("mdy"),
|
||||||
|
))
|
||||||
.build("Settings");
|
.build("Settings");
|
||||||
|
|
||||||
const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings());
|
const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings());
|
||||||
|
|
||||||
export function useSettings(): [
|
export function useSettings(): [
|
||||||
Readonly<Settings>,
|
Readonly<Settings>,
|
||||||
<T extends keyof Settings>(key: T, value: Settings[T]) => void,
|
(s: Settings) => void,
|
||||||
] {
|
] {
|
||||||
const { value, update } = useLocalStorage(SETTINGS_KEY);
|
const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings);
|
||||||
|
|
||||||
const parsed: Settings = value ?? defaultSettings;
|
// const parsed: Settings = value ?? defaultSettings;
|
||||||
function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
|
// function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
|
||||||
update({ ...parsed, [k]: v });
|
// const next = { ...parsed, [k]: v }
|
||||||
}
|
// update(next);
|
||||||
return [parsed, updateField];
|
// }
|
||||||
|
return [value, update];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateFormatForSettings(s: Settings): string {
|
||||||
|
switch (s.dateFormat) {
|
||||||
|
case "ymd": return "yyyy/MM/dd"
|
||||||
|
case "dmy": return "dd/MM/yyyy"
|
||||||
|
case "mdy": return "MM/dd/yyyy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function datetimeFormatForSettings(s: Settings): string {
|
||||||
|
return dateFormatForSettings(s) + " HH:mm:ss"
|
||||||
}
|
}
|
@ -19,7 +19,6 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Amounts } from "@gnu-taler/taler-util";
|
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
@ -29,9 +28,8 @@ import {
|
|||||||
FormProvider,
|
FormProvider,
|
||||||
} from "../../../components/form/FormProvider.js";
|
} from "../../../components/form/FormProvider.js";
|
||||||
import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
|
import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
|
||||||
import { SetTokenNewInstanceModal } from "../../../components/modal/index.js";
|
|
||||||
import { MerchantBackend } from "../../../declaration.js";
|
import { MerchantBackend } from "../../../declaration.js";
|
||||||
import { INSTANCE_ID_REGEX, PAYTO_REGEX } from "../../../utils/constants.js";
|
import { INSTANCE_ID_REGEX } from "../../../utils/constants.js";
|
||||||
import { undefinedIfEmpty } from "../../../utils/table.js";
|
import { undefinedIfEmpty } from "../../../utils/table.js";
|
||||||
|
|
||||||
export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & {
|
export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & {
|
||||||
@ -47,19 +45,19 @@ interface Props {
|
|||||||
function with_defaults(id?: string): Partial<Entity> {
|
function with_defaults(id?: string): Partial<Entity> {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
accounts: [],
|
// accounts: [],
|
||||||
user_type: "business",
|
user_type: "business",
|
||||||
|
use_stefan: false,
|
||||||
default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours
|
default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours
|
||||||
default_wire_fee_amortization: 1,
|
|
||||||
default_wire_transfer_delay: { d_us: 1000 * 2 * 60 * 60 * 24 * 1000 }, // two days
|
default_wire_transfer_delay: { d_us: 1000 * 2 * 60 * 60 * 24 * 1000 }, // two days
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
|
export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
|
||||||
const [value, valueHandler] = useState(with_defaults(forceId));
|
const [value, valueHandler] = useState(with_defaults(forceId));
|
||||||
const [isTokenSet, updateIsTokenSet] = useState<boolean>(false);
|
// const [isTokenSet, updateIsTokenSet] = useState<boolean>(false);
|
||||||
const [isTokenDialogActive, updateIsTokenDialogActive] =
|
// const [isTokenDialogActive, updateIsTokenDialogActive] =
|
||||||
useState<boolean>(false);
|
// useState<boolean>(false);
|
||||||
|
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
@ -67,42 +65,24 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
|
|||||||
id: !value.id
|
id: !value.id
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: !INSTANCE_ID_REGEX.test(value.id)
|
: !INSTANCE_ID_REGEX.test(value.id)
|
||||||
? i18n.str`is not valid`
|
? i18n.str`is not valid`
|
||||||
: undefined,
|
: undefined,
|
||||||
name: !value.name ? i18n.str`required` : undefined,
|
name: !value.name ? i18n.str`required` : undefined,
|
||||||
user_type: !value.user_type
|
user_type: !value.user_type
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: value.user_type !== "business" && value.user_type !== "individual"
|
: value.user_type !== "business" && value.user_type !== "individual"
|
||||||
? i18n.str`should be business or individual`
|
? i18n.str`should be business or individual`
|
||||||
: undefined,
|
|
||||||
accounts:
|
|
||||||
!value.accounts || !value.accounts.length
|
|
||||||
? i18n.str`required`
|
|
||||||
: undefinedIfEmpty(
|
|
||||||
value.accounts.map((p) => {
|
|
||||||
return !PAYTO_REGEX.test(p.payto_uri)
|
|
||||||
? i18n.str`is not valid`
|
|
||||||
: undefined;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
default_max_deposit_fee: !value.default_max_deposit_fee
|
|
||||||
? i18n.str`required`
|
|
||||||
: !Amounts.parse(value.default_max_deposit_fee)
|
|
||||||
? i18n.str`invalid format`
|
|
||||||
: undefined,
|
|
||||||
default_max_wire_fee: !value.default_max_wire_fee
|
|
||||||
? i18n.str`required`
|
|
||||||
: !Amounts.parse(value.default_max_wire_fee)
|
|
||||||
? i18n.str`invalid format`
|
|
||||||
: undefined,
|
|
||||||
default_wire_fee_amortization:
|
|
||||||
value.default_wire_fee_amortization === undefined
|
|
||||||
? i18n.str`required`
|
|
||||||
: isNaN(value.default_wire_fee_amortization)
|
|
||||||
? i18n.str`is not a number`
|
|
||||||
: value.default_wire_fee_amortization < 1
|
|
||||||
? i18n.str`must be 1 or greater`
|
|
||||||
: undefined,
|
: undefined,
|
||||||
|
// accounts:
|
||||||
|
// !value.accounts || !value.accounts.length
|
||||||
|
// ? i18n.str`required`
|
||||||
|
// : undefinedIfEmpty(
|
||||||
|
// value.accounts.map((p) => {
|
||||||
|
// return !PAYTO_REGEX.test(p.payto_uri)
|
||||||
|
// ? i18n.str`is not valid`
|
||||||
|
// : undefined;
|
||||||
|
// }),
|
||||||
|
// ),
|
||||||
default_pay_delay: !value.default_pay_delay
|
default_pay_delay: !value.default_pay_delay
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -129,12 +109,12 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
|
|||||||
|
|
||||||
const submit = (): Promise<void> => {
|
const submit = (): Promise<void> => {
|
||||||
// use conversion instead of this
|
// use conversion instead of this
|
||||||
const newToken = value.auth_token;
|
// const newToken = value.auth_token;
|
||||||
value.auth_token = undefined;
|
// value.auth_token = undefined;
|
||||||
value.auth =
|
value.auth = { method: "external" }
|
||||||
newToken === null || newToken === undefined
|
// newToken === null || newToken === undefined
|
||||||
? { method: "external" }
|
// ? { method: "external" }
|
||||||
: { method: "token", token: `secret-token:${newToken}` };
|
// : { method: "token", token: `secret-token:${newToken}` };
|
||||||
if (!value.address) value.address = {};
|
if (!value.address) value.address = {};
|
||||||
if (!value.jurisdiction) value.jurisdiction = {};
|
if (!value.jurisdiction) value.jurisdiction = {};
|
||||||
// remove above use conversion
|
// remove above use conversion
|
||||||
@ -142,16 +122,16 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
|
|||||||
return onCreate(value as Entity);
|
return onCreate(value as Entity);
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateToken(token: string | null) {
|
// function updateToken(token: string | null) {
|
||||||
valueHandler((old) => ({
|
// valueHandler((old) => ({
|
||||||
...old,
|
// ...old,
|
||||||
auth_token: token === null ? undefined : token,
|
// auth_token: token === null ? undefined : token,
|
||||||
}));
|
// }));
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="columns">
|
{/* <div class="columns">
|
||||||
<div class="column" />
|
<div class="column" />
|
||||||
<div class="column is-four-fifths">
|
<div class="column is-four-fifths">
|
||||||
{isTokenDialogActive && (
|
{isTokenDialogActive && (
|
||||||
@ -174,9 +154,9 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="column" />
|
<div class="column" />
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<section class="hero is-hero-bar">
|
{/* <section class="hero is-hero-bar">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<div class="level-item has-text-centered">
|
<div class="level-item has-text-centered">
|
||||||
@ -186,8 +166,8 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
|
|||||||
!isTokenSet
|
!isTokenSet
|
||||||
? "button is-danger has-tooltip-bottom"
|
? "button is-danger has-tooltip-bottom"
|
||||||
: !value.auth_token
|
: !value.auth_token
|
||||||
? "button has-tooltip-bottom"
|
? "button has-tooltip-bottom"
|
||||||
: "button is-info has-tooltip-bottom"
|
: "button is-info has-tooltip-bottom"
|
||||||
}
|
}
|
||||||
data-tooltip={i18n.str`change authorization configuration`}
|
data-tooltip={i18n.str`change authorization configuration`}
|
||||||
onClick={() => updateIsTokenDialogActive(true)}
|
onClick={() => updateIsTokenDialogActive(true)}
|
||||||
@ -228,7 +208,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section> */}
|
||||||
|
|
||||||
<section class="section is-main-section">
|
<section class="section is-main-section">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
@ -250,7 +230,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
|
|||||||
)}
|
)}
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
disabled={!isTokenSet || hasErrors}
|
disabled={hasErrors}
|
||||||
data-tooltip={
|
data-tooltip={
|
||||||
hasErrors
|
hasErrors
|
||||||
? i18n.str`Need to complete marked fields and choose authorization method`
|
? i18n.str`Need to complete marked fields and choose authorization method`
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { h, VNode, FunctionalComponent } from "preact";
|
||||||
|
import { CreatePage as TestedComponent } from "./CreatePage.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Pages/Accounts/Create",
|
||||||
|
component: TestedComponent,
|
||||||
|
};
|
@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
|
||||||
|
import {
|
||||||
|
FormErrors,
|
||||||
|
FormProvider,
|
||||||
|
} from "../../../../components/form/FormProvider.js";
|
||||||
|
import { Input } from "../../../../components/form/Input.js";
|
||||||
|
import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
|
||||||
|
import { InputSelector } from "../../../../components/form/InputSelector.js";
|
||||||
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
||||||
|
|
||||||
|
type Entity = MerchantBackend.BankAccounts.AccountAddDetails & { repeatPassword: string };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onCreate: (d: Entity) => Promise<void>;
|
||||||
|
onBack?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountAuthType = ["none", "basic"];
|
||||||
|
|
||||||
|
function isValidURL(s: string): boolean {
|
||||||
|
try {
|
||||||
|
const u = new URL(s)
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
const [state, setState] = useState<Partial<Entity>>({});
|
||||||
|
const errors: FormErrors<Entity> = {
|
||||||
|
payto_uri: !state.payto_uri ? i18n.str`required` : undefined,
|
||||||
|
|
||||||
|
credit_facade_credentials: !state.credit_facade_credentials
|
||||||
|
? undefined
|
||||||
|
: undefinedIfEmpty({
|
||||||
|
username:
|
||||||
|
state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username
|
||||||
|
? i18n.str`required`
|
||||||
|
: undefined,
|
||||||
|
password:
|
||||||
|
state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password
|
||||||
|
? i18n.str`required`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
credit_facade_url: !state.credit_facade_url
|
||||||
|
? undefined
|
||||||
|
: !isValidURL(state.credit_facade_url) ? i18n.str`not valid url`
|
||||||
|
: undefined,
|
||||||
|
repeatPassword:
|
||||||
|
!state.credit_facade_credentials
|
||||||
|
? undefined
|
||||||
|
: state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword)
|
||||||
|
? i18n.str`is not the same`
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasErrors = Object.keys(errors).some(
|
||||||
|
(k) => (errors as any)[k] !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
if (hasErrors) return Promise.reject();
|
||||||
|
delete state.repeatPassword
|
||||||
|
return onCreate(state as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<section class="section is-main-section">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column" />
|
||||||
|
<div class="column is-four-fifths">
|
||||||
|
<FormProvider
|
||||||
|
object={state}
|
||||||
|
valueHandler={setState}
|
||||||
|
errors={errors}
|
||||||
|
>
|
||||||
|
<InputPaytoForm<Entity>
|
||||||
|
name="payto_uri"
|
||||||
|
label={i18n.str`Account`}
|
||||||
|
/>
|
||||||
|
<Input<Entity>
|
||||||
|
name="credit_facade_url"
|
||||||
|
label={i18n.str`Account info URL`}
|
||||||
|
help="https://bank.com"
|
||||||
|
expand
|
||||||
|
tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
|
||||||
|
/>
|
||||||
|
<InputSelector
|
||||||
|
name="credit_facade_credentials.type"
|
||||||
|
label={i18n.str`Auth type`}
|
||||||
|
tooltip={i18n.str`Choose the authentication type for the account info URL`}
|
||||||
|
values={accountAuthType}
|
||||||
|
toStr={(str) => {
|
||||||
|
if (str === "none") return "Without authentication";
|
||||||
|
return "Username and password";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{state.credit_facade_credentials?.type === "basic" ? (
|
||||||
|
<Fragment>
|
||||||
|
<Input
|
||||||
|
name="credit_facade_credentials.username"
|
||||||
|
label={i18n.str`Username`}
|
||||||
|
tooltip={i18n.str`Username to access the account information.`}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="credit_facade_credentials.password"
|
||||||
|
inputType="password"
|
||||||
|
label={i18n.str`Password`}
|
||||||
|
tooltip={i18n.str`Password to access the account information.`}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="repeatPassword"
|
||||||
|
inputType="password"
|
||||||
|
label={i18n.str`Repeat password`}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
) : undefined}
|
||||||
|
</FormProvider>
|
||||||
|
|
||||||
|
<div class="buttons is-right mt-5">
|
||||||
|
{onBack && (
|
||||||
|
<button class="button" onClick={onBack}>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<AsyncButton
|
||||||
|
disabled={hasErrors}
|
||||||
|
data-tooltip={
|
||||||
|
hasErrors
|
||||||
|
? i18n.str`Need to complete marked fields`
|
||||||
|
: "confirm operation"
|
||||||
|
}
|
||||||
|
onClick={submitForm}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Confirm</i18n.Translate>
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { NotificationCard } from "../../../../components/menu/index.js";
|
||||||
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
import { useWebhookAPI } from "../../../../hooks/webhooks.js";
|
||||||
|
import { Notification } from "../../../../utils/types.js";
|
||||||
|
import { CreatePage } from "./CreatePage.js";
|
||||||
|
import { useOtpDeviceAPI } from "../../../../hooks/otp.js";
|
||||||
|
import { useBankAccountAPI } from "../../../../hooks/bank.js";
|
||||||
|
|
||||||
|
export type Entity = MerchantBackend.BankAccounts.AccountAddDetails;
|
||||||
|
interface Props {
|
||||||
|
onBack?: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
|
||||||
|
const { createBankAccount } = useBankAccountAPI();
|
||||||
|
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NotificationCard notification={notif} />
|
||||||
|
<CreatePage
|
||||||
|
onBack={onBack}
|
||||||
|
onCreate={(request: Entity) => {
|
||||||
|
return createBankAccount(request)
|
||||||
|
.then((d) => {
|
||||||
|
onConfirm()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`could not create device`,
|
||||||
|
type: "ERROR",
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { FunctionalComponent, h } from "preact";
|
||||||
|
import { ListPage as TestedComponent } from "./ListPage.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Pages/Accounts/List",
|
||||||
|
component: TestedComponent,
|
||||||
|
};
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { CardTable } from "./Table.js";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
devices: MerchantBackend.BankAccounts.BankAccountEntry[];
|
||||||
|
onLoadMoreBefore?: () => void;
|
||||||
|
onLoadMoreAfter?: () => void;
|
||||||
|
onCreate: () => void;
|
||||||
|
onDelete: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
|
||||||
|
onSelect: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListPage({
|
||||||
|
devices,
|
||||||
|
onCreate,
|
||||||
|
onDelete,
|
||||||
|
onSelect,
|
||||||
|
onLoadMoreBefore,
|
||||||
|
onLoadMoreAfter,
|
||||||
|
}: Props): VNode {
|
||||||
|
const form = { payto_uri: "" };
|
||||||
|
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
return (
|
||||||
|
<section class="section is-main-section">
|
||||||
|
<CardTable
|
||||||
|
accounts={devices.map((o) => ({
|
||||||
|
...o,
|
||||||
|
id: String(o.h_wire),
|
||||||
|
}))}
|
||||||
|
onCreate={onCreate}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onLoadMoreBefore={onLoadMoreBefore}
|
||||||
|
hasMoreBefore={!onLoadMoreBefore}
|
||||||
|
onLoadMoreAfter={onLoadMoreAfter}
|
||||||
|
hasMoreAfter={!onLoadMoreAfter}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,385 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { StateUpdater, useState } from "preact/hooks";
|
||||||
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
|
type Entity = MerchantBackend.BankAccounts.BankAccountEntry;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
accounts: Entity[];
|
||||||
|
onDelete: (e: Entity) => void;
|
||||||
|
onSelect: (e: Entity) => void;
|
||||||
|
onCreate: () => void;
|
||||||
|
onLoadMoreBefore?: () => void;
|
||||||
|
hasMoreBefore?: boolean;
|
||||||
|
hasMoreAfter?: boolean;
|
||||||
|
onLoadMoreAfter?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardTable({
|
||||||
|
accounts,
|
||||||
|
onCreate,
|
||||||
|
onDelete,
|
||||||
|
onSelect,
|
||||||
|
onLoadMoreAfter,
|
||||||
|
onLoadMoreBefore,
|
||||||
|
hasMoreAfter,
|
||||||
|
hasMoreBefore,
|
||||||
|
}: Props): VNode {
|
||||||
|
const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="card has-table">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="mdi mdi-newspaper" />
|
||||||
|
</span>
|
||||||
|
<i18n.Translate>Bank accounts</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
<div class="card-header-icon" aria-label="more options">
|
||||||
|
<span
|
||||||
|
class="has-tooltip-left"
|
||||||
|
data-tooltip={i18n.str`add new accounts`}
|
||||||
|
>
|
||||||
|
<button class="button is-info" type="button" onClick={onCreate}>
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="mdi mdi-plus mdi-36px" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="b-table has-pagination">
|
||||||
|
<div class="table-wrapper has-mobile-cards">
|
||||||
|
{accounts.length > 0 ? (
|
||||||
|
<Table
|
||||||
|
accounts={accounts}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onSelect={onSelect}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
rowSelectionHandler={rowSelectionHandler}
|
||||||
|
onLoadMoreAfter={onLoadMoreAfter}
|
||||||
|
onLoadMoreBefore={onLoadMoreBefore}
|
||||||
|
hasMoreAfter={hasMoreAfter}
|
||||||
|
hasMoreBefore={hasMoreBefore}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyTable />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
interface TableProps {
|
||||||
|
rowSelection: string[];
|
||||||
|
accounts: Entity[];
|
||||||
|
onDelete: (e: Entity) => void;
|
||||||
|
onSelect: (e: Entity) => void;
|
||||||
|
rowSelectionHandler: StateUpdater<string[]>;
|
||||||
|
onLoadMoreBefore?: () => void;
|
||||||
|
hasMoreBefore?: boolean;
|
||||||
|
hasMoreAfter?: boolean;
|
||||||
|
onLoadMoreAfter?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelected<T>(id: T): (prev: T[]) => T[] {
|
||||||
|
return (prev: T[]): T[] =>
|
||||||
|
prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Table({
|
||||||
|
accounts,
|
||||||
|
onLoadMoreAfter,
|
||||||
|
onDelete,
|
||||||
|
onSelect,
|
||||||
|
onLoadMoreBefore,
|
||||||
|
hasMoreAfter,
|
||||||
|
hasMoreBefore,
|
||||||
|
}: TableProps): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], }
|
||||||
|
const accountsByType = accounts.reduce((prev, acc) => {
|
||||||
|
const parsed = parsePaytoUri(acc.payto_uri)
|
||||||
|
if (!parsed) return prev //skip
|
||||||
|
if (parsed.targetType !== "bitcoin" && parsed.targetType !== "x-taler-bank" && parsed.targetType !== "iban") {
|
||||||
|
prev["unknown"].push({ parsed, acc })
|
||||||
|
} else {
|
||||||
|
prev[parsed.targetType].push({ parsed, acc })
|
||||||
|
}
|
||||||
|
return prev
|
||||||
|
}, emptyList)
|
||||||
|
|
||||||
|
const bitcoinAccounts = accountsByType["bitcoin"]
|
||||||
|
const talerbankAccounts = accountsByType["x-taler-bank"]
|
||||||
|
const ibanAccounts = accountsByType["iban"]
|
||||||
|
const unkownAccounts = accountsByType["unknown"]
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
|
||||||
|
{bitcoinAccounts.length > 0 && <div class="table-container">
|
||||||
|
<p class="card-header-title"><i18n.Translate>Bitcoin type accounts</i18n.Translate></p>
|
||||||
|
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>Address</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>Sewgit 1</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>Sewgit 2</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{bitcoinAccounts.map(({ parsed, acc }, idx) => {
|
||||||
|
const ac = parsed as PaytoUriBitcoin
|
||||||
|
return (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.targetPath}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.segwitAddrs[0]}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.segwitAddrs[1]}
|
||||||
|
</td>
|
||||||
|
<td class="is-actions-cell right-sticky">
|
||||||
|
<div class="buttons is-right">
|
||||||
|
<button
|
||||||
|
class="button is-danger is-small has-tooltip-left"
|
||||||
|
data-tooltip={i18n.str`delete selected accounts from the database`}
|
||||||
|
onClick={() => onDelete(acc)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{talerbankAccounts.length > 0 && <div class="table-container">
|
||||||
|
<p class="card-header-title"><i18n.Translate>Taler type accounts</i18n.Translate></p>
|
||||||
|
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>Host</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>Account name</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{talerbankAccounts.map(({ parsed, acc }, idx) => {
|
||||||
|
const ac = parsed as PaytoUriTalerBank
|
||||||
|
return (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.host}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.account}
|
||||||
|
</td>
|
||||||
|
<td class="is-actions-cell right-sticky">
|
||||||
|
<div class="buttons is-right">
|
||||||
|
<button
|
||||||
|
class="button is-danger is-small has-tooltip-left"
|
||||||
|
data-tooltip={i18n.str`delete selected accounts from the database`}
|
||||||
|
onClick={() => onDelete(acc)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{ibanAccounts.length > 0 && <div class="table-container">
|
||||||
|
<p class="card-header-title"><i18n.Translate>IBAN type accounts</i18n.Translate></p>
|
||||||
|
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>Account name</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>IBAN</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>BIC</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ibanAccounts.map(({ parsed, acc }, idx) => {
|
||||||
|
const ac = parsed as PaytoUriIBAN
|
||||||
|
return (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.params["receiver-name"]}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.iban}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.bic ?? ""}
|
||||||
|
</td>
|
||||||
|
<td class="is-actions-cell right-sticky">
|
||||||
|
<div class="buttons is-right">
|
||||||
|
<button
|
||||||
|
class="button is-danger is-small has-tooltip-left"
|
||||||
|
data-tooltip={i18n.str`delete selected accounts from the database`}
|
||||||
|
onClick={() => onDelete(acc)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{unkownAccounts.length > 0 && <div class="table-container">
|
||||||
|
<p class="card-header-title"><i18n.Translate>Other type accounts</i18n.Translate></p>
|
||||||
|
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>Type</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<i18n.Translate>Path</i18n.Translate>
|
||||||
|
</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{unkownAccounts.map(({ parsed, acc }, idx) => {
|
||||||
|
const ac = parsed as PaytoUriUnknown
|
||||||
|
return (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.targetType}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
onClick={(): void => onSelect(acc)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{ac.targetPath}
|
||||||
|
</td>
|
||||||
|
<td class="is-actions-cell right-sticky">
|
||||||
|
<div class="buttons is-right">
|
||||||
|
<button
|
||||||
|
class="button is-danger is-small has-tooltip-left"
|
||||||
|
data-tooltip={i18n.str`delete selected accounts from the database`}
|
||||||
|
onClick={() => onDelete(acc)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>}
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyTable(): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
return (
|
||||||
|
<div class="content has-text-grey has-text-centered">
|
||||||
|
<p>
|
||||||
|
<span class="icon is-large">
|
||||||
|
<i class="mdi mdi-emoticon-sad mdi-48px" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i18n.Translate>
|
||||||
|
There is no accounts yet, add more pressing the + sign
|
||||||
|
</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
|
import {
|
||||||
|
ErrorType,
|
||||||
|
HttpError,
|
||||||
|
useTranslationContext,
|
||||||
|
} from "@gnu-taler/web-util/browser";
|
||||||
|
import { Fragment, VNode, h } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { Loading } from "../../../../components/exception/loading.js";
|
||||||
|
import { NotificationCard } from "../../../../components/menu/index.js";
|
||||||
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
|
||||||
|
import { Notification } from "../../../../utils/types.js";
|
||||||
|
import { ListPage } from "./ListPage.js";
|
||||||
|
import { useBankAccountAPI, useInstanceBankAccounts } from "../../../../hooks/bank.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onUnauthorized: () => VNode;
|
||||||
|
onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
|
||||||
|
onNotFound: () => VNode;
|
||||||
|
onCreate: () => void;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListValidators({
|
||||||
|
onUnauthorized,
|
||||||
|
onLoadError,
|
||||||
|
onCreate,
|
||||||
|
onSelect,
|
||||||
|
onNotFound,
|
||||||
|
}: Props): VNode {
|
||||||
|
const [position, setPosition] = useState<string | undefined>(undefined);
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||||
|
const { deleteBankAccount } = useBankAccountAPI();
|
||||||
|
const result = useInstanceBankAccounts({ position }, (id) => setPosition(id));
|
||||||
|
|
||||||
|
if (result.loading) return <Loading />;
|
||||||
|
if (!result.ok) {
|
||||||
|
if (
|
||||||
|
result.type === ErrorType.CLIENT &&
|
||||||
|
result.status === HttpStatusCode.Unauthorized
|
||||||
|
)
|
||||||
|
return onUnauthorized();
|
||||||
|
if (
|
||||||
|
result.type === ErrorType.CLIENT &&
|
||||||
|
result.status === HttpStatusCode.NotFound
|
||||||
|
)
|
||||||
|
return onNotFound();
|
||||||
|
return onLoadError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<NotificationCard notification={notif} />
|
||||||
|
|
||||||
|
<ListPage
|
||||||
|
devices={result.data.accounts}
|
||||||
|
onLoadMoreBefore={
|
||||||
|
result.isReachingStart ? result.loadMorePrev : undefined
|
||||||
|
}
|
||||||
|
onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
|
||||||
|
onCreate={onCreate}
|
||||||
|
onSelect={(e) => {
|
||||||
|
onSelect(e.h_wire);
|
||||||
|
}}
|
||||||
|
onDelete={(e: MerchantBackend.BankAccounts.BankAccountEntry) =>
|
||||||
|
deleteBankAccount(e.h_wire)
|
||||||
|
.then(() =>
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`bank account delete successfully`,
|
||||||
|
type: "SUCCESS",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch((error) =>
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`could not delete the bank account`,
|
||||||
|
type: "ERROR",
|
||||||
|
description: error.message,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { h, VNode, FunctionalComponent } from "preact";
|
||||||
|
import { UpdatePage as TestedComponent } from "./UpdatePage.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Pages/Validators/Update",
|
||||||
|
component: TestedComponent,
|
||||||
|
argTypes: {
|
||||||
|
onUpdate: { action: "onUpdate" },
|
||||||
|
onBack: { action: "onBack" },
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
|
||||||
|
import {
|
||||||
|
FormErrors,
|
||||||
|
FormProvider,
|
||||||
|
} from "../../../../components/form/FormProvider.js";
|
||||||
|
import { Input } from "../../../../components/form/Input.js";
|
||||||
|
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||||
|
|
||||||
|
type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onUpdate: (d: Entity) => Promise<void>;
|
||||||
|
onBack?: () => void;
|
||||||
|
account: Entity;
|
||||||
|
}
|
||||||
|
export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
const [state, setState] = useState<Partial<Entity>>(account);
|
||||||
|
|
||||||
|
const errors: FormErrors<Entity> = {
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasErrors = Object.keys(errors).some(
|
||||||
|
(k) => (errors as any)[k] !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
if (hasErrors) return Promise.reject();
|
||||||
|
return onUpdate(state as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<section class="section">
|
||||||
|
<section class="hero is-hero-bar">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item">
|
||||||
|
<span class="is-size-4">
|
||||||
|
Account: <b>{account.id}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<section class="section is-main-section">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-four-fifths">
|
||||||
|
<FormProvider
|
||||||
|
object={state}
|
||||||
|
valueHandler={setState}
|
||||||
|
errors={errors}
|
||||||
|
>
|
||||||
|
<Input<Entity>
|
||||||
|
name="credit_facade_url"
|
||||||
|
label={i18n.str`Description`}
|
||||||
|
tooltip={i18n.str`dddd`}
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
|
||||||
|
<div class="buttons is-right mt-5">
|
||||||
|
{onBack && (
|
||||||
|
<button class="button" onClick={onBack}>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<AsyncButton
|
||||||
|
disabled={hasErrors}
|
||||||
|
data-tooltip={
|
||||||
|
hasErrors
|
||||||
|
? i18n.str`Need to complete marked fields`
|
||||||
|
: "confirm operation"
|
||||||
|
}
|
||||||
|
onClick={submitForm}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Confirm</i18n.Translate>
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
|
import {
|
||||||
|
ErrorType,
|
||||||
|
HttpError,
|
||||||
|
useTranslationContext,
|
||||||
|
} from "@gnu-taler/web-util/browser";
|
||||||
|
import { Fragment, VNode, h } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { Loading } from "../../../../components/exception/loading.js";
|
||||||
|
import { NotificationCard } from "../../../../components/menu/index.js";
|
||||||
|
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||||
|
import { useBankAccountAPI, useBankAccountDetails } from "../../../../hooks/bank.js";
|
||||||
|
import { Notification } from "../../../../utils/types.js";
|
||||||
|
import { UpdatePage } from "./UpdatePage.js";
|
||||||
|
|
||||||
|
export type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onBack?: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onUnauthorized: () => VNode;
|
||||||
|
onNotFound: () => VNode;
|
||||||
|
onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
|
||||||
|
bid: string;
|
||||||
|
}
|
||||||
|
export default function UpdateValidator({
|
||||||
|
bid,
|
||||||
|
onConfirm,
|
||||||
|
onBack,
|
||||||
|
onUnauthorized,
|
||||||
|
onNotFound,
|
||||||
|
onLoadError,
|
||||||
|
}: Props): VNode {
|
||||||
|
const { updateBankAccount } = useBankAccountAPI();
|
||||||
|
const result = useBankAccountDetails(bid);
|
||||||
|
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||||
|
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
if (result.loading) return <Loading />;
|
||||||
|
if (!result.ok) {
|
||||||
|
if (
|
||||||
|
result.type === ErrorType.CLIENT &&
|
||||||
|
result.status === HttpStatusCode.Unauthorized
|
||||||
|
)
|
||||||
|
return onUnauthorized();
|
||||||
|
if (
|
||||||
|
result.type === ErrorType.CLIENT &&
|
||||||
|
result.status === HttpStatusCode.NotFound
|
||||||
|
)
|
||||||
|
return onNotFound();
|
||||||
|
return onLoadError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<NotificationCard notification={notif} />
|
||||||
|
<UpdatePage
|
||||||
|
account={{ ...result.data, id: bid }}
|
||||||
|
onBack={onBack}
|
||||||
|
onUpdate={(data) => {
|
||||||
|
return updateBankAccount(bid, data)
|
||||||
|
.then(onConfirm)
|
||||||
|
.catch((error) => {
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`could not update account`,
|
||||||
|
type: "ERROR",
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
@ -36,14 +36,13 @@ interface Props {
|
|||||||
function convert(
|
function convert(
|
||||||
from: MerchantBackend.Instances.QueryInstancesResponse,
|
from: MerchantBackend.Instances.QueryInstancesResponse,
|
||||||
): Entity {
|
): Entity {
|
||||||
const { accounts: allAccounts, ...rest } = from;
|
|
||||||
const accounts = allAccounts.filter((a) => a.active);
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
default_wire_fee_amortization: 1,
|
default_wire_fee_amortization: 1,
|
||||||
|
use_stefan: true,
|
||||||
default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour
|
default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour
|
||||||
default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours
|
default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours
|
||||||
};
|
};
|
||||||
return { ...defaults, ...rest, accounts };
|
return { ...defaults, ...from };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetailPage({ selected }: Props): VNode {
|
export function DetailPage({ selected }: Props): VNode {
|
||||||
@ -74,11 +73,6 @@ export function DetailPage({ selected }: Props): VNode {
|
|||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<FormProvider<Entity> object={value} valueHandler={valueHandler}>
|
<FormProvider<Entity> object={value} valueHandler={valueHandler}>
|
||||||
<Input<Entity> name="name" readonly label={i18n.str`Name`} />
|
<Input<Entity> name="name" readonly label={i18n.str`Name`} />
|
||||||
<Input<Entity>
|
|
||||||
name="accounts"
|
|
||||||
readonly
|
|
||||||
label={i18n.str`Account address`}
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
<div class="column" />
|
<div class="column" />
|
||||||
|
@ -51,17 +51,15 @@ function createExample<Props>(
|
|||||||
|
|
||||||
export const Example = createExample(TestedComponent, {
|
export const Example = createExample(TestedComponent, {
|
||||||
selected: {
|
selected: {
|
||||||
accounts: [],
|
|
||||||
name: "name",
|
name: "name",
|
||||||
auth: { method: "external" },
|
auth: { method: "external" },
|
||||||
address: {},
|
address: {},
|
||||||
|
user_type: "business",
|
||||||
jurisdiction: {},
|
jurisdiction: {},
|
||||||
default_max_deposit_fee: "TESTKUDOS:2",
|
use_stefan: true,
|
||||||
default_max_wire_fee: "TESTKUDOS:1",
|
|
||||||
default_pay_delay: {
|
default_pay_delay: {
|
||||||
d_us: 1000 * 1000, //one second
|
d_us: 1000 * 1000, //one second
|
||||||
},
|
},
|
||||||
default_wire_fee_amortization: 1,
|
|
||||||
default_wire_transfer_delay: {
|
default_wire_transfer_delay: {
|
||||||
d_us: 1000 * 1000, //one second
|
d_us: 1000 * 1000, //one second
|
||||||
},
|
},
|
||||||
|
@ -54,5 +54,5 @@ export const Example = tests.createExample(TestedComponent, {
|
|||||||
payto_uri: "payto://iban/de123123123",
|
payto_uri: "payto://iban/de123123123",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as MerchantBackend.Instances.AccountKycRedirects,
|
} as MerchantBackend.KYC.AccountKycRedirects,
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@ import { h, VNode } from "preact";
|
|||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
status: MerchantBackend.Instances.AccountKycRedirects;
|
status: MerchantBackend.KYC.AccountKycRedirects;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListPage({ status }: Props): VNode {
|
export function ListPage({ status }: Props): VNode {
|
||||||
@ -85,11 +85,11 @@ export function ListPage({ status }: Props): VNode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
interface PendingTableProps {
|
interface PendingTableProps {
|
||||||
entries: MerchantBackend.Instances.MerchantAccountKycRedirect[];
|
entries: MerchantBackend.KYC.MerchantAccountKycRedirect[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimedOutTableProps {
|
interface TimedOutTableProps {
|
||||||
entries: MerchantBackend.Instances.ExchangeKycTimeout[];
|
entries: MerchantBackend.KYC.ExchangeKycTimeout[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function PendingTable({ entries }: PendingTableProps): VNode {
|
function PendingTable({ entries }: PendingTableProps): VNode {
|
||||||
|
@ -42,12 +42,13 @@ function createExample<Props>(
|
|||||||
|
|
||||||
export const Example = createExample(TestedComponent, {
|
export const Example = createExample(TestedComponent, {
|
||||||
instanceConfig: {
|
instanceConfig: {
|
||||||
default_max_deposit_fee: "",
|
|
||||||
default_max_wire_fee: "",
|
|
||||||
default_pay_delay: {
|
default_pay_delay: {
|
||||||
d_us: 1000 * 1000 * 60 * 60, //one hour
|
d_us: 1000 * 1000 * 60 * 60, //one hour
|
||||||
},
|
},
|
||||||
default_wire_fee_amortization: 1,
|
default_wire_transfer_delay: {
|
||||||
|
d_us: 1000 * 1000 * 60 * 60, //one hour
|
||||||
|
},
|
||||||
|
use_stefan: true,
|
||||||
},
|
},
|
||||||
instanceInventory: [
|
instanceInventory: [
|
||||||
{
|
{
|
||||||
|
@ -44,6 +44,7 @@ import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
|
|||||||
import { rate } from "../../../../utils/amount.js";
|
import { rate } from "../../../../utils/amount.js";
|
||||||
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
||||||
import { useSettings } from "../../../../hooks/useSettings.js";
|
import { useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
import { InputToggle } from "../../../../components/form/InputToggle.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
|
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
|
||||||
@ -52,34 +53,38 @@ interface Props {
|
|||||||
instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[];
|
instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[];
|
||||||
}
|
}
|
||||||
interface InstanceConfig {
|
interface InstanceConfig {
|
||||||
default_max_wire_fee: string;
|
use_stefan: boolean;
|
||||||
default_max_deposit_fee: string;
|
|
||||||
default_wire_fee_amortization: number;
|
|
||||||
default_pay_delay: Duration;
|
default_pay_delay: Duration;
|
||||||
|
default_wire_transfer_delay: Duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
function with_defaults(config: InstanceConfig): Partial<Entity> {
|
function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> {
|
||||||
const defaultPayDeadline =
|
const defaultPayDeadline =
|
||||||
!config.default_pay_delay || config.default_pay_delay.d_us === "forever"
|
!config.default_pay_delay || config.default_pay_delay.d_us === "forever"
|
||||||
? undefined
|
? undefined
|
||||||
: add(new Date(), {
|
: add(new Date(), {
|
||||||
seconds: config.default_pay_delay.d_us / (1000 * 1000),
|
seconds: config.default_pay_delay.d_us / (1000 * 1000),
|
||||||
});
|
});
|
||||||
|
const defaultWireDeadline =
|
||||||
|
!config.default_wire_transfer_delay || config.default_wire_transfer_delay.d_us === "forever"
|
||||||
|
? undefined
|
||||||
|
: add(new Date(), {
|
||||||
|
seconds: config.default_wire_transfer_delay.d_us / (1000 * 1000),
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inventoryProducts: {},
|
inventoryProducts: {},
|
||||||
products: [],
|
products: [],
|
||||||
pricing: {},
|
pricing: {},
|
||||||
payments: {
|
payments: {
|
||||||
max_wire_fee: config.default_max_wire_fee,
|
max_fee: undefined,
|
||||||
max_fee: config.default_max_deposit_fee,
|
|
||||||
wire_fee_amortization: config.default_wire_fee_amortization,
|
|
||||||
pay_deadline: defaultPayDeadline,
|
pay_deadline: defaultPayDeadline,
|
||||||
refund_deadline: defaultPayDeadline,
|
refund_deadline: defaultPayDeadline,
|
||||||
createToken: true,
|
createToken: true,
|
||||||
|
wire_transfer_deadline: defaultWireDeadline,
|
||||||
},
|
},
|
||||||
shipping: {},
|
shipping: {},
|
||||||
extra: "",
|
extra: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,8 +112,6 @@ interface Payments {
|
|||||||
wire_transfer_deadline?: Date;
|
wire_transfer_deadline?: Date;
|
||||||
auto_refund_deadline?: Date;
|
auto_refund_deadline?: Date;
|
||||||
max_fee?: string;
|
max_fee?: string;
|
||||||
max_wire_fee?: string;
|
|
||||||
wire_fee_amortization?: number;
|
|
||||||
createToken: boolean;
|
createToken: boolean;
|
||||||
minimum_age?: number;
|
minimum_age?: number;
|
||||||
}
|
}
|
||||||
@ -118,7 +121,7 @@ interface Entity {
|
|||||||
pricing: Partial<Pricing>;
|
pricing: Partial<Pricing>;
|
||||||
payments: Partial<Payments>;
|
payments: Partial<Payments>;
|
||||||
shipping: Partial<Shipping>;
|
shipping: Partial<Shipping>;
|
||||||
extra: string;
|
extra: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringIsValidJSON = (value: string) => {
|
const stringIsValidJSON = (value: string) => {
|
||||||
@ -136,8 +139,9 @@ export function CreatePage({
|
|||||||
instanceConfig,
|
instanceConfig,
|
||||||
instanceInventory,
|
instanceInventory,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const [value, valueHandler] = useState(with_defaults(instanceConfig));
|
|
||||||
const config = useConfigContext();
|
const config = useConfigContext();
|
||||||
|
const instance_default = with_defaults(instanceConfig, config.currency)
|
||||||
|
const [value, valueHandler] = useState(instance_default);
|
||||||
const zero = Amounts.zeroOfCurrency(config.currency);
|
const zero = Amounts.zeroOfCurrency(config.currency);
|
||||||
const [settings] = useSettings()
|
const [settings] = useSettings()
|
||||||
const inventoryList = Object.values(value.inventoryProducts || {});
|
const inventoryList = Object.values(value.inventoryProducts || {});
|
||||||
@ -160,10 +164,10 @@ export function CreatePage({
|
|||||||
? i18n.str`must be greater than 0`
|
? i18n.str`must be greater than 0`
|
||||||
: undefined,
|
: undefined,
|
||||||
}),
|
}),
|
||||||
extra:
|
// extra:
|
||||||
value.extra && !stringIsValidJSON(value.extra)
|
// value.extra && !stringIsValidJSON(value.extra)
|
||||||
? i18n.str`not a valid json`
|
// ? i18n.str`not a valid json`
|
||||||
: undefined,
|
// : undefined,
|
||||||
payments: undefinedIfEmpty({
|
payments: undefinedIfEmpty({
|
||||||
refund_deadline: !value.payments?.refund_deadline
|
refund_deadline: !value.payments?.refund_deadline
|
||||||
? undefined
|
? undefined
|
||||||
@ -202,6 +206,7 @@ export function CreatePage({
|
|||||||
)
|
)
|
||||||
? i18n.str`auto refund cannot be after refund deadline`
|
? i18n.str`auto refund cannot be after refund deadline`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
||||||
}),
|
}),
|
||||||
shipping: undefinedIfEmpty({
|
shipping: undefinedIfEmpty({
|
||||||
delivery_date: !value.shipping?.delivery_date
|
delivery_date: !value.shipping?.delivery_date
|
||||||
@ -225,7 +230,7 @@ export function CreatePage({
|
|||||||
amount: order.pricing.order_price,
|
amount: order.pricing.order_price,
|
||||||
summary: order.pricing.summary,
|
summary: order.pricing.summary,
|
||||||
products: productList,
|
products: productList,
|
||||||
extra: value.extra,
|
extra: JSON.stringify(value.extra),
|
||||||
pay_deadline: value.payments.pay_deadline
|
pay_deadline: value.payments.pay_deadline
|
||||||
? {
|
? {
|
||||||
t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000),
|
t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000),
|
||||||
@ -250,9 +255,7 @@ export function CreatePage({
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
wire_fee_amortization: value.payments.wire_fee_amortization as number,
|
|
||||||
max_fee: value.payments.max_fee as string,
|
max_fee: value.payments.max_fee as string,
|
||||||
max_wire_fee: value.payments.max_wire_fee as string,
|
|
||||||
|
|
||||||
delivery_date: value.shipping.delivery_date
|
delivery_date: value.shipping.delivery_date
|
||||||
? { t_s: value.shipping.delivery_date.getTime() / 1000 }
|
? { t_s: value.shipping.delivery_date.getTime() / 1000 }
|
||||||
@ -326,6 +329,8 @@ export function CreatePage({
|
|||||||
const totalAsString = Amounts.stringify(totalPrice.amount);
|
const totalAsString = Amounts.stringify(totalPrice.amount);
|
||||||
const allProducts = productList.concat(inventoryList.map(asProduct));
|
const allProducts = productList.concat(inventoryList.map(asProduct));
|
||||||
|
|
||||||
|
const [newField, setNewField] = useState("")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
valueHandler((v) => {
|
valueHandler((v) => {
|
||||||
return {
|
return {
|
||||||
@ -486,16 +491,61 @@ export function CreatePage({
|
|||||||
name="payments.pay_deadline"
|
name="payments.pay_deadline"
|
||||||
label={i18n.str`Payment deadline`}
|
label={i18n.str`Payment deadline`}
|
||||||
tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`}
|
tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`}
|
||||||
|
side={
|
||||||
|
<span>
|
||||||
|
<button class="button" onClick={() => {
|
||||||
|
valueHandler({
|
||||||
|
...value,
|
||||||
|
payments: {
|
||||||
|
...(value.payments ?? {}),
|
||||||
|
pay_deadline: instance_default.payments?.pay_deadline
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<i18n.Translate>default</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<InputDate
|
<InputDate
|
||||||
name="payments.refund_deadline"
|
name="payments.refund_deadline"
|
||||||
label={i18n.str`Refund deadline`}
|
label={i18n.str`Refund deadline`}
|
||||||
tooltip={i18n.str`Time until which the order can be refunded by the merchant.`}
|
tooltip={i18n.str`Time until which the order can be refunded by the merchant.`}
|
||||||
|
side={
|
||||||
|
<span>
|
||||||
|
<button class="button" onClick={() => {
|
||||||
|
valueHandler({
|
||||||
|
...value,
|
||||||
|
payments: {
|
||||||
|
...(value.payments ?? {}),
|
||||||
|
refund_deadline: instance_default.payments?.refund_deadline
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<i18n.Translate>default</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<InputDate
|
<InputDate
|
||||||
name="payments.wire_transfer_deadline"
|
name="payments.wire_transfer_deadline"
|
||||||
label={i18n.str`Wire transfer deadline`}
|
label={i18n.str`Wire transfer deadline`}
|
||||||
tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`}
|
tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`}
|
||||||
|
side={
|
||||||
|
<span>
|
||||||
|
<button class="button" onClick={() => {
|
||||||
|
valueHandler({
|
||||||
|
...value,
|
||||||
|
payments: {
|
||||||
|
...(value.payments ?? {}),
|
||||||
|
wire_transfer_deadline: instance_default.payments?.wire_transfer_deadline
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<i18n.Translate>default</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<InputDate
|
<InputDate
|
||||||
name="payments.auto_refund_deadline"
|
name="payments.auto_refund_deadline"
|
||||||
@ -505,23 +555,13 @@ export function CreatePage({
|
|||||||
|
|
||||||
<InputCurrency
|
<InputCurrency
|
||||||
name="payments.max_fee"
|
name="payments.max_fee"
|
||||||
label={i18n.str`Maximum deposit fee`}
|
label={i18n.str`Maximum fee`}
|
||||||
tooltip={i18n.str`Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
|
tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
|
||||||
/>
|
/>
|
||||||
<InputCurrency
|
<InputToggle
|
||||||
name="payments.max_wire_fee"
|
|
||||||
label={i18n.str`Maximum wire fee`}
|
|
||||||
tooltip={i18n.str`Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.`}
|
|
||||||
/>
|
|
||||||
<InputNumber
|
|
||||||
name="payments.wire_fee_amortization"
|
|
||||||
label={i18n.str`Wire fee amortization`}
|
|
||||||
tooltip={i18n.str`Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.`}
|
|
||||||
/>
|
|
||||||
<InputBoolean
|
|
||||||
name="payments.createToken"
|
name="payments.createToken"
|
||||||
label={i18n.str`Create token`}
|
label={i18n.str`Create token`}
|
||||||
tooltip={i18n.str`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`}
|
tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`}
|
||||||
/>
|
/>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
name="payments.minimum_age"
|
name="payments.minimum_age"
|
||||||
@ -530,7 +570,7 @@ export function CreatePage({
|
|||||||
help={
|
help={
|
||||||
minAgeByProducts > 0
|
minAgeByProducts > 0
|
||||||
? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
|
? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
|
||||||
: undefined
|
: i18n.str`No product with age restriction in this order`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
@ -542,12 +582,53 @@ export function CreatePage({
|
|||||||
label={i18n.str`Additional information`}
|
label={i18n.str`Additional information`}
|
||||||
tooltip={i18n.str`Custom information to be included in the contract for this order.`}
|
tooltip={i18n.str`Custom information to be included in the contract for this order.`}
|
||||||
>
|
>
|
||||||
<Input
|
{Object.keys(value.extra ?? {}).map((key) => {
|
||||||
name="extra"
|
|
||||||
inputType="multiline"
|
return <Input
|
||||||
label={`Value`}
|
name={`extra.${key}`}
|
||||||
tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
|
inputType="multiline"
|
||||||
/>
|
label={key}
|
||||||
|
tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
|
||||||
|
side={
|
||||||
|
<button class="button" onClick={(e) => {
|
||||||
|
if (value.extra && value.extra[key] !== undefined) {
|
||||||
|
console.log(value.extra)
|
||||||
|
delete value.extra[key]
|
||||||
|
}
|
||||||
|
valueHandler({
|
||||||
|
...value,
|
||||||
|
})
|
||||||
|
}}>remove</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
})}
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<div class="field-label is-normal">
|
||||||
|
<label class="label">
|
||||||
|
<i18n.Translate>Custom field name</i18n.Translate>
|
||||||
|
<span class="icon has-tooltip-right" data-tooltip={"new extra field"}>
|
||||||
|
<i class="mdi mdi-information" />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field-body is-flex-grow-3">
|
||||||
|
<div class="field">
|
||||||
|
<p class="control">
|
||||||
|
<input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="button" onClick={(e) => {
|
||||||
|
setNewField("")
|
||||||
|
valueHandler({
|
||||||
|
...value,
|
||||||
|
extra: {
|
||||||
|
...(value.extra ?? {}),
|
||||||
|
[newField]: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}>add</button>
|
||||||
|
</div>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
}
|
}
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
@ -38,7 +38,7 @@ export type Entity = {
|
|||||||
};
|
};
|
||||||
interface Props {
|
interface Props {
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
onConfirm: () => void;
|
onConfirm: (id: string) => void;
|
||||||
onUnauthorized: () => VNode;
|
onUnauthorized: () => VNode;
|
||||||
onNotFound: () => VNode;
|
onNotFound: () => VNode;
|
||||||
onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
|
onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
|
||||||
@ -95,7 +95,9 @@ export default function OrderCreate({
|
|||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => {
|
onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => {
|
||||||
createOrder(request)
|
createOrder(request)
|
||||||
.then(onConfirm)
|
.then((r) => {
|
||||||
|
return onConfirm(r.data.order_id)
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setNotif({
|
setNotif({
|
||||||
message: "could not create order",
|
message: "could not create order",
|
||||||
|
@ -50,13 +50,11 @@ const defaultContractTerm = {
|
|||||||
auditors: [],
|
auditors: [],
|
||||||
exchanges: [],
|
exchanges: [],
|
||||||
max_fee: "TESTKUDOS:1",
|
max_fee: "TESTKUDOS:1",
|
||||||
max_wire_fee: "TESTKUDOS:1",
|
|
||||||
merchant: {} as any,
|
merchant: {} as any,
|
||||||
merchant_base_url: "http://merchant.url/",
|
merchant_base_url: "http://merchant.url/",
|
||||||
order_id: "2021.165-03GDFC26Y1NNG",
|
order_id: "2021.165-03GDFC26Y1NNG",
|
||||||
products: [],
|
products: [],
|
||||||
summary: "text summary",
|
summary: "text summary",
|
||||||
wire_fee_amortization: 1,
|
|
||||||
wire_transfer_deadline: {
|
wire_transfer_deadline: {
|
||||||
t_s: "never",
|
t_s: "never",
|
||||||
},
|
},
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
|
import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { format, formatDistance } from "date-fns";
|
import { format, formatDistance } from "date-fns";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
@ -38,6 +38,7 @@ import { MerchantBackend } from "../../../../declaration.js";
|
|||||||
import { mergeRefunds } from "../../../../utils/amount.js";
|
import { mergeRefunds } from "../../../../utils/amount.js";
|
||||||
import { RefundModal } from "../list/Table.js";
|
import { RefundModal } from "../list/Table.js";
|
||||||
import { Event, Timeline } from "./Timeline.js";
|
import { Event, Timeline } from "./Timeline.js";
|
||||||
|
import { dateFormatForSettings, datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
|
type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
|
||||||
type CT = MerchantBackend.ContractTerms;
|
type CT = MerchantBackend.ContractTerms;
|
||||||
@ -87,18 +88,6 @@ function ContractTerms({ value }: { value: CT }) {
|
|||||||
label={i18n.str`Max fee`}
|
label={i18n.str`Max fee`}
|
||||||
tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`}
|
tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`}
|
||||||
/>
|
/>
|
||||||
<Input<CT>
|
|
||||||
readonly
|
|
||||||
name="max_wire_fee"
|
|
||||||
label={i18n.str`Max wire fee`}
|
|
||||||
tooltip={i18n.str`maximum wire fee accepted by the merchant`}
|
|
||||||
/>
|
|
||||||
<Input<CT>
|
|
||||||
readonly
|
|
||||||
name="wire_fee_amortization"
|
|
||||||
label={i18n.str`Wire fee amortization`}
|
|
||||||
tooltip={i18n.str`over how many customer transactions does the merchant expect to amortize wire fees on average`}
|
|
||||||
/>
|
|
||||||
<InputDate<CT>
|
<InputDate<CT>
|
||||||
readonly
|
readonly
|
||||||
name="timestamp"
|
name="timestamp"
|
||||||
@ -204,6 +193,7 @@ function ClaimedPage({
|
|||||||
|
|
||||||
const [value, valueHandler] = useState<Partial<Claimed>>(order);
|
const [value, valueHandler] = useState<Partial<Claimed>>(order);
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings] = useSettings()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -249,7 +239,7 @@ function ClaimedPage({
|
|||||||
</b>{" "}
|
</b>{" "}
|
||||||
{format(
|
{format(
|
||||||
new Date(order.contract_terms.timestamp.t_s * 1000),
|
new Date(order.contract_terms.timestamp.t_s * 1000),
|
||||||
"yyyy-MM-dd HH:mm:ss",
|
datetimeFormatForSettings(settings)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -427,9 +417,10 @@ function PaidPage({
|
|||||||
|
|
||||||
const [value, valueHandler] = useState<Partial<Paid>>(order);
|
const [value, valueHandler] = useState<Partial<Paid>>(order);
|
||||||
const { url } = useBackendContext();
|
const { url } = useBackendContext();
|
||||||
const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part
|
const refundurl = stringifyRefundUri({
|
||||||
const proto = url.startsWith("http://") ? "taler+http" : "taler";
|
merchantBaseUrl: url,
|
||||||
const refundurl = `${proto}://refund/${refundHost}/${order.contract_terms.order_id}/`;
|
orderId: order.contract_terms.order_id
|
||||||
|
})
|
||||||
const refundable =
|
const refundable =
|
||||||
new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000;
|
new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000;
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
@ -618,6 +609,7 @@ function UnpaidPage({
|
|||||||
}) {
|
}) {
|
||||||
const [value, valueHandler] = useState<Partial<Unpaid>>(order);
|
const [value, valueHandler] = useState<Partial<Unpaid>>(order);
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings] = useSettings()
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<section class="hero is-hero-bar">
|
<section class="hero is-hero-bar">
|
||||||
@ -666,7 +658,7 @@ function UnpaidPage({
|
|||||||
? "never"
|
? "never"
|
||||||
: format(
|
: format(
|
||||||
new Date(order.creation_time.t_s * 1000),
|
new Date(order.creation_time.t_s * 1000),
|
||||||
"yyyy-MM-dd HH:mm:ss",
|
datetimeFormatForSettings(settings)
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { h } from "preact";
|
import { h } from "preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
events: Event[];
|
events: Event[];
|
||||||
@ -30,7 +31,7 @@ export function Timeline({ events: e }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
events.sort((a, b) => a.when.getTime() - b.when.getTime());
|
events.sort((a, b) => a.when.getTime() - b.when.getTime());
|
||||||
|
const [settings] = useSettings();
|
||||||
const [state, setState] = useState(events);
|
const [state, setState] = useState(events);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handle = setTimeout(() => {
|
const handle = setTimeout(() => {
|
||||||
@ -104,7 +105,7 @@ export function Timeline({ events: e }: Props) {
|
|||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
{e.description !== "now" && <p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p>}
|
{e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>}
|
||||||
<p>{e.description}</p>
|
<p>{e.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,19 +26,24 @@ import { useState } from "preact/hooks";
|
|||||||
import { DatePicker } from "../../../../components/picker/DatePicker.js";
|
import { DatePicker } from "../../../../components/picker/DatePicker.js";
|
||||||
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||||
import { CardTable } from "./Table.js";
|
import { CardTable } from "./Table.js";
|
||||||
|
import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
|
||||||
export interface ListPageProps {
|
export interface ListPageProps {
|
||||||
errorOrderId: string | undefined;
|
errorOrderId: string | undefined;
|
||||||
|
|
||||||
onShowAll: () => void;
|
onShowAll: () => void;
|
||||||
|
onShowNotPaid: () => void;
|
||||||
onShowPaid: () => void;
|
onShowPaid: () => void;
|
||||||
onShowRefunded: () => void;
|
onShowRefunded: () => void;
|
||||||
onShowNotWired: () => void;
|
onShowNotWired: () => void;
|
||||||
|
onShowWired: () => void;
|
||||||
onCopyURL: (id: string) => void;
|
onCopyURL: (id: string) => void;
|
||||||
isAllActive: string;
|
isAllActive: string;
|
||||||
isPaidActive: string;
|
isPaidActive: string;
|
||||||
|
isNotPaidActive: string;
|
||||||
isRefundedActive: string;
|
isRefundedActive: string;
|
||||||
isNotWiredActive: string;
|
isNotWiredActive: string;
|
||||||
|
isWiredActive: string;
|
||||||
|
|
||||||
jumpToDate?: Date;
|
jumpToDate?: Date;
|
||||||
onSelectDate: (date?: Date) => void;
|
onSelectDate: (date?: Date) => void;
|
||||||
@ -66,18 +71,23 @@ export function ListPage({
|
|||||||
onCopyURL,
|
onCopyURL,
|
||||||
onShowAll,
|
onShowAll,
|
||||||
onShowPaid,
|
onShowPaid,
|
||||||
|
onShowNotPaid,
|
||||||
onShowRefunded,
|
onShowRefunded,
|
||||||
onShowNotWired,
|
onShowNotWired,
|
||||||
|
onShowWired,
|
||||||
onSelectDate,
|
onSelectDate,
|
||||||
isPaidActive,
|
isPaidActive,
|
||||||
isRefundedActive,
|
isRefundedActive,
|
||||||
isNotWiredActive,
|
isNotWiredActive,
|
||||||
onCreate,
|
onCreate,
|
||||||
|
isNotPaidActive,
|
||||||
|
isWiredActive,
|
||||||
}: ListPageProps): VNode {
|
}: ListPageProps): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const dateTooltip = i18n.str`select date to show nearby orders`;
|
const dateTooltip = i18n.str`select date to show nearby orders`;
|
||||||
const [pickDate, setPickDate] = useState(false);
|
const [pickDate, setPickDate] = useState(false);
|
||||||
const [orderId, setOrderId] = useState<string>("");
|
const [orderId, setOrderId] = useState<string>("");
|
||||||
|
const [settings] = useSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section class="section is-main-section">
|
<section class="section is-main-section">
|
||||||
@ -116,13 +126,13 @@ export function ListPage({
|
|||||||
<div class="column is-two-thirds">
|
<div class="column is-two-thirds">
|
||||||
<div class="tabs" style={{ overflow: "inherit" }}>
|
<div class="tabs" style={{ overflow: "inherit" }}>
|
||||||
<ul>
|
<ul>
|
||||||
<li class={isAllActive}>
|
<li class={isNotPaidActive}>
|
||||||
<div
|
<div
|
||||||
class="has-tooltip-right"
|
class="has-tooltip-right"
|
||||||
data-tooltip={i18n.str`remove all filters`}
|
data-tooltip={i18n.str`only show paid orders`}
|
||||||
>
|
>
|
||||||
<a onClick={onShowAll}>
|
<a onClick={onShowNotPaid}>
|
||||||
<i18n.Translate>All</i18n.Translate>
|
<i18n.Translate>New</i18n.Translate>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -156,6 +166,26 @@ export function ListPage({
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<li class={isWiredActive}>
|
||||||
|
<div
|
||||||
|
class="has-tooltip-left"
|
||||||
|
data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
|
||||||
|
>
|
||||||
|
<a onClick={onShowWired}>
|
||||||
|
<i18n.Translate>Completed</i18n.Translate>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class={isAllActive}>
|
||||||
|
<div
|
||||||
|
class="has-tooltip-right"
|
||||||
|
data-tooltip={i18n.str`remove all filters`}
|
||||||
|
>
|
||||||
|
<a onClick={onShowAll}>
|
||||||
|
<i18n.Translate>All</i18n.Translate>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -180,8 +210,8 @@ export function ListPage({
|
|||||||
class="input"
|
class="input"
|
||||||
type="text"
|
type="text"
|
||||||
readonly
|
readonly
|
||||||
value={!jumpToDate ? "" : format(jumpToDate, "yyyy/MM/dd")}
|
value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))}
|
||||||
placeholder={i18n.str`date (YYYY/MM/DD)`}
|
placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPickDate(true);
|
setPickDate(true);
|
||||||
}}
|
}}
|
||||||
|
@ -36,6 +36,7 @@ import { ConfirmModal } from "../../../../components/modal/index.js";
|
|||||||
import { useConfigContext } from "../../../../context/config.js";
|
import { useConfigContext } from "../../../../context/config.js";
|
||||||
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||||
import { mergeRefunds } from "../../../../utils/amount.js";
|
import { mergeRefunds } from "../../../../utils/amount.js";
|
||||||
|
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
|
type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -136,6 +137,7 @@ function Table({
|
|||||||
hasMoreBefore,
|
hasMoreBefore,
|
||||||
}: TableProps): VNode {
|
}: TableProps): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings] = useSettings();
|
||||||
return (
|
return (
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
{onLoadMoreBefore && (
|
{onLoadMoreBefore && (
|
||||||
@ -173,9 +175,9 @@ function Table({
|
|||||||
{i.timestamp.t_s === "never"
|
{i.timestamp.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: format(
|
: format(
|
||||||
new Date(i.timestamp.t_s * 1000),
|
new Date(i.timestamp.t_s * 1000),
|
||||||
"yyyy/MM/dd HH:mm:ss",
|
datetimeFormatForSettings(settings),
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
onClick={(): void => onSelect(i)}
|
onClick={(): void => onSelect(i)}
|
||||||
@ -260,6 +262,7 @@ export function RefundModal({
|
|||||||
}: RefundModalProps): VNode {
|
}: RefundModalProps): VNode {
|
||||||
type State = { mainReason?: string; description?: string; refund?: string };
|
type State = { mainReason?: string; description?: string; refund?: string };
|
||||||
const [form, setValue] = useState<State>({});
|
const [form, setValue] = useState<State>({});
|
||||||
|
const [settings] = useSettings();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
// const [errors, setErrors] = useState<FormErrors<State>>({});
|
// const [errors, setErrors] = useState<FormErrors<State>>({});
|
||||||
|
|
||||||
@ -281,8 +284,8 @@ export function RefundModal({
|
|||||||
const totalRefundable = !orderPrice
|
const totalRefundable = !orderPrice
|
||||||
? Amounts.zeroOfCurrency(totalRefunded.currency)
|
? Amounts.zeroOfCurrency(totalRefunded.currency)
|
||||||
: refunds.length
|
: refunds.length
|
||||||
? Amounts.sub(orderPrice, totalRefunded).amount
|
? Amounts.sub(orderPrice, totalRefunded).amount
|
||||||
: orderPrice;
|
: orderPrice;
|
||||||
|
|
||||||
const isRefundable = Amounts.isNonZero(totalRefundable);
|
const isRefundable = Amounts.isNonZero(totalRefundable);
|
||||||
const duplicatedText = i18n.str`duplicated`;
|
const duplicatedText = i18n.str`duplicated`;
|
||||||
@ -296,10 +299,10 @@ export function RefundModal({
|
|||||||
refund: !form.refund
|
refund: !form.refund
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: !Amounts.parse(form.refund)
|
: !Amounts.parse(form.refund)
|
||||||
? i18n.str`invalid format`
|
? i18n.str`invalid format`
|
||||||
: Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
|
: Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
|
||||||
? i18n.str`this value exceed the refundable amount`
|
? i18n.str`this value exceed the refundable amount`
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
const hasErrors = Object.keys(errors).some(
|
const hasErrors = Object.keys(errors).some(
|
||||||
(k) => (errors as any)[k] !== undefined,
|
(k) => (errors as any)[k] !== undefined,
|
||||||
@ -361,9 +364,9 @@ export function RefundModal({
|
|||||||
{r.timestamp.t_s === "never"
|
{r.timestamp.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: format(
|
: format(
|
||||||
new Date(r.timestamp.t_s * 1000),
|
new Date(r.timestamp.t_s * 1000),
|
||||||
"yyyy-MM-dd HH:mm:ss",
|
datetimeFormatForSettings(settings),
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{r.amount}</td>
|
<td>{r.amount}</td>
|
||||||
<td>{r.reason}</td>
|
<td>{r.reason}</td>
|
||||||
|
@ -55,7 +55,7 @@ export default function OrderList({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onNotFound,
|
onNotFound,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const [filter, setFilter] = useState<InstanceOrderFilter>({});
|
const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" });
|
||||||
const [orderToBeRefunded, setOrderToBeRefunded] = useState<
|
const [orderToBeRefunded, setOrderToBeRefunded] = useState<
|
||||||
MerchantBackend.Orders.OrderHistoryEntry | undefined
|
MerchantBackend.Orders.OrderHistoryEntry | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@ -88,13 +88,15 @@ export default function OrderList({
|
|||||||
return onLoadError(result);
|
return onLoadError(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPaidActive = filter.paid === "yes" ? "is-active" : "";
|
const isNotPaidActive = filter.paid === "no" ? "is-active" : "";
|
||||||
|
const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : "";
|
||||||
const isRefundedActive = filter.refunded === "yes" ? "is-active" : "";
|
const isRefundedActive = filter.refunded === "yes" ? "is-active" : "";
|
||||||
const isNotWiredActive = filter.wired === "no" ? "is-active" : "";
|
const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : "";
|
||||||
|
const isWiredActive = filter.wired === "yes" ? "is-active" : "";
|
||||||
const isAllActive =
|
const isAllActive =
|
||||||
filter.paid === undefined &&
|
filter.paid === undefined &&
|
||||||
filter.refunded === undefined &&
|
filter.refunded === undefined &&
|
||||||
filter.wired === undefined
|
filter.wired === undefined
|
||||||
? "is-active"
|
? "is-active"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
@ -127,7 +129,9 @@ export default function OrderList({
|
|||||||
errorOrderId={errorOrderId}
|
errorOrderId={errorOrderId}
|
||||||
isAllActive={isAllActive}
|
isAllActive={isAllActive}
|
||||||
isNotWiredActive={isNotWiredActive}
|
isNotWiredActive={isNotWiredActive}
|
||||||
|
isWiredActive={isWiredActive}
|
||||||
isPaidActive={isPaidActive}
|
isPaidActive={isPaidActive}
|
||||||
|
isNotPaidActive={isNotPaidActive}
|
||||||
isRefundedActive={isRefundedActive}
|
isRefundedActive={isRefundedActive}
|
||||||
jumpToDate={filter.date}
|
jumpToDate={filter.date}
|
||||||
onCopyURL={(id) =>
|
onCopyURL={(id) =>
|
||||||
@ -137,9 +141,11 @@ export default function OrderList({
|
|||||||
onSearchOrderById={testIfOrderExistAndSelect}
|
onSearchOrderById={testIfOrderExistAndSelect}
|
||||||
onSelectDate={setNewDate}
|
onSelectDate={setNewDate}
|
||||||
onShowAll={() => setFilter({})}
|
onShowAll={() => setFilter({})}
|
||||||
|
onShowNotPaid={() => setFilter({ paid: "no" })}
|
||||||
onShowPaid={() => setFilter({ paid: "yes" })}
|
onShowPaid={() => setFilter({ paid: "yes" })}
|
||||||
onShowRefunded={() => setFilter({ refunded: "yes" })}
|
onShowRefunded={() => setFilter({ refunded: "yes" })}
|
||||||
onShowNotWired={() => setFilter({ wired: "no" })}
|
onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })}
|
||||||
|
onShowWired={() => setFilter({ wired: "yes" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{orderToBeRefunded && (
|
{orderToBeRefunded && (
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
||||||
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
||||||
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||||
|
import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Products.ProductDetail & WithId;
|
type Entity = MerchantBackend.Products.ProductDetail & WithId;
|
||||||
|
|
||||||
@ -122,6 +123,7 @@ function Table({
|
|||||||
onDelete,
|
onDelete,
|
||||||
}: TableProps): VNode {
|
}: TableProps): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings] = useSettings();
|
||||||
return (
|
return (
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
||||||
@ -134,7 +136,7 @@ function Table({
|
|||||||
<i18n.Translate>Description</i18n.Translate>
|
<i18n.Translate>Description</i18n.Translate>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<i18n.Translate>Sell</i18n.Translate>
|
<i18n.Translate>Price per unit</i18n.Translate>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<i18n.Translate>Taxes</i18n.Translate>
|
<i18n.Translate>Taxes</i18n.Translate>
|
||||||
@ -156,10 +158,10 @@ function Table({
|
|||||||
const restStockInfo = !i.next_restock
|
const restStockInfo = !i.next_restock
|
||||||
? ""
|
? ""
|
||||||
: i.next_restock.t_s === "never"
|
: i.next_restock.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: `restock at ${format(
|
: `restock at ${format(
|
||||||
new Date(i.next_restock.t_s * 1000),
|
new Date(i.next_restock.t_s * 1000),
|
||||||
"yyyy/MM/dd",
|
dateFormatForSettings(settings),
|
||||||
)}`;
|
)}`;
|
||||||
let stockInfo: ComponentChildren = "";
|
let stockInfo: ComponentChildren = "";
|
||||||
if (i.total_stock < 0) {
|
if (i.total_stock < 0) {
|
||||||
@ -332,26 +334,35 @@ function FastProductWithInfiniteStockUpdateForm({
|
|||||||
/>
|
/>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
<div class="buttons is-right mt-5">
|
<div class="buttons is-expanded">
|
||||||
<button class="button" onClick={onCancel}>
|
|
||||||
<i18n.Translate>Cancel</i18n.Translate>
|
<div class="buttons mt-5">
|
||||||
</button>
|
|
||||||
<span
|
<button class="button " onClick={onCancel}>
|
||||||
class="has-tooltip-left"
|
<i18n.Translate>Clone</i18n.Translate>
|
||||||
data-tooltip={i18n.str`update product with new price`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="button is-info"
|
|
||||||
onClick={() =>
|
|
||||||
onUpdate({
|
|
||||||
...product,
|
|
||||||
price: value.price,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Confirm</i18n.Translate>
|
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</div>
|
||||||
|
<div class="buttons is-right mt-5">
|
||||||
|
<button class="button" onClick={onCancel}>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
class="has-tooltip-left"
|
||||||
|
data-tooltip={i18n.str`update product with new price`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button is-info"
|
||||||
|
onClick={() =>
|
||||||
|
onUpdate({
|
||||||
|
...product,
|
||||||
|
price: value.price,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Confirm update</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@ -374,9 +385,8 @@ function FastProductWithManagedStockUpdateForm({
|
|||||||
const errors: FormErrors<FastProductUpdate> = {
|
const errors: FormErrors<FastProductUpdate> = {
|
||||||
lost:
|
lost:
|
||||||
currentStock + value.incoming < value.lost
|
currentStock + value.incoming < value.lost
|
||||||
? `lost cannot be greater that current + incoming (max ${
|
? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming
|
||||||
currentStock + value.incoming
|
})`
|
||||||
})`
|
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
import { Notification } from "../../../../utils/types.js";
|
import { Notification } from "../../../../utils/types.js";
|
||||||
import { CardTable } from "./Table.js";
|
import { CardTable } from "./Table.js";
|
||||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
|
import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onUnauthorized: () => VNode;
|
onUnauthorized: () => VNode;
|
||||||
@ -53,6 +54,8 @@ export default function ProductList({
|
|||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const result = useInstanceProducts();
|
const result = useInstanceProducts();
|
||||||
const { deleteProduct, updateProduct } = useProductAPI();
|
const { deleteProduct, updateProduct } = useProductAPI();
|
||||||
|
const [deleting, setDeleting] =
|
||||||
|
useState<MerchantBackend.Products.ProductDetail & WithId | null>(null);
|
||||||
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||||
|
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
@ -97,22 +100,43 @@ export default function ProductList({
|
|||||||
}
|
}
|
||||||
onSelect={(product) => onSelect(product.id)}
|
onSelect={(product) => onSelect(product.id)}
|
||||||
onDelete={(prod: MerchantBackend.Products.ProductDetail & WithId) =>
|
onDelete={(prod: MerchantBackend.Products.ProductDetail & WithId) =>
|
||||||
deleteProduct(prod.id)
|
setDeleting(prod)
|
||||||
.then(() =>
|
|
||||||
setNotif({
|
|
||||||
message: i18n.str`product delete successfully`,
|
|
||||||
type: "SUCCESS",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.catch((error) =>
|
|
||||||
setNotif({
|
|
||||||
message: i18n.str`could not delete the product`,
|
|
||||||
type: "ERROR",
|
|
||||||
description: error.message,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{deleting && (
|
||||||
|
<ConfirmModal
|
||||||
|
label={`Delete product`}
|
||||||
|
description={`Delete the product "${deleting.description}"`}
|
||||||
|
danger
|
||||||
|
active
|
||||||
|
onCancel={() => setDeleting(null)}
|
||||||
|
onConfirm={async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await deleteProduct(deleting.id);
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`,
|
||||||
|
type: "SUCCESS",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`Failed to delete product`,
|
||||||
|
type: "ERROR",
|
||||||
|
description: error instanceof Error ? error.message : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setDeleting(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
If you delete the product named <b>"{deleting.description}"</b> (ID:{" "}
|
||||||
|
<b>{deleting.id}</b>), the stock and related information will be lost
|
||||||
|
</p>
|
||||||
|
<p class="warning">
|
||||||
|
Deleting an product <b>cannot be undone</b>.
|
||||||
|
</p>
|
||||||
|
</ConfirmModal>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ import {
|
|||||||
URL_REGEX,
|
URL_REGEX,
|
||||||
} from "../../../../utils/constants.js";
|
} from "../../../../utils/constants.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Tips.ReserveCreateRequest;
|
type Entity = MerchantBackend.Rewards.ReserveCreateRequest;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate: (d: Entity) => Promise<void>;
|
onCreate: (d: Entity) => Promise<void>;
|
||||||
@ -80,15 +80,15 @@ function ViewStep({
|
|||||||
initial_balance: !reserve.initial_balance
|
initial_balance: !reserve.initial_balance
|
||||||
? "cannot be empty"
|
? "cannot be empty"
|
||||||
: !(parseInt(reserve.initial_balance.split(":")[1], 10) > 0)
|
: !(parseInt(reserve.initial_balance.split(":")[1], 10) > 0)
|
||||||
? i18n.str`it should be greater than 0`
|
? i18n.str`it should be greater than 0`
|
||||||
: undefined,
|
: undefined,
|
||||||
exchange_url: !reserve.exchange_url
|
exchange_url: !reserve.exchange_url
|
||||||
? i18n.str`cannot be empty`
|
? i18n.str`cannot be empty`
|
||||||
: !URL_REGEX.test(reserve.exchange_url)
|
: !URL_REGEX.test(reserve.exchange_url)
|
||||||
? i18n.str`must be a valid URL`
|
? i18n.str`must be a valid URL`
|
||||||
: !exchangeQueryError
|
: !exchangeQueryError
|
||||||
? undefined
|
? undefined
|
||||||
: exchangeQueryError,
|
: exchangeQueryError,
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasErrors = Object.keys(errors).some(
|
const hasErrors = Object.keys(errors).some(
|
||||||
|
@ -22,8 +22,8 @@ import { CreatedSuccessfully as Template } from "../../../../components/notifica
|
|||||||
import { MerchantBackend, WireAccount } from "../../../../declaration.js";
|
import { MerchantBackend, WireAccount } from "../../../../declaration.js";
|
||||||
|
|
||||||
type Entity = {
|
type Entity = {
|
||||||
request: MerchantBackend.Tips.ReserveCreateRequest;
|
request: MerchantBackend.Rewards.ReserveCreateRequest;
|
||||||
response: MerchantBackend.Tips.ReserveCreateConfirmation;
|
response: MerchantBackend.Rewards.ReserveCreateConfirmation;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -98,15 +98,15 @@ export function ShowAccountsOfReserveAsQRWithLink({
|
|||||||
const accountsInfo = !accounts
|
const accountsInfo = !accounts
|
||||||
? []
|
? []
|
||||||
: accounts
|
: accounts
|
||||||
.map((acc) => {
|
.map((acc) => {
|
||||||
const p = parsePaytoUri(acc.payto_uri);
|
const p = parsePaytoUri(acc.payto_uri);
|
||||||
if (p) {
|
if (p) {
|
||||||
p.params["message"] = message;
|
p.params["message"] = message;
|
||||||
p.params["amount"] = amount;
|
p.params["amount"] = amount;
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
})
|
})
|
||||||
.filter(isNotUndefined);
|
.filter(isNotUndefined);
|
||||||
|
|
||||||
const links = accountsInfo.map((a) => stringifyPaytoUri(a));
|
const links = accountsInfo.map((a) => stringifyPaytoUri(a));
|
||||||
|
|
||||||
|
@ -39,9 +39,9 @@ export default function CreateReserve({ onBack, onConfirm }: Props): VNode {
|
|||||||
|
|
||||||
const [createdOk, setCreatedOk] = useState<
|
const [createdOk, setCreatedOk] = useState<
|
||||||
| {
|
| {
|
||||||
request: MerchantBackend.Tips.ReserveCreateRequest;
|
request: MerchantBackend.Rewards.ReserveCreateRequest;
|
||||||
response: MerchantBackend.Tips.ReserveCreateConfirmation;
|
response: MerchantBackend.Rewards.ReserveCreateConfirmation;
|
||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ export default function CreateReserve({ onBack, onConfirm }: Props): VNode {
|
|||||||
<NotificationCard notification={notif} />
|
<NotificationCard notification={notif} />
|
||||||
<CreatePage
|
<CreatePage
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
onCreate={(request: MerchantBackend.Tips.ReserveCreateRequest) => {
|
onCreate={(request: MerchantBackend.Rewards.ReserveCreateRequest) => {
|
||||||
return createReserve(request)
|
return createReserve(request)
|
||||||
.then((r) => setCreatedOk({ request, response: r.data }))
|
.then((r) => setCreatedOk({ request, response: r.data }))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -36,11 +36,12 @@ import { InputDate } from "../../../../components/form/InputDate.js";
|
|||||||
import { TextField } from "../../../../components/form/TextField.js";
|
import { TextField } from "../../../../components/form/TextField.js";
|
||||||
import { SimpleModal } from "../../../../components/modal/index.js";
|
import { SimpleModal } from "../../../../components/modal/index.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
import { useTipDetails } from "../../../../hooks/reserves.js";
|
import { useRewardDetails } from "../../../../hooks/reserves.js";
|
||||||
import { TipInfo } from "./TipInfo.js";
|
import { RewardInfo } from "./RewardInfo.js";
|
||||||
import { ShowAccountsOfReserveAsQRWithLink } from "../create/CreatedSuccessfully.js";
|
import { ShowAccountsOfReserveAsQRWithLink } from "../create/CreatedSuccessfully.js";
|
||||||
|
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Tips.ReserveDetail;
|
type Entity = MerchantBackend.Rewards.ReserveDetail;
|
||||||
type CT = MerchantBackend.ContractTerms;
|
type CT = MerchantBackend.ContractTerms;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -116,14 +117,14 @@ export function DetailPage({ id, selected, onBack }: Props): VNode {
|
|||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="mdi mdi-cash-register" />
|
<i class="mdi mdi-cash-register" />
|
||||||
</span>
|
</span>
|
||||||
<i18n.Translate>Tips</i18n.Translate>
|
<i18n.Translate>Rewards</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="b-table has-pagination">
|
<div class="b-table has-pagination">
|
||||||
<div class="table-wrapper has-mobile-cards">
|
<div class="table-wrapper has-mobile-cards">
|
||||||
{selected.tips && selected.tips.length > 0 ? (
|
{selected.rewards && selected.rewards.length > 0 ? (
|
||||||
<Table tips={selected.tips} />
|
<Table rewards={selected.rewards} />
|
||||||
) : (
|
) : (
|
||||||
<EmptyTable />
|
<EmptyTable />
|
||||||
)}
|
)}
|
||||||
@ -163,7 +164,7 @@ function EmptyTable(): VNode {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<i18n.Translate>
|
<i18n.Translate>
|
||||||
No tips has been authorized from this reserve
|
No reward has been authorized from this reserve
|
||||||
</i18n.Translate>
|
</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -171,10 +172,10 @@ function EmptyTable(): VNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TableProps {
|
interface TableProps {
|
||||||
tips: MerchantBackend.Tips.TipStatusEntry[];
|
rewards: MerchantBackend.Rewards.RewardStatusEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function Table({ tips }: TableProps): VNode {
|
function Table({ rewards }: TableProps): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
return (
|
return (
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
@ -196,8 +197,8 @@ function Table({ tips }: TableProps): VNode {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{tips.map((t, i) => {
|
{rewards.map((t, i) => {
|
||||||
return <TipRow id={t.tip_id} key={i} entry={t} />;
|
return <RewardRow id={t.reward_id} key={i} entry={t} />;
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -205,15 +206,16 @@ function Table({ tips }: TableProps): VNode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TipRow({
|
function RewardRow({
|
||||||
id,
|
id,
|
||||||
entry,
|
entry,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
entry: MerchantBackend.Tips.TipStatusEntry;
|
entry: MerchantBackend.Rewards.RewardStatusEntry;
|
||||||
}) {
|
}) {
|
||||||
const [selected, setSelected] = useState(false);
|
const [selected, setSelected] = useState(false);
|
||||||
const result = useTipDetails(id);
|
const result = useRewardDetails(id);
|
||||||
|
const [settings] = useSettings();
|
||||||
if (result.loading) {
|
if (result.loading) {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
@ -242,11 +244,11 @@ function TipRow({
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
{selected && (
|
{selected && (
|
||||||
<SimpleModal
|
<SimpleModal
|
||||||
description="tip"
|
description="reward"
|
||||||
active
|
active
|
||||||
onCancel={() => setSelected(false)}
|
onCancel={() => setSelected(false)}
|
||||||
>
|
>
|
||||||
<TipInfo id={id} amount={info.total_authorized} entity={info} />
|
<RewardInfo id={id} amount={info.total_authorized} entity={info} />
|
||||||
</SimpleModal>
|
</SimpleModal>
|
||||||
)}
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
@ -256,7 +258,7 @@ function TipRow({
|
|||||||
<td onClick={onSelect}>
|
<td onClick={onSelect}>
|
||||||
{info.expiration.t_s === "never"
|
{info.expiration.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: format(info.expiration.t_s * 1000, "yyyy/MM/dd HH:mm:ss")}
|
: format(info.expiration.t_s * 1000, datetimeFormatForSettings(settings))}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -92,7 +92,7 @@ export const NotYetFunded = createExample(TestedComponent, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const FundedWithEmptyTips = createExample(TestedComponent, {
|
export const FundedWithEmptyRewards = createExample(TestedComponent, {
|
||||||
id: "THISISTHERESERVEID",
|
id: "THISISTHERESERVEID",
|
||||||
selected: {
|
selected: {
|
||||||
active: true,
|
active: true,
|
||||||
@ -115,10 +115,10 @@ export const FundedWithEmptyTips = createExample(TestedComponent, {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
exchange_url: "http://exchange.taler/",
|
exchange_url: "http://exchange.taler/",
|
||||||
tips: [
|
rewards: [
|
||||||
{
|
{
|
||||||
reason: "asdasd",
|
reason: "asdasd",
|
||||||
tip_id: "123",
|
reward_id: "123",
|
||||||
total_amount: "TESTKUDOS:1",
|
total_amount: "TESTKUDOS:1",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -17,8 +17,10 @@ import { format } from "date-fns";
|
|||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useBackendContext } from "../../../../context/backend.js";
|
import { useBackendContext } from "../../../../context/backend.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
import { stringifyRewardUri } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Tips.TipDetails;
|
type Entity = MerchantBackend.Rewards.RewardDetails;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@ -26,11 +28,10 @@ interface Props {
|
|||||||
amount: string;
|
amount: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TipInfo({ id, amount, entity }: Props): VNode {
|
export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode {
|
||||||
const { url } = useBackendContext();
|
const { url: merchantBaseUrl } = useBackendContext();
|
||||||
const tipHost = url.replace(/.*:\/\//, ""); // remove protocol part
|
const [settings] = useSettings();
|
||||||
const proto = url.startsWith("http://") ? "taler+http" : "taler";
|
const rewardURL = stringifyRewardUri({ merchantBaseUrl, merchantRewardId })
|
||||||
const tipURL = `${proto}://tip/${tipHost}/${id}`;
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
@ -52,8 +53,8 @@ export function TipInfo({ id, amount, entity }: Props): VNode {
|
|||||||
<div class="field-body is-flex-grow-3">
|
<div class="field-body is-flex-grow-3">
|
||||||
<div class="field" style={{ overflowWrap: "anywhere" }}>
|
<div class="field" style={{ overflowWrap: "anywhere" }}>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<a target="_blank" rel="noreferrer" href={tipURL}>
|
<a target="_blank" rel="noreferrer" href={rewardURL}>
|
||||||
{tipURL}
|
{rewardURL}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -73,9 +74,9 @@ export function TipInfo({ id, amount, entity }: Props): VNode {
|
|||||||
!entity.expiration || entity.expiration.t_s === "never"
|
!entity.expiration || entity.expiration.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: format(
|
: format(
|
||||||
entity.expiration.t_s * 1000,
|
entity.expiration.t_s * 1000,
|
||||||
"yyyy/MM/dd HH:mm:ss",
|
datetimeFormatForSettings(settings),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
@ -34,32 +34,32 @@ import {
|
|||||||
ContinueModal,
|
ContinueModal,
|
||||||
} from "../../../../components/modal/index.js";
|
} from "../../../../components/modal/index.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
import { AuthorizeTipSchema } from "../../../../schemas/index.js";
|
import { AuthorizeRewardSchema } from "../../../../schemas/index.js";
|
||||||
import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
|
import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
|
||||||
|
|
||||||
interface AuthorizeTipModalProps {
|
interface AuthorizeRewardModalProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onConfirm: (value: MerchantBackend.Tips.TipCreateRequest) => void;
|
onConfirm: (value: MerchantBackend.Rewards.RewardCreateRequest) => void;
|
||||||
tipAuthorized?: {
|
rewardAuthorized?: {
|
||||||
response: MerchantBackend.Tips.TipCreateConfirmation;
|
response: MerchantBackend.Rewards.RewardCreateConfirmation;
|
||||||
request: MerchantBackend.Tips.TipCreateRequest;
|
request: MerchantBackend.Rewards.RewardCreateRequest;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AuthorizeTipModal({
|
export function AuthorizeRewardModal({
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
tipAuthorized,
|
rewardAuthorized,
|
||||||
}: AuthorizeTipModalProps): VNode {
|
}: AuthorizeRewardModalProps): VNode {
|
||||||
// const result = useOrderDetails(id)
|
// const result = useOrderDetails(id)
|
||||||
type State = MerchantBackend.Tips.TipCreateRequest;
|
type State = MerchantBackend.Rewards.RewardCreateRequest;
|
||||||
const [form, setValue] = useState<Partial<State>>({});
|
const [form, setValue] = useState<Partial<State>>({});
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
// const [errors, setErrors] = useState<FormErrors<State>>({})
|
// const [errors, setErrors] = useState<FormErrors<State>>({})
|
||||||
let errors: FormErrors<State> = {};
|
let errors: FormErrors<State> = {};
|
||||||
try {
|
try {
|
||||||
AuthorizeTipSchema.validateSync(form, { abortEarly: false });
|
AuthorizeRewardSchema.validateSync(form, { abortEarly: false });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof yup.ValidationError) {
|
if (err instanceof yup.ValidationError) {
|
||||||
const yupErrors = err.inner as any[];
|
const yupErrors = err.inner as any[];
|
||||||
@ -77,12 +77,12 @@ export function AuthorizeTipModal({
|
|||||||
const validateAndConfirm = () => {
|
const validateAndConfirm = () => {
|
||||||
onConfirm(form as State);
|
onConfirm(form as State);
|
||||||
};
|
};
|
||||||
if (tipAuthorized) {
|
if (rewardAuthorized) {
|
||||||
return (
|
return (
|
||||||
<ContinueModal description="tip" active onConfirm={onCancel}>
|
<ContinueModal description="reward" active onConfirm={onCancel}>
|
||||||
<CreatedSuccessfully
|
<CreatedSuccessfully
|
||||||
entity={tipAuthorized.response}
|
entity={rewardAuthorized.response}
|
||||||
request={tipAuthorized.request}
|
request={rewardAuthorized.request}
|
||||||
onConfirm={onCancel}
|
onConfirm={onCancel}
|
||||||
/>
|
/>
|
||||||
</ContinueModal>
|
</ContinueModal>
|
||||||
@ -91,7 +91,7 @@ export function AuthorizeTipModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
description="tip"
|
description="New reward"
|
||||||
active
|
active
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
disabled={hasErrors}
|
disabled={hasErrors}
|
||||||
@ -105,18 +105,18 @@ export function AuthorizeTipModal({
|
|||||||
<InputCurrency<State>
|
<InputCurrency<State>
|
||||||
name="amount"
|
name="amount"
|
||||||
label={i18n.str`Amount`}
|
label={i18n.str`Amount`}
|
||||||
tooltip={i18n.str`amount of tip`}
|
tooltip={i18n.str`amount of reward`}
|
||||||
/>
|
/>
|
||||||
<Input<State>
|
<Input<State>
|
||||||
name="justification"
|
name="justification"
|
||||||
label={i18n.str`Justification`}
|
label={i18n.str`Justification`}
|
||||||
inputType="multiline"
|
inputType="multiline"
|
||||||
tooltip={i18n.str`reason for the tip`}
|
tooltip={i18n.str`reason for the reward`}
|
||||||
/>
|
/>
|
||||||
<Input<State>
|
<Input<State>
|
||||||
name="next_url"
|
name="next_url"
|
||||||
label={i18n.str`URL after tip`}
|
label={i18n.str`URL after reward`}
|
||||||
tooltip={i18n.str`URL to visit after tip payment`}
|
tooltip={i18n.str`URL to visit after reward payment`}
|
||||||
/>
|
/>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</ConfirmModal>
|
</ConfirmModal>
|
@ -17,12 +17,13 @@ import { format } from "date-fns";
|
|||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
|
import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Tips.TipCreateConfirmation;
|
type Entity = MerchantBackend.Rewards.RewardCreateConfirmation;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
request: MerchantBackend.Tips.TipCreateRequest;
|
request: MerchantBackend.Rewards.RewardCreateRequest;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onCreateAnother?: () => void;
|
onCreateAnother?: () => void;
|
||||||
}
|
}
|
||||||
@ -33,6 +34,7 @@ export function CreatedSuccessfully({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
onCreateAnother,
|
onCreateAnother,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
|
const [settings] = useSettings();
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
@ -66,7 +68,7 @@ export function CreatedSuccessfully({
|
|||||||
<div class="field-body is-flex-grow-3">
|
<div class="field-body is-flex-grow-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<input readonly class="input" value={entity.tip_status_url} />
|
<input readonly class="input" value={entity.reward_status_url} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -82,13 +84,13 @@ export function CreatedSuccessfully({
|
|||||||
class="input"
|
class="input"
|
||||||
readonly
|
readonly
|
||||||
value={
|
value={
|
||||||
!entity.tip_expiration ||
|
!entity.reward_expiration ||
|
||||||
entity.tip_expiration.t_s === "never"
|
entity.reward_expiration.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: format(
|
: format(
|
||||||
entity.tip_expiration.t_s * 1000,
|
entity.reward_expiration.t_s * 1000,
|
||||||
"yyyy/MM/dd HH:mm:ss",
|
datetimeFormatForSettings(settings),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
@ -25,12 +25,6 @@ import { CardTable as TestedComponent } from "./Table.js";
|
|||||||
export default {
|
export default {
|
||||||
title: "Pages/Reserve/List",
|
title: "Pages/Reserve/List",
|
||||||
component: TestedComponent,
|
component: TestedComponent,
|
||||||
argTypes: {
|
|
||||||
onCreate: { action: "onCreate" },
|
|
||||||
onDelete: { action: "onDelete" },
|
|
||||||
onNewTip: { action: "onNewTip" },
|
|
||||||
onSelect: { action: "onSelect" },
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function createExample<Props>(
|
function createExample<Props>(
|
||||||
|
@ -23,12 +23,13 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||||
|
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId;
|
type Entity = MerchantBackend.Rewards.ReserveStatusEntry & WithId;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
instances: Entity[];
|
instances: Entity[];
|
||||||
onNewTip: (id: Entity) => void;
|
onNewReward: (id: Entity) => void;
|
||||||
onSelect: (id: Entity) => void;
|
onSelect: (id: Entity) => void;
|
||||||
onDelete: (id: Entity) => void;
|
onDelete: (id: Entity) => void;
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
@ -38,7 +39,7 @@ export function CardTable({
|
|||||||
instances,
|
instances,
|
||||||
onCreate,
|
onCreate,
|
||||||
onSelect,
|
onSelect,
|
||||||
onNewTip,
|
onNewReward,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const [withoutFunds, withFunds] = instances.reduce((prev, current) => {
|
const [withoutFunds, withFunds] = instances.reduce((prev, current) => {
|
||||||
@ -70,7 +71,7 @@ export function CardTable({
|
|||||||
<div class="table-wrapper has-mobile-cards">
|
<div class="table-wrapper has-mobile-cards">
|
||||||
<TableWithoutFund
|
<TableWithoutFund
|
||||||
instances={withoutFunds}
|
instances={withoutFunds}
|
||||||
onNewTip={onNewTip}
|
onNewReward={onNewReward}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
@ -108,7 +109,7 @@ export function CardTable({
|
|||||||
{withFunds.length > 0 ? (
|
{withFunds.length > 0 ? (
|
||||||
<Table
|
<Table
|
||||||
instances={withFunds}
|
instances={withFunds}
|
||||||
onNewTip={onNewTip}
|
onNewReward={onNewReward}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
@ -124,13 +125,14 @@ export function CardTable({
|
|||||||
}
|
}
|
||||||
interface TableProps {
|
interface TableProps {
|
||||||
instances: Entity[];
|
instances: Entity[];
|
||||||
onNewTip: (id: Entity) => void;
|
onNewReward: (id: Entity) => void;
|
||||||
onDelete: (id: Entity) => void;
|
onDelete: (id: Entity) => void;
|
||||||
onSelect: (id: Entity) => void;
|
onSelect: (id: Entity) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
|
function Table({ instances, onNewReward, onSelect, onDelete }: TableProps): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings] = useSettings();
|
||||||
return (
|
return (
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
||||||
@ -164,7 +166,7 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
|
|||||||
>
|
>
|
||||||
{i.creation_time.t_s === "never"
|
{i.creation_time.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: format(i.creation_time.t_s * 1000, "yyyy/MM/dd HH:mm:ss")}
|
: format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
onClick={(): void => onSelect(i)}
|
onClick={(): void => onSelect(i)}
|
||||||
@ -173,9 +175,9 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
|
|||||||
{i.expiration_time.t_s === "never"
|
{i.expiration_time.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: format(
|
: format(
|
||||||
i.expiration_time.t_s * 1000,
|
i.expiration_time.t_s * 1000,
|
||||||
"yyyy/MM/dd HH:mm:ss",
|
datetimeFormatForSettings(settings),
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
onClick={(): void => onSelect(i)}
|
onClick={(): void => onSelect(i)}
|
||||||
@ -207,11 +209,11 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button is-small is-info has-tooltip-left"
|
class="button is-small is-info has-tooltip-left"
|
||||||
data-tooltip={i18n.str`authorize new tip from selected reserve`}
|
data-tooltip={i18n.str`authorize new reward from selected reserve`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(): void => onNewTip(i)}
|
onClick={(): void => onNewReward(i)}
|
||||||
>
|
>
|
||||||
New Tip
|
New Reward
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -249,6 +251,7 @@ function TableWithoutFund({
|
|||||||
onDelete,
|
onDelete,
|
||||||
}: TableProps): VNode {
|
}: TableProps): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings] = useSettings();
|
||||||
return (
|
return (
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
|
||||||
@ -276,7 +279,7 @@ function TableWithoutFund({
|
|||||||
>
|
>
|
||||||
{i.creation_time.t_s === "never"
|
{i.creation_time.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: format(i.creation_time.t_s * 1000, "yyyy/MM/dd HH:mm:ss")}
|
: format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
onClick={(): void => onSelect(i)}
|
onClick={(): void => onSelect(i)}
|
||||||
@ -285,9 +288,9 @@ function TableWithoutFund({
|
|||||||
{i.expiration_time.t_s === "never"
|
{i.expiration_time.t_s === "never"
|
||||||
? "never"
|
? "never"
|
||||||
: format(
|
: format(
|
||||||
i.expiration_time.t_s * 1000,
|
i.expiration_time.t_s * 1000,
|
||||||
"yyyy/MM/dd HH:mm:ss",
|
datetimeFormatForSettings(settings),
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
onClick={(): void => onSelect(i)}
|
onClick={(): void => onSelect(i)}
|
||||||
|
@ -34,9 +34,10 @@ import {
|
|||||||
useReservesAPI,
|
useReservesAPI,
|
||||||
} from "../../../../hooks/reserves.js";
|
} from "../../../../hooks/reserves.js";
|
||||||
import { Notification } from "../../../../utils/types.js";
|
import { Notification } from "../../../../utils/types.js";
|
||||||
import { AuthorizeTipModal } from "./AutorizeTipModal.js";
|
import { AuthorizeRewardModal } from "./AutorizeRewardModal.js";
|
||||||
import { CardTable } from "./Table.js";
|
import { CardTable } from "./Table.js";
|
||||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
|
import { ConfirmModal } from "../../../../components/modal/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onUnauthorized: () => VNode;
|
onUnauthorized: () => VNode;
|
||||||
@ -46,12 +47,12 @@ interface Props {
|
|||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TipConfirmation {
|
interface RewardConfirmation {
|
||||||
response: MerchantBackend.Tips.TipCreateConfirmation;
|
response: MerchantBackend.Rewards.RewardCreateConfirmation;
|
||||||
request: MerchantBackend.Tips.TipCreateRequest;
|
request: MerchantBackend.Rewards.RewardCreateRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ListTips({
|
export default function ListRewards({
|
||||||
onUnauthorized,
|
onUnauthorized,
|
||||||
onLoadError,
|
onLoadError,
|
||||||
onNotFound,
|
onNotFound,
|
||||||
@ -59,14 +60,16 @@ export default function ListTips({
|
|||||||
onCreate,
|
onCreate,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const result = useInstanceReserves();
|
const result = useInstanceReserves();
|
||||||
const { deleteReserve, authorizeTipReserve } = useReservesAPI();
|
const { deleteReserve, authorizeRewardReserve } = useReservesAPI();
|
||||||
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const [reserveForTip, setReserveForTip] = useState<string | undefined>(
|
const [reserveForReward, setReserveForReward] = useState<string | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
const [tipAuthorized, setTipAuthorized] = useState<
|
const [deleting, setDeleting] =
|
||||||
TipConfirmation | undefined
|
useState<MerchantBackend.Rewards.ReserveStatusEntry | null>(null);
|
||||||
|
const [rewardAuthorized, setRewardAuthorized] = useState<
|
||||||
|
RewardConfirmation | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
if (result.loading) return <Loading />;
|
if (result.loading) return <Loading />;
|
||||||
@ -88,30 +91,30 @@ export default function ListTips({
|
|||||||
<section class="section is-main-section">
|
<section class="section is-main-section">
|
||||||
<NotificationCard notification={notif} />
|
<NotificationCard notification={notif} />
|
||||||
|
|
||||||
{reserveForTip && (
|
{reserveForReward && (
|
||||||
<AuthorizeTipModal
|
<AuthorizeRewardModal
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setReserveForTip(undefined);
|
setReserveForReward(undefined);
|
||||||
setTipAuthorized(undefined);
|
setRewardAuthorized(undefined);
|
||||||
}}
|
}}
|
||||||
tipAuthorized={tipAuthorized}
|
rewardAuthorized={rewardAuthorized}
|
||||||
onConfirm={async (request) => {
|
onConfirm={async (request) => {
|
||||||
try {
|
try {
|
||||||
const response = await authorizeTipReserve(
|
const response = await authorizeRewardReserve(
|
||||||
reserveForTip,
|
reserveForReward,
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
setTipAuthorized({
|
setRewardAuthorized({
|
||||||
request,
|
request,
|
||||||
response: response.data,
|
response: response.data,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNotif({
|
setNotif({
|
||||||
message: i18n.str`could not create the tip`,
|
message: i18n.str`could not create the reward`,
|
||||||
type: "ERROR",
|
type: "ERROR",
|
||||||
description: error instanceof Error ? error.message : undefined,
|
description: error instanceof Error ? error.message : undefined,
|
||||||
});
|
});
|
||||||
setReserveForTip(undefined);
|
setReserveForReward(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -122,10 +125,47 @@ export default function ListTips({
|
|||||||
.filter((r) => r.active)
|
.filter((r) => r.active)
|
||||||
.map((o) => ({ ...o, id: o.reserve_pub }))}
|
.map((o) => ({ ...o, id: o.reserve_pub }))}
|
||||||
onCreate={onCreate}
|
onCreate={onCreate}
|
||||||
onDelete={(reserve) => deleteReserve(reserve.reserve_pub)}
|
onDelete={(reserve) => {
|
||||||
|
setDeleting(reserve)
|
||||||
|
}}
|
||||||
onSelect={(reserve) => onSelect(reserve.id)}
|
onSelect={(reserve) => onSelect(reserve.id)}
|
||||||
onNewTip={(reserve) => setReserveForTip(reserve.id)}
|
onNewReward={(reserve) => setReserveForReward(reserve.id)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{deleting && (
|
||||||
|
<ConfirmModal
|
||||||
|
label={`Delete reserve`}
|
||||||
|
description={`Delete the reserve`}
|
||||||
|
danger
|
||||||
|
active
|
||||||
|
onCancel={() => setDeleting(null)}
|
||||||
|
onConfirm={async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await deleteReserve(deleting.reserve_pub);
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`Reserve for "${deleting.merchant_initial_amount}" (ID: ${deleting.reserve_pub}) has been deleted`,
|
||||||
|
type: "SUCCESS",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`Failed to delete reserve`,
|
||||||
|
type: "ERROR",
|
||||||
|
description: error instanceof Error ? error.message : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setDeleting(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
If you delete the reserve for <b>"{deleting.merchant_initial_amount}"</b> you won't be able to create more rewards. <br />
|
||||||
|
Reserve ID: <b>{deleting.reserve_pub}</b>
|
||||||
|
</p>
|
||||||
|
<p class="warning">
|
||||||
|
Deleting an template <b>cannot be undone</b>.
|
||||||
|
</p>
|
||||||
|
</ConfirmModal>
|
||||||
|
)}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ import {
|
|||||||
MerchantTemplateContractDetails,
|
MerchantTemplateContractDetails,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
|
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
|
||||||
import {
|
import {
|
||||||
@ -35,17 +35,16 @@ import { Input } from "../../../../components/form/Input.js";
|
|||||||
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
||||||
import { InputDuration } from "../../../../components/form/InputDuration.js";
|
import { InputDuration } from "../../../../components/form/InputDuration.js";
|
||||||
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
||||||
import { InputSelector } from "../../../../components/form/InputSelector.js";
|
|
||||||
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
||||||
import { useBackendContext } from "../../../../context/backend.js";
|
import { useBackendContext } from "../../../../context/backend.js";
|
||||||
|
import { useInstanceContext } from "../../../../context/instance.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
import {
|
import {
|
||||||
isBase32RFC3548Charset,
|
isBase32RFC3548Charset
|
||||||
randomBase32Key,
|
|
||||||
} from "../../../../utils/crypto.js";
|
} from "../../../../utils/crypto.js";
|
||||||
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
||||||
import { QR } from "../../../../components/exception/QR.js";
|
import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
|
||||||
import { useInstanceContext } from "../../../../context/instance.js";
|
import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Template.TemplateAddDetails;
|
type Entity = MerchantBackend.Template.TemplateAddDetails;
|
||||||
|
|
||||||
@ -54,16 +53,11 @@ interface Props {
|
|||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const algorithms = [0, 1, 2];
|
|
||||||
const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
|
|
||||||
|
|
||||||
export function CreatePage({ onCreate, onBack }: Props): VNode {
|
export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const backend = useBackendContext();
|
const backend = useBackendContext();
|
||||||
const { id: instanceId } = useInstanceContext();
|
const devices = useInstanceOtpDevices()
|
||||||
const issuer = new URL(backend.url).hostname;
|
|
||||||
|
|
||||||
const [showKey, setShowKey] = useState(false);
|
|
||||||
const [state, setState] = useState<Partial<Entity>>({
|
const [state, setState] = useState<Partial<Entity>>({
|
||||||
template_contract: {
|
template_contract: {
|
||||||
minimum_age: 0,
|
minimum_age: 0,
|
||||||
@ -78,7 +72,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
|||||||
: Amounts.parse(state.template_contract?.amount);
|
: Amounts.parse(state.template_contract?.amount);
|
||||||
|
|
||||||
const errors: FormErrors<Entity> = {
|
const errors: FormErrors<Entity> = {
|
||||||
template_id: !state.template_id ? i18n.str`should not be empty` : undefined,
|
template_id: !state.template_id
|
||||||
|
? i18n.str`should not be empty`
|
||||||
|
: !/[a-zA-Z0-9]*/.test(state.template_id)
|
||||||
|
? i18n.str`no valid. only characters and numbers`
|
||||||
|
: undefined,
|
||||||
template_description: !state.template_description
|
template_description: !state.template_description
|
||||||
? i18n.str`should not be empty`
|
? i18n.str`should not be empty`
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -104,15 +102,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
|||||||
? i18n.str`to short`
|
? i18n.str`to short`
|
||||||
: undefined,
|
: undefined,
|
||||||
} as Partial<MerchantTemplateContractDetails>),
|
} as Partial<MerchantTemplateContractDetails>),
|
||||||
pos_key: !state.pos_key
|
|
||||||
? !state.pos_algorithm
|
|
||||||
? undefined
|
|
||||||
: i18n.str`required`
|
|
||||||
: !isBase32RFC3548Charset(state.pos_key)
|
|
||||||
? i18n.str`just letters and numbers from 2 to 7`
|
|
||||||
: state.pos_key.length !== 32
|
|
||||||
? i18n.str`size of the key should be 32`
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasErrors = Object.keys(errors).some(
|
const hasErrors = Object.keys(errors).some(
|
||||||
@ -124,7 +113,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
|||||||
return onCreate(state as any);
|
return onCreate(state as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
const qrText = `otpauth://totp/${instanceId}/${state.template_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`;
|
const deviceList = !devices.ok ? [] : devices.data.otp_devices
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -139,7 +128,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
|||||||
>
|
>
|
||||||
<InputWithAddon<Entity>
|
<InputWithAddon<Entity>
|
||||||
name="template_id"
|
name="template_id"
|
||||||
help={`${backend.url}/instances/templates/${state.template_id ?? ""}`}
|
help={`${backend.url}/templates/${state.template_id ?? ""}`}
|
||||||
label={i18n.str`Identifier`}
|
label={i18n.str`Identifier`}
|
||||||
tooltip={i18n.str`Name of the template in URLs.`}
|
tooltip={i18n.str`Name of the template in URLs.`}
|
||||||
/>
|
/>
|
||||||
@ -172,83 +161,21 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
|||||||
help=""
|
help=""
|
||||||
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
|
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
|
||||||
/>
|
/>
|
||||||
<InputSelector<Entity>
|
<Input<Entity>
|
||||||
name="pos_algorithm"
|
name="otp_id"
|
||||||
label={i18n.str`Verification algorithm`}
|
label={i18n.str`OTP device`}
|
||||||
tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
|
readonly
|
||||||
values={algorithms}
|
tooltip={i18n.str`Use to verify transaction in offline mode.`}
|
||||||
toStr={(v) => algorithmsNames[v]}
|
|
||||||
fromStr={(v) => Number(v)}
|
|
||||||
/>
|
/>
|
||||||
{state.pos_algorithm && state.pos_algorithm > 0 ? (
|
<InputSearchOnList
|
||||||
<Fragment>
|
label={i18n.str`Search device`}
|
||||||
<InputWithAddon<Entity>
|
onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))}
|
||||||
name="pos_key"
|
list={deviceList.map(e => ({
|
||||||
label={i18n.str`Point-of-sale key`}
|
description: e.device_description,
|
||||||
inputType={showKey ? "text" : "password"}
|
id: e.otp_device_id
|
||||||
help="Be sure to be very hard to guess or use the random generator"
|
}))}
|
||||||
tooltip={i18n.str`Useful to validate the purchase`}
|
/>
|
||||||
fromStr={(v) => v.toUpperCase()}
|
|
||||||
addonAfter={
|
|
||||||
<span class="icon">
|
|
||||||
{showKey ? (
|
|
||||||
<i class="mdi mdi-eye" />
|
|
||||||
) : (
|
|
||||||
<i class="mdi mdi-eye-off" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
side={
|
|
||||||
<span style={{ display: "flex" }}>
|
|
||||||
<button
|
|
||||||
data-tooltip={i18n.str`generate random secret key`}
|
|
||||||
class="button is-info mr-3"
|
|
||||||
onClick={(e) => {
|
|
||||||
const pos_key = randomBase32Key();
|
|
||||||
setState((s) => ({ ...s, pos_key }));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i18n.Translate>random</i18n.Translate>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
data-tooltip={
|
|
||||||
showKey
|
|
||||||
? i18n.str`show secret key`
|
|
||||||
: i18n.str`hide secret key`
|
|
||||||
}
|
|
||||||
class="button is-info mr-3"
|
|
||||||
onClick={(e) => {
|
|
||||||
setShowKey(!showKey);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showKey ? (
|
|
||||||
<i18n.Translate>hide</i18n.Translate>
|
|
||||||
) : (
|
|
||||||
<i18n.Translate>show</i18n.Translate>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{showKey && (
|
|
||||||
<Fragment>
|
|
||||||
<QR text={qrText} />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
color: "grey",
|
|
||||||
fontSize: "small",
|
|
||||||
width: 200,
|
|
||||||
textAlign: "center",
|
|
||||||
margin: "auto",
|
|
||||||
wordBreak: "break-all",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{qrText}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
) : undefined}
|
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
<div class="buttons is-right mt-5">
|
<div class="buttons is-right mt-5">
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
import { Notification } from "../../../../utils/types.js";
|
import { Notification } from "../../../../utils/types.js";
|
||||||
import { ListPage } from "./ListPage.js";
|
import { ListPage } from "./ListPage.js";
|
||||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
|
import { ConfirmModal } from "../../../../components/modal/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onUnauthorized: () => VNode;
|
onUnauthorized: () => VNode;
|
||||||
@ -61,6 +62,8 @@ export default function ListTemplates({
|
|||||||
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||||
const { deleteTemplate } = useTemplateAPI();
|
const { deleteTemplate } = useTemplateAPI();
|
||||||
const result = useInstanceTemplates({ position }, (id) => setPosition(id));
|
const result = useInstanceTemplates({ position }, (id) => setPosition(id));
|
||||||
|
const [deleting, setDeleting] =
|
||||||
|
useState<MerchantBackend.Template.TemplateEntry | null>(null);
|
||||||
|
|
||||||
if (result.loading) return <Loading />;
|
if (result.loading) return <Loading />;
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
@ -97,23 +100,45 @@ export default function ListTemplates({
|
|||||||
onQR={(e) => {
|
onQR={(e) => {
|
||||||
onQR(e.template_id);
|
onQR(e.template_id);
|
||||||
}}
|
}}
|
||||||
onDelete={(e: MerchantBackend.Template.TemplateEntry) =>
|
onDelete={(e: MerchantBackend.Template.TemplateEntry) => {
|
||||||
deleteTemplate(e.template_id)
|
setDeleting(e)
|
||||||
.then(() =>
|
}
|
||||||
setNotif({
|
|
||||||
message: i18n.str`template delete successfully`,
|
|
||||||
type: "SUCCESS",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.catch((error) =>
|
|
||||||
setNotif({
|
|
||||||
message: i18n.str`could not delete the template`,
|
|
||||||
type: "ERROR",
|
|
||||||
description: error.message,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{deleting && (
|
||||||
|
<ConfirmModal
|
||||||
|
label={`Delete template`}
|
||||||
|
description={`Delete the template "${deleting.template_description}"`}
|
||||||
|
danger
|
||||||
|
active
|
||||||
|
onCancel={() => setDeleting(null)}
|
||||||
|
onConfirm={async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await deleteTemplate(deleting.template_id);
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`,
|
||||||
|
type: "SUCCESS",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`Failed to delete template`,
|
||||||
|
type: "ERROR",
|
||||||
|
description: error instanceof Error ? error.message : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setDeleting(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
If you delete the template <b>"{deleting.template_description}"</b> (ID:{" "}
|
||||||
|
<b>{deleting.template_id}</b>) you may loose information
|
||||||
|
</p>
|
||||||
|
<p class="warning">
|
||||||
|
Deleting an template <b>cannot be undone</b>.
|
||||||
|
</p>
|
||||||
|
</ConfirmModal>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { QR } from "../../../../components/exception/QR.js";
|
import { QR } from "../../../../components/exception/QR.js";
|
||||||
@ -35,35 +35,32 @@ import { useConfigContext } from "../../../../context/config.js";
|
|||||||
import { useInstanceContext } from "../../../../context/instance.js";
|
import { useInstanceContext } from "../../../../context/instance.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
|
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
|
||||||
|
import { useOtpDeviceDetails } from "../../../../hooks/otp.js";
|
||||||
|
import { Loading } from "../../../../components/exception/loading.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Template.UsingTemplateDetails;
|
type Entity = MerchantBackend.Template.UsingTemplateDetails;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
template: MerchantBackend.Template.TemplateDetails;
|
contract: MerchantBackend.Template.TemplateContractDetails;
|
||||||
id: string;
|
id: string;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QrPage({ template, id: templateId, onBack }: Props): VNode {
|
export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const { url: backendUrl } = useBackendContext();
|
const { url: backendUrl } = useBackendContext();
|
||||||
const { id: instanceId } = useInstanceContext();
|
const { id: instanceId } = useInstanceContext();
|
||||||
const config = useConfigContext();
|
const config = useConfigContext();
|
||||||
const [setupTOTP, setSetupTOTP] = useState(false);
|
|
||||||
|
|
||||||
const [state, setState] = useState<Partial<Entity>>({
|
const [state, setState] = useState<Partial<Entity>>({
|
||||||
amount: template.template_contract.amount,
|
amount: contract.amount,
|
||||||
summary: template.template_contract.summary,
|
summary: contract.summary,
|
||||||
});
|
});
|
||||||
|
|
||||||
const errors: FormErrors<Entity> = {};
|
const errors: FormErrors<Entity> = {};
|
||||||
|
|
||||||
const hasErrors = Object.keys(errors).some(
|
const fixedAmount = !!contract.amount;
|
||||||
(k) => (errors as any)[k] !== undefined,
|
const fixedSummary = !!contract.summary;
|
||||||
);
|
|
||||||
|
|
||||||
const fixedAmount = !!template.template_contract.amount;
|
|
||||||
const fixedSummary = !!template.template_contract.summary;
|
|
||||||
|
|
||||||
const templateParams: Record<string, string> = {}
|
const templateParams: Record<string, string> = {}
|
||||||
if (!fixedAmount) {
|
if (!fixedAmount) {
|
||||||
@ -89,40 +86,9 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
|
|||||||
const issuer = encodeURIComponent(
|
const issuer = encodeURIComponent(
|
||||||
`${new URL(backendUrl).host}/${instanceId}`,
|
`${new URL(backendUrl).host}/${instanceId}`,
|
||||||
);
|
);
|
||||||
const oauthUri = !template.pos_algorithm
|
|
||||||
? undefined
|
|
||||||
: template.pos_algorithm === 1
|
|
||||||
? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
|
|
||||||
: template.pos_algorithm === 2
|
|
||||||
? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const keySlice = template.pos_key?.substring(0, 4);
|
|
||||||
|
|
||||||
const oauthUriWithoutSecret = !template.pos_algorithm
|
|
||||||
? undefined
|
|
||||||
: template.pos_algorithm === 1
|
|
||||||
? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
|
|
||||||
: template.pos_algorithm === 2
|
|
||||||
? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
|
|
||||||
: undefined;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{oauthUri && (
|
|
||||||
<ConfirmModal
|
|
||||||
description="Setup TOTP"
|
|
||||||
active={setupTOTP}
|
|
||||||
onCancel={() => {
|
|
||||||
setSetupTOTP(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p>Scan this qr code with your TOTP device</p>
|
|
||||||
<QR text={oauthUri} />
|
|
||||||
<pre style={{ textAlign: "center" }}>
|
|
||||||
<a href={oauthUri}>{oauthUriWithoutSecret}</a>
|
|
||||||
</pre>
|
|
||||||
</ConfirmModal>
|
|
||||||
)}
|
|
||||||
<section class="section is-main-section">
|
<section class="section is-main-section">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column" />
|
<div class="column" />
|
||||||
@ -176,14 +142,6 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
|
|||||||
>
|
>
|
||||||
<i18n.Translate>Print</i18n.Translate>
|
<i18n.Translate>Print</i18n.Translate>
|
||||||
</button>
|
</button>
|
||||||
{oauthUri && (
|
|
||||||
<button
|
|
||||||
class="button is-info"
|
|
||||||
onClick={() => setSetupTOTP(true)}
|
|
||||||
>
|
|
||||||
<i18n.Translate>Setup TOTP</i18n.Translate>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column" />
|
<div class="column" />
|
||||||
|
@ -74,7 +74,7 @@ export default function TemplateQrPage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NotificationCard notification={notif} />
|
<NotificationCard notification={notif} />
|
||||||
<QrPage template={result.data} id={tid} onBack={onBack} />
|
<QrPage contract={result.data.template_contract} id={tid} onBack={onBack} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -61,10 +61,7 @@ const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
|
|||||||
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const backend = useBackendContext();
|
const backend = useBackendContext();
|
||||||
const { id: instanceId } = useInstanceContext();
|
|
||||||
const issuer = new URL(backend.url).hostname;
|
|
||||||
|
|
||||||
const [showKey, setShowKey] = useState(false);
|
|
||||||
const [state, setState] = useState<Partial<Entity>>(template);
|
const [state, setState] = useState<Partial<Entity>>(template);
|
||||||
|
|
||||||
const parsedPrice = !state.template_contract?.amount
|
const parsedPrice = !state.template_contract?.amount
|
||||||
@ -78,34 +75,25 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
|||||||
template_contract: !state.template_contract
|
template_contract: !state.template_contract
|
||||||
? undefined
|
? undefined
|
||||||
: undefinedIfEmpty({
|
: undefinedIfEmpty({
|
||||||
amount: !state.template_contract?.amount
|
amount: !state.template_contract?.amount
|
||||||
? undefined
|
? undefined
|
||||||
: !parsedPrice
|
: !parsedPrice
|
||||||
? i18n.str`not valid`
|
? i18n.str`not valid`
|
||||||
: Amounts.isZero(parsedPrice)
|
: Amounts.isZero(parsedPrice)
|
||||||
? i18n.str`must be greater than 0`
|
? i18n.str`must be greater than 0`
|
||||||
: undefined,
|
|
||||||
minimum_age:
|
|
||||||
state.template_contract.minimum_age < 0
|
|
||||||
? i18n.str`should be greater that 0`
|
|
||||||
: undefined,
|
: undefined,
|
||||||
pay_duration: !state.template_contract.pay_duration
|
minimum_age:
|
||||||
? i18n.str`can't be empty`
|
state.template_contract.minimum_age < 0
|
||||||
: state.template_contract.pay_duration.d_us === "forever"
|
? i18n.str`should be greater that 0`
|
||||||
|
: undefined,
|
||||||
|
pay_duration: !state.template_contract.pay_duration
|
||||||
|
? i18n.str`can't be empty`
|
||||||
|
: state.template_contract.pay_duration.d_us === "forever"
|
||||||
? undefined
|
? undefined
|
||||||
: state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second
|
: state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second
|
||||||
? i18n.str`to short`
|
? i18n.str`to short`
|
||||||
: undefined,
|
: undefined,
|
||||||
} as Partial<MerchantTemplateContractDetails>),
|
} as Partial<MerchantTemplateContractDetails>),
|
||||||
pos_key: !state.pos_key
|
|
||||||
? !state.pos_algorithm
|
|
||||||
? undefined
|
|
||||||
: i18n.str`required`
|
|
||||||
: !isBase32RFC3548Charset(state.pos_key)
|
|
||||||
? i18n.str`just letters and numbers from 2 to 7`
|
|
||||||
: state.pos_key.length !== 32
|
|
||||||
? i18n.str`size of the key should be 32`
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasErrors = Object.keys(errors).some(
|
const hasErrors = Object.keys(errors).some(
|
||||||
@ -117,7 +105,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
|||||||
return onUpdate(state as any);
|
return onUpdate(state as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
const qrText = `otpauth://totp/${instanceId}/${state.id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -128,7 +115,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
|||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<span class="is-size-4">
|
<span class="is-size-4">
|
||||||
{backend.url}/instances/template/{template.id}
|
{backend.url}/templates/{template.id}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -182,84 +169,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
|||||||
help=""
|
help=""
|
||||||
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
|
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
|
||||||
/>
|
/>
|
||||||
<InputSelector<Entity>
|
|
||||||
name="pos_algorithm"
|
|
||||||
label={i18n.str`Verification algorithm`}
|
|
||||||
tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
|
|
||||||
values={algorithms}
|
|
||||||
toStr={(v) => algorithmsNames[v]}
|
|
||||||
fromStr={(v) => Number(v)}
|
|
||||||
/>
|
|
||||||
{state.pos_algorithm && state.pos_algorithm > 0 ? (
|
|
||||||
<Fragment>
|
|
||||||
<InputWithAddon<Entity>
|
|
||||||
name="pos_key"
|
|
||||||
label={i18n.str`Point-of-sale key`}
|
|
||||||
inputType={showKey ? "text" : "password"}
|
|
||||||
help="Be sure to be very hard to guess or use the random generator"
|
|
||||||
expand
|
|
||||||
tooltip={i18n.str`Useful to validate the purchase`}
|
|
||||||
fromStr={(v) => v.toUpperCase()}
|
|
||||||
addonAfter={
|
|
||||||
<span class="icon">
|
|
||||||
{showKey ? (
|
|
||||||
<i class="mdi mdi-eye" />
|
|
||||||
) : (
|
|
||||||
<i class="mdi mdi-eye-off" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
side={
|
|
||||||
<span style={{ display: "flex" }}>
|
|
||||||
<button
|
|
||||||
data-tooltip={i18n.str`generate random secret key`}
|
|
||||||
class="button is-info mr-3"
|
|
||||||
onClick={(e) => {
|
|
||||||
const pos_key = randomBase32Key();
|
|
||||||
setState((s) => ({ ...s, pos_key }));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i18n.Translate>random</i18n.Translate>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
data-tooltip={
|
|
||||||
showKey
|
|
||||||
? i18n.str`show secret key`
|
|
||||||
: i18n.str`hide secret key`
|
|
||||||
}
|
|
||||||
class="button is-info mr-3"
|
|
||||||
onClick={(e) => {
|
|
||||||
setShowKey(!showKey);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showKey ? (
|
|
||||||
<i18n.Translate>hide</i18n.Translate>
|
|
||||||
) : (
|
|
||||||
<i18n.Translate>show</i18n.Translate>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{showKey && (
|
|
||||||
<Fragment>
|
|
||||||
<QR text={qrText} />
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
color: "grey",
|
|
||||||
fontSize: "small",
|
|
||||||
width: 200,
|
|
||||||
textAlign: "center",
|
|
||||||
margin: "auto",
|
|
||||||
wordBreak: "break-all",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{qrText}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
) : undefined}
|
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
<div class="buttons is-right mt-5">
|
<div class="buttons is-right mt-5">
|
||||||
|
@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { AsyncButton } from "../../../components/exception/AsyncButton.js";
|
||||||
|
import { FormProvider } from "../../../components/form/FormProvider.js";
|
||||||
|
import { Input } from "../../../components/form/Input.js";
|
||||||
|
import { useInstanceContext } from "../../../context/instance.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
instanceId: string;
|
||||||
|
currentToken: string | undefined;
|
||||||
|
onClearToken: () => void;
|
||||||
|
onNewToken: (s: string) => void;
|
||||||
|
onBack?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewToken, onClearToken }: Props): VNode {
|
||||||
|
type State = { old_token: string; new_token: string; repeat_token: string };
|
||||||
|
const [form, setValue] = useState<Partial<State>>({
|
||||||
|
old_token: "",
|
||||||
|
new_token: "",
|
||||||
|
repeat_token: "",
|
||||||
|
});
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
const hasOldtoken = !!oldToken
|
||||||
|
const hasInputTheCorrectOldToken = hasOldtoken && oldToken !== form.old_token;
|
||||||
|
const errors = {
|
||||||
|
old_token: hasInputTheCorrectOldToken
|
||||||
|
? i18n.str`is not the same as the current access token`
|
||||||
|
: undefined,
|
||||||
|
new_token: !form.new_token
|
||||||
|
? i18n.str`cannot be empty`
|
||||||
|
: form.new_token === form.old_token
|
||||||
|
? i18n.str`cannot be the same as the old token`
|
||||||
|
: undefined,
|
||||||
|
repeat_token:
|
||||||
|
form.new_token !== form.repeat_token
|
||||||
|
? i18n.str`is not the same`
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasErrors = Object.keys(errors).some(
|
||||||
|
(k) => (errors as any)[k] !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const instance = useInstanceContext();
|
||||||
|
|
||||||
|
const text = i18n.str`You are updating the access token from instance with id ${instance.id}`;
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
if (hasErrors) return;
|
||||||
|
onNewToken(form.new_token as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<section class="section">
|
||||||
|
<section class="hero is-hero-bar">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item">
|
||||||
|
<span class="is-size-4">
|
||||||
|
Instance id: <b>{instanceId}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column" />
|
||||||
|
<div class="column is-four-fifths">
|
||||||
|
<FormProvider errors={errors} object={form} valueHandler={setValue}>
|
||||||
|
{hasOldtoken && (
|
||||||
|
<Input<State>
|
||||||
|
name="old_token"
|
||||||
|
label={i18n.str`Current access token`}
|
||||||
|
tooltip={i18n.str`access token currently in use`}
|
||||||
|
inputType="password"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!hasInputTheCorrectOldToken && <Fragment>
|
||||||
|
{hasOldtoken && <Fragment>
|
||||||
|
<p>
|
||||||
|
<i18n.Translate>
|
||||||
|
Clearing the access token will mean public access to the instance.
|
||||||
|
</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
<div class="buttons is-right mt-5">
|
||||||
|
<button
|
||||||
|
disabled={!!hasInputTheCorrectOldToken}
|
||||||
|
class="button"
|
||||||
|
onClick={onClearToken}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Clear token</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Input<State>
|
||||||
|
name="new_token"
|
||||||
|
label={i18n.str`New access token`}
|
||||||
|
tooltip={i18n.str`next access token to be used`}
|
||||||
|
inputType="password"
|
||||||
|
/>
|
||||||
|
<Input<State>
|
||||||
|
name="repeat_token"
|
||||||
|
label={i18n.str`Repeat access token`}
|
||||||
|
tooltip={i18n.str`confirm the same access token`}
|
||||||
|
inputType="password"
|
||||||
|
/>
|
||||||
|
</Fragment>}
|
||||||
|
</FormProvider>
|
||||||
|
<div class="buttons is-right mt-5">
|
||||||
|
{onBack && (
|
||||||
|
<button class="button" onClick={onBack}>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<AsyncButton
|
||||||
|
disabled={hasErrors}
|
||||||
|
data-tooltip={
|
||||||
|
hasErrors
|
||||||
|
? i18n.str`Need to complete marked fields`
|
||||||
|
: "confirm operation"
|
||||||
|
}
|
||||||
|
onClick={submitForm}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Confirm change</i18n.Translate>
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
|
import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { Fragment, VNode, h } from "preact";
|
||||||
|
import { Loading } from "../../../components/exception/loading.js";
|
||||||
|
import { MerchantBackend } from "../../../declaration.js";
|
||||||
|
import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
|
||||||
|
import { DetailPage } from "./DetailPage.js";
|
||||||
|
import { useInstanceContext } from "../../../context/instance.js";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { NotificationCard } from "../../../components/menu/index.js";
|
||||||
|
import { Notification } from "../../../utils/types.js";
|
||||||
|
import { useBackendContext } from "../../../context/backend.js";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onUnauthorized: () => VNode;
|
||||||
|
onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
|
||||||
|
onChange: () => void;
|
||||||
|
onNotFound: () => VNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PREFIX = "secret-token:"
|
||||||
|
|
||||||
|
export default function Token({
|
||||||
|
onLoadError,
|
||||||
|
onChange,
|
||||||
|
onUnauthorized,
|
||||||
|
onNotFound,
|
||||||
|
}: Props): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||||
|
const { clearToken, setNewToken } = useInstanceAPI();
|
||||||
|
const { token: rootToken } = useBackendContext();
|
||||||
|
const { token: instanceToken, id, admin } = useInstanceContext();
|
||||||
|
|
||||||
|
const currentToken = !admin ? rootToken : instanceToken
|
||||||
|
const hasPrefix = currentToken !== undefined && currentToken.startsWith(PREFIX)
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<NotificationCard notification={notif} />
|
||||||
|
<DetailPage
|
||||||
|
instanceId={id}
|
||||||
|
currentToken={hasPrefix ? currentToken.substring(PREFIX.length) : currentToken}
|
||||||
|
onClearToken={async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await clearToken();
|
||||||
|
onChange();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`Failed to clear token`,
|
||||||
|
type: "ERROR",
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onNewToken={async (newToken): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await setNewToken(`secret-token:${newToken}`);
|
||||||
|
onChange();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setNotif({
|
||||||
|
message: i18n.str`Failed to set new token`,
|
||||||
|
type: "ERROR",
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 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 { DetailPage as TestedComponent } from "./DetailPage.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Pages/Token",
|
||||||
|
component: TestedComponent,
|
||||||
|
};
|
||||||
|
|
@ -28,6 +28,7 @@ import { useInstanceDetails } from "../../../../hooks/instance.js";
|
|||||||
import { useTransferAPI } from "../../../../hooks/transfer.js";
|
import { useTransferAPI } from "../../../../hooks/transfer.js";
|
||||||
import { Notification } from "../../../../utils/types.js";
|
import { Notification } from "../../../../utils/types.js";
|
||||||
import { CreatePage } from "./CreatePage.js";
|
import { CreatePage } from "./CreatePage.js";
|
||||||
|
import { useBankAccountDetails, useInstanceBankAccounts } from "../../../../hooks/bank.js";
|
||||||
|
|
||||||
export type Entity = MerchantBackend.Transfers.TransferInformation;
|
export type Entity = MerchantBackend.Transfers.TransferInformation;
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -39,7 +40,7 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
|
|||||||
const { informTransfer } = useTransferAPI();
|
const { informTransfer } = useTransferAPI();
|
||||||
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const instance = useInstanceDetails();
|
const instance = useInstanceBankAccounts();
|
||||||
const accounts = !instance.ok
|
const accounts = !instance.ok
|
||||||
? []
|
? []
|
||||||
: instance.data.accounts.map((a) => a.payto_uri);
|
: instance.data.accounts.map((a) => a.payto_uri);
|
||||||
|
@ -24,6 +24,7 @@ import { format } from "date-fns";
|
|||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { StateUpdater, useState } from "preact/hooks";
|
import { StateUpdater, useState } from "preact/hooks";
|
||||||
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||||
|
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Transfers.TransferDetails & WithId;
|
type Entity = MerchantBackend.Transfers.TransferDetails & WithId;
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ export function CardTable({
|
|||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">
|
<p class="card-header-title">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="mdi mdi-bank" />
|
<i class="mdi mdi-arrow-left-right" />
|
||||||
</span>
|
</span>
|
||||||
<i18n.Translate>Transfers</i18n.Translate>
|
<i18n.Translate>Transfers</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
@ -121,6 +122,7 @@ function Table({
|
|||||||
hasMoreBefore,
|
hasMoreBefore,
|
||||||
}: TableProps): VNode {
|
}: TableProps): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const [settings] = useSettings();
|
||||||
return (
|
return (
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
{onLoadMoreBefore && (
|
{onLoadMoreBefore && (
|
||||||
@ -175,9 +177,9 @@ function Table({
|
|||||||
? i.execution_time.t_s == "never"
|
? i.execution_time.t_s == "never"
|
||||||
? i18n.str`never`
|
? i18n.str`never`
|
||||||
: format(
|
: format(
|
||||||
i.execution_time.t_s * 1000,
|
i.execution_time.t_s * 1000,
|
||||||
"yyyy/MM/dd HH:mm:ss",
|
datetimeFormatForSettings(settings),
|
||||||
)
|
)
|
||||||
: i18n.str`unknown`}
|
: i18n.str`unknown`}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -28,6 +28,7 @@ import { useInstanceDetails } from "../../../../hooks/instance.js";
|
|||||||
import { useInstanceTransfers } from "../../../../hooks/transfer.js";
|
import { useInstanceTransfers } from "../../../../hooks/transfer.js";
|
||||||
import { ListPage } from "./ListPage.js";
|
import { ListPage } from "./ListPage.js";
|
||||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
|
import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onUnauthorized: () => VNode;
|
onUnauthorized: () => VNode;
|
||||||
@ -51,7 +52,7 @@ export default function ListTransfer({
|
|||||||
|
|
||||||
const [position, setPosition] = useState<string | undefined>(undefined);
|
const [position, setPosition] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const instance = useInstanceDetails();
|
const instance = useInstanceBankAccounts();
|
||||||
const accounts = !instance.ok
|
const accounts = !instance.ok
|
||||||
? []
|
? []
|
||||||
: instance.data.accounts.map((a) => a.payto_uri);
|
: instance.data.accounts.map((a) => a.payto_uri);
|
||||||
|
@ -42,17 +42,15 @@ function createExample<Props>(
|
|||||||
|
|
||||||
export const Example = createExample(TestedComponent, {
|
export const Example = createExample(TestedComponent, {
|
||||||
selected: {
|
selected: {
|
||||||
accounts: [],
|
|
||||||
name: "name",
|
name: "name",
|
||||||
auth: { method: "external" },
|
auth: { method: "external" },
|
||||||
address: {},
|
address: {},
|
||||||
|
user_type: "business",
|
||||||
|
use_stefan: true,
|
||||||
jurisdiction: {},
|
jurisdiction: {},
|
||||||
default_max_deposit_fee: "TESTKUDOS:2",
|
|
||||||
default_max_wire_fee: "TESTKUDOS:1",
|
|
||||||
default_pay_delay: {
|
default_pay_delay: {
|
||||||
d_us: 1000 * 1000, //one second
|
d_us: 1000 * 1000, //one second
|
||||||
},
|
},
|
||||||
default_wire_fee_amortization: 1,
|
|
||||||
default_wire_transfer_delay: {
|
default_wire_transfer_delay: {
|
||||||
d_us: 1000 * 1000, //one second
|
d_us: 1000 * 1000, //one second
|
||||||
},
|
},
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Amounts } from "@gnu-taler/taler-util";
|
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
@ -29,10 +28,8 @@ import {
|
|||||||
FormProvider,
|
FormProvider,
|
||||||
} from "../../../components/form/FormProvider.js";
|
} from "../../../components/form/FormProvider.js";
|
||||||
import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
|
import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
|
||||||
import { UpdateTokenModal } from "../../../components/modal/index.js";
|
|
||||||
import { useInstanceContext } from "../../../context/instance.js";
|
import { useInstanceContext } from "../../../context/instance.js";
|
||||||
import { MerchantBackend } from "../../../declaration.js";
|
import { MerchantBackend } from "../../../declaration.js";
|
||||||
import { PAYTO_REGEX } from "../../../utils/constants.js";
|
|
||||||
import { undefinedIfEmpty } from "../../../utils/table.js";
|
import { undefinedIfEmpty } from "../../../utils/table.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {
|
type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {
|
||||||
@ -53,23 +50,23 @@ interface Props {
|
|||||||
function convert(
|
function convert(
|
||||||
from: MerchantBackend.Instances.QueryInstancesResponse,
|
from: MerchantBackend.Instances.QueryInstancesResponse,
|
||||||
): Entity {
|
): Entity {
|
||||||
const { accounts: qAccounts, ...rest } = from;
|
const { ...rest } = from;
|
||||||
const accounts = qAccounts
|
// const accounts = qAccounts
|
||||||
.filter((a) => a.active)
|
// .filter((a) => a.active)
|
||||||
.map(
|
// .map(
|
||||||
(a) =>
|
// (a) =>
|
||||||
({
|
// ({
|
||||||
payto_uri: a.payto_uri,
|
// payto_uri: a.payto_uri,
|
||||||
credit_facade_url: a.credit_facade_url,
|
// credit_facade_url: a.credit_facade_url,
|
||||||
credit_facade_credentials: a.credit_facade_credentials,
|
// credit_facade_credentials: a.credit_facade_credentials,
|
||||||
} as MerchantBackend.Instances.MerchantBankAccount),
|
// } as MerchantBackend.Instances.MerchantBankAccount),
|
||||||
);
|
// );
|
||||||
const defaults = {
|
const defaults = {
|
||||||
default_wire_fee_amortization: 1,
|
use_stefan: false,
|
||||||
default_pay_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 }, //two hours
|
default_pay_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 }, //two hours
|
||||||
default_wire_transfer_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 * 2 }, //two hours
|
default_wire_transfer_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 * 2 }, //two hours
|
||||||
};
|
};
|
||||||
return { ...defaults, ...rest, accounts };
|
return { ...defaults, ...rest };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTokenValuePart(t?: string): string | undefined {
|
function getTokenValuePart(t?: string): string | undefined {
|
||||||
@ -85,21 +82,21 @@ export function UpdatePage({
|
|||||||
selected,
|
selected,
|
||||||
onBack,
|
onBack,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const { id, token } = useInstanceContext();
|
const { id } = useInstanceContext();
|
||||||
const currentTokenValue = getTokenValuePart(token);
|
// const currentTokenValue = getTokenValuePart(token);
|
||||||
|
|
||||||
function updateToken(token: string | undefined | null) {
|
// function updateToken(token: string | undefined | null) {
|
||||||
const value =
|
// const value =
|
||||||
token && token.startsWith("secret-token:")
|
// token && token.startsWith("secret-token:")
|
||||||
? token.substring("secret-token:".length)
|
// ? token.substring("secret-token:".length)
|
||||||
: token;
|
// : token;
|
||||||
|
|
||||||
if (!token) {
|
// if (!token) {
|
||||||
onChangeAuth({ method: "external" });
|
// onChangeAuth({ method: "external" });
|
||||||
} else {
|
// } else {
|
||||||
onChangeAuth({ method: "token", token: `secret-token:${value}` });
|
// onChangeAuth({ method: "token", token: `secret-token:${value}` });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
|
const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
|
||||||
|
|
||||||
@ -110,35 +107,7 @@ export function UpdatePage({
|
|||||||
user_type: !value.user_type
|
user_type: !value.user_type
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: value.user_type !== "business" && value.user_type !== "individual"
|
: value.user_type !== "business" && value.user_type !== "individual"
|
||||||
? i18n.str`should be business or individual`
|
? i18n.str`should be business or individual`
|
||||||
: undefined,
|
|
||||||
accounts:
|
|
||||||
!value.accounts || !value.accounts.length
|
|
||||||
? i18n.str`required`
|
|
||||||
: undefinedIfEmpty(
|
|
||||||
value.accounts.map((p) => {
|
|
||||||
return !PAYTO_REGEX.test(p.payto_uri)
|
|
||||||
? i18n.str`is not valid`
|
|
||||||
: undefined;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
default_max_deposit_fee: !value.default_max_deposit_fee
|
|
||||||
? i18n.str`required`
|
|
||||||
: !Amounts.parse(value.default_max_deposit_fee)
|
|
||||||
? i18n.str`invalid format`
|
|
||||||
: undefined,
|
|
||||||
default_max_wire_fee: !value.default_max_wire_fee
|
|
||||||
? i18n.str`required`
|
|
||||||
: !Amounts.parse(value.default_max_wire_fee)
|
|
||||||
? i18n.str`invalid format`
|
|
||||||
: undefined,
|
|
||||||
default_wire_fee_amortization:
|
|
||||||
value.default_wire_fee_amortization === undefined
|
|
||||||
? i18n.str`required`
|
|
||||||
: isNaN(value.default_wire_fee_amortization)
|
|
||||||
? i18n.str`is not a number`
|
|
||||||
: value.default_wire_fee_amortization < 1
|
|
||||||
? i18n.str`must be 1 or greater`
|
|
||||||
: undefined,
|
: undefined,
|
||||||
default_pay_delay: !value.default_pay_delay
|
default_pay_delay: !value.default_pay_delay
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
@ -163,10 +132,11 @@ export function UpdatePage({
|
|||||||
const hasErrors = Object.keys(errors).some(
|
const hasErrors = Object.keys(errors).some(
|
||||||
(k) => (errors as any)[k] !== undefined,
|
(k) => (errors as any)[k] !== undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const submit = async (): Promise<void> => {
|
const submit = async (): Promise<void> => {
|
||||||
await onUpdate(value as Entity);
|
await onUpdate(value as Entity);
|
||||||
};
|
};
|
||||||
const [active, setActive] = useState(false);
|
// const [active, setActive] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -181,7 +151,7 @@ export function UpdatePage({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right">
|
{/* <div class="level-right">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
<button
|
<button
|
||||||
@ -200,33 +170,11 @@ export function UpdatePage({
|
|||||||
</button>
|
</button>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column" />
|
|
||||||
<div class="column is-four-fifths">
|
|
||||||
{active && (
|
|
||||||
<UpdateTokenModal
|
|
||||||
oldToken={currentTokenValue}
|
|
||||||
onCancel={() => {
|
|
||||||
setActive(false);
|
|
||||||
}}
|
|
||||||
onClear={() => {
|
|
||||||
updateToken(null);
|
|
||||||
setActive(false);
|
|
||||||
}}
|
|
||||||
onConfirm={(newToken) => {
|
|
||||||
updateToken(newToken);
|
|
||||||
setActive(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="column" />
|
|
||||||
</div>
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021-2023 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { h, VNode, FunctionalComponent } from "preact";
|
||||||
|
import { CreatePage as TestedComponent } from "./CreatePage.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Pages/Validators/Create",
|
||||||
|
component: TestedComponent,
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user