diff --git a/Makefile b/Makefile
index 45deb0d2c..7b4003b74 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,11 @@ git-archive-all = ./build-system/taler-build-scripts/archive-with-submodules/git
include .config.mk
+# Let recursive Makefiles know that they're being invoked
+# from the top-level makefile.
+export TOPLEVEL := yes
+export TOP_DESTDIR := $(abspath $(DESTDIR))
+
.PHONY: compile
compile:
pnpm install -r --frozen-lockfile
@@ -121,18 +126,18 @@ lint:
install:
pnpm install --frozen-lockfile
pnpm run compile
- make -C packages/taler-wallet-cli TOPLEVEL=yes install-nodeps
- make -C packages/anastasis-cli TOPLEVEL=yes install-nodeps
- make -C packages/taler-harness TOPLEVEL=yes install-nodeps
- make -C packages/demobank-ui TOPLEVEL=yes install-nodeps
- make -C packages/merchant-backoffice-ui TOPLEVEL=yes install-nodeps
- make -C packages/aml-backoffice-ui TOPLEVEL=yes install-nodeps
+ $(MAKE) -C packages/taler-wallet-cli install-nodeps
+ $(MAKE) -C packages/anastasis-cli install-nodeps
+ $(MAKE) -C packages/taler-harness install-nodeps
+ $(MAKE) -C packages/demobank-ui install-nodeps
+ $(MAKE) -C packages/merchant-backoffice-ui install-nodeps
+ $(MAKE) -C packages/aml-backoffice-ui install-nodeps
.PHONY: install-tools
# Install taler-wallet-cli, anastasis-cli and taler-harness
install-tools:
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/anastasis-cli... --filter @gnu-taler/taler-harness...
pnpm run --filter @gnu-taler/taler-wallet-cli... --filter @gnu-taler/anastasis-cli... --filter @gnu-taler/taler-harness... compile
- make -C packages/taler-wallet-cli TOPLEVEL=yes install-nodeps
- make -C packages/anastasis-cli TOPLEVEL=yes install-nodeps
- make -C packages/taler-harness TOPLEVEL=yes install-nodeps
+ $(MAKE) -C packages/taler-wallet-cli install-nodeps
+ $(MAKE) -C packages/anastasis-cli install-nodeps
+ $(MAKE) -C packages/taler-harness install-nodeps
diff --git a/contrib/copy-backend-into-prebuilt.sh b/contrib/copy-backend-into-prebuilt.sh
index b9fa2a68e..383871ac6 100755
--- a/contrib/copy-backend-into-prebuilt.sh
+++ b/contrib/copy-backend-into-prebuilt.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-[ ! -d prebuilt ] && echo 'directory "prebuilt" not found. first checkout the prebuilt branch into a prebuilt directory' && exit 1
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
for file in depleted_tip.en.html offer_refund.en.html offer_tip.en.html request_payment.en.html show_order_details.en.html; do
cp packages/merchant-backend-ui/dist/pages/$file prebuilt/backend/
diff --git a/contrib/copy-backoffice-into-prebuilt.sh b/contrib/copy-backoffice-into-prebuilt.sh
index aecebf01b..d21b91096 100755
--- a/contrib/copy-backoffice-into-prebuilt.sh
+++ b/contrib/copy-backoffice-into-prebuilt.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-[ ! -d prebuilt ] && echo 'directory "prebuilt" not found. first checkout the prebuilt branch into a prebuilt directory' && exit 1
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
find packages/merchant-backoffice-ui/dist/prod/ -type f -printf '%P\n' | sort > prebuilt/backoffice/bof
diff --git a/contrib/copy-demobank-into-prebuilt.sh b/contrib/copy-demobank-into-prebuilt.sh
index f5292a767..755b66150 100755
--- a/contrib/copy-demobank-into-prebuilt.sh
+++ b/contrib/copy-demobank-into-prebuilt.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-[ ! -d prebuilt ] && echo 'directory "prebuilt" not found. first checkout the prebuilt branch into a prebuilt directory' && exit 1
+[ ! -d prebuilt ] && git worktree add -f prebuilt prebuilt && exit 1
find packages/demobank-ui/dist/prod/ -type f -printf '%P\n' | sort > prebuilt/demobank/bof
diff --git a/packages/aml-backoffice-ui/Makefile b/packages/aml-backoffice-ui/Makefile
index 2653ce92f..64f9f83d1 100644
--- a/packages/aml-backoffice-ui/Makefile
+++ b/packages/aml-backoffice-ui/Makefile
@@ -3,6 +3,7 @@
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
else
$(info package-level build)
-include ../../.config.mk
@@ -15,7 +16,7 @@ $(info prefix is $(prefix))
all:
@echo run \'make install\' to install
-spa_dir=$(prefix)/share/taler/aml-backoffice-ui
+spa_dir=$(DESTDIR)$(prefix)/share/taler/aml-backoffice-ui
.PHONY: install-nodeps
install-nodeps:
diff --git a/packages/anastasis-cli/Makefile b/packages/anastasis-cli/Makefile
index 292f7000f..724a5e40d 100644
--- a/packages/anastasis-cli/Makefile
+++ b/packages/anastasis-cli/Makefile
@@ -3,6 +3,7 @@
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
else
$(info package-level build)
-include ../../.config.mk
@@ -20,19 +21,19 @@ warn-noprefix:
@echo "no prefix configured, did you run ./configure?"
install: warn-noprefix
else
-install_target = $(prefix)/lib/anastasis-cli
+bindir = $(prefix)/bin
+libdir = $(prefix)/lib/anastasis-cli
+nodedir = $(libdir)/node_modules/anastasis-cli
.PHONY: install install-nodeps deps
install-nodeps:
./build-node.mjs
- install -d $(prefix)/bin
- install -d $(install_target)/bin
- install -d $(install_target)/node_modules/anastasis-cli
- install -d $(install_target)/node_modules/anastasis-cli/bin
- install -d $(install_target)/node_modules/anastasis-cli/dist
- install ./dist/anastasis-cli-bundled.cjs $(install_target)/node_modules/anastasis-cli/dist/
- install ./dist/anastasis-cli-bundled.cjs.map $(install_target)/node_modules/anastasis-cli/dist/
- install ./bin/anastasis-cli.mjs $(install_target)/node_modules/anastasis-cli/bin/
- ln -sf $(install_target)/node_modules/anastasis-cli/bin/anastasis-cli.mjs $(prefix)/bin/anastasis-cli
+ install -d $(DESTDIR)$(bindir)
+ install -d $(DESTDIR)$(nodedir)/bin
+ install -d $(DESTDIR)$(nodedir)/dist
+ install ./dist/anastasis-cli-bundled.cjs $(DESTDIR)$(nodedir)/dist/
+ install ./dist/anastasis-cli-bundled.cjs.map $(DESTDIR)$(nodedir)/dist/
+ install ./bin/anastasis-cli.mjs $(DESTDIR)$(nodedir)/bin/
+ ln -sf ../lib/anastasis-cli/node_modules/anastasis-cli/bin/anastasis-cli.mjs $(DESTDIR)$(bindir)/anastasis-cli
deps:
pnpm install --frozen-lockfile --filter @gnu-taler/anastasis-cli...
install:
diff --git a/packages/demobank-ui/Makefile b/packages/demobank-ui/Makefile
index 8e41cc7c6..2399cc427 100644
--- a/packages/demobank-ui/Makefile
+++ b/packages/demobank-ui/Makefile
@@ -3,6 +3,7 @@
ifeq ($(TOPLEVEL), yes)
$(info top-level build)
-include ../../.config.mk
+ override DESTDIR := $(TOP_DESTDIR)
else
$(info package-level build)
-include ../../.config.mk
@@ -15,7 +16,7 @@ $(info prefix is $(prefix))
all:
@echo run \'make install\' to install
-spa_dir=$(prefix)/share/taler/demobank-ui
+spa_dir=$(DESTDIR)$(prefix)/share/taler/demobank-ui
.PHONY: deps
deps:
diff --git a/packages/demobank-ui/README.md b/packages/demobank-ui/README.md
index 1732b5f38..877799748 100644
--- a/packages/demobank-ui/README.md
+++ b/packages/demobank-ui/README.md
@@ -36,11 +36,18 @@ to the default settings:
```
globalThis.talerDemobankSettings = {
- backendBaseURL: "https://bank.demo.taler.net/demobanks/default/",
+ // location of libeufin server
+ backendBaseURL: "https://bank.demo.taler.net/",
allowRegistrations: true,
bankName: "Taler Bank",
// Show explainer text and navbar to other demo sites
showDemoNav: true,
+ // href value of the icon in the top left
+ iconLinkURL: "https://demo.taler.net/",
+ // show the button "create random user" in registration form
+ allowRandomAccountCreation: true,
+ // do not create random password for random users
+ simplePasswordForRandomAccounts: true,
// Names and links for other demo sites to show in the navbar
demoSites: [
["Landing", "https://demo.taler.net/"],
diff --git a/packages/demobank-ui/build.mjs b/packages/demobank-ui/build.mjs
index 22b91803a..64ddc3774 100755
--- a/packages/demobank-ui/build.mjs
+++ b/packages/demobank-ui/build.mjs
@@ -21,8 +21,8 @@ await build({
type: "production",
source: {
js: ["src/index.tsx"],
- assets: [{base:"src",files:["src/index.html"]}],
+ assets: [{ base: "src", files: ["src/index.html"] }],
},
destination: "./dist/prod",
- css: "sass",
+ css: "postcss",
});
diff --git a/packages/demobank-ui/dev.mjs b/packages/demobank-ui/dev.mjs
index 8b870451b..f29a05e49 100755
--- a/packages/demobank-ui/dev.mjs
+++ b/packages/demobank-ui/dev.mjs
@@ -18,17 +18,17 @@
import { serve } from "@gnu-taler/web-util/node";
import { initializeDev } from "@gnu-taler/web-util/build";
-const devEntryPoints = ["src/stories.tsx", "src/index.tsx"];
+const devEntryPoints = ["src/stories.tsx", "src/index.tsx", "src/demobank-ui-settings.js"];
const build = initializeDev({
type: "development",
source: {
js: devEntryPoints,
- assets: [{base:"src",files:["src/index.html"]}],
+ assets: [{ base: "src", files: ["src/index.html"] }],
},
destination: "./dist/dev",
public: "/app",
- css: "sass",
+ css: "postcss",
});
await build();
diff --git a/packages/demobank-ui/package.json b/packages/demobank-ui/package.json
index 8b999aeed..17059afeb 100644
--- a/packages/demobank-ui/package.json
+++ b/packages/demobank-ui/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/demobank-ui",
- "version": "0.1.0",
+ "version": "0.9.3-dev.27",
"license": "AGPL-3.0-OR-LATER",
"type": "module",
"scripts": {
@@ -46,6 +46,9 @@
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
"@gnu-taler/pogen": "^0.0.5",
+ "@tailwindcss/forms": "^0.5.3",
+ "@tailwindcss/typography": "^0.5.9",
+ "autoprefixer": "^10.4.14",
"@types/chai": "^4.3.0",
"@types/history": "^4.7.8",
"@types/mocha": "^10.0.1",
@@ -62,6 +65,7 @@
"po2json": "^0.4.5",
"preact-render-to-string": "^5.2.6",
"sass": "1.56.1",
+ "tailwindcss": "^3.3.2",
"typescript": "5.2.2"
},
"pogen": {
diff --git a/packages/demobank-ui/postcss.config.js b/packages/demobank-ui/postcss.config.js
new file mode 100644
index 000000000..2e7af2b7f
--- /dev/null
+++ b/packages/demobank-ui/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/packages/demobank-ui/src/assets/logo-2021.svg b/packages/demobank-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/demobank-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/demobank-ui/src/components/Attention.tsx b/packages/demobank-ui/src/components/Attention.tsx
new file mode 100644
index 000000000..3313e5796
--- /dev/null
+++ b/packages/demobank-ui/src/components/Attention.tsx
@@ -0,0 +1,59 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { assertUnreachable } from "./Routing.js";
+
+interface Props {
+ type?: "info" | "success" | "warning" | "danger",
+ onClose?: () => void,
+ title: TranslatedString,
+ children?: ComponentChildren ,
+}
+export function Attention({ type = "info", title, children, onClose }: Props): VNode {
+ return
+
+
+
+
+ {(() => {
+ switch (type) {
+ case "info":
+ return
+ case "warning":
+ return
+ case "danger":
+ return
+ case "success":
+ return
+ default:
+ assertUnreachable(type)
+ }
+ })()}
+
+
+
+
+ {title}
+
+
+ {children}
+
+
+ {onClose &&
+
+
{
+ e.preventDefault();
+ onClose();
+ }}
+ >
+
+
+
+
+
+ }
+
+
+
+
+}
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx
index 4b7649fb6..a32deb266 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -19,6 +19,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State } from "./index.js";
import { format } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
@@ -62,8 +63,8 @@ export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
? format(item.confirmation_time, "dd/MM/yyyy HH:mm:ss")
: "-"}
- {Amounts.stringifyValue(item.amount_debit)}
- {Amounts.stringifyValue(item.amount_credit)}
+
+
{item.status}
+
+
+
+ )
+};
+
+export function CopiedIcon(): VNode {
+ return (
+
+
+
+ )
+};
+
+export function CopyButton({ getContent }: { getContent: () => string }): VNode {
+ const [copied, setCopied] = useState(false);
+ function copyText(): void {
+ navigator.clipboard.writeText(getContent() || "");
+ setCopied(true);
+ }
+ useEffect(() => {
+ if (copied) {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }
+ }, [copied]);
+
+ if (!copied) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/demobank-ui/src/scss/_misc.scss b/packages/demobank-ui/src/components/ErrorLoading.tsx
similarity index 52%
rename from packages/demobank-ui/src/scss/_misc.scss
rename to packages/demobank-ui/src/components/ErrorLoading.tsx
index 65bd28dbd..ee62671ce 100644
--- a/packages/demobank-ui/src/scss/_misc.scss
+++ b/packages/demobank-ui/src/components/ErrorLoading.tsx
@@ -1,6 +1,7 @@
+/*
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,37 +15,15 @@
GNU Taler; see the file COPYING. If not, see
*/
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { h, VNode } from "preact";
+import { Attention } from "./Attention.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
-.is-user-avatar {
- &.has-max-width {
- max-width: $size-base * 7;
- }
-
- &.is-aligned-center {
- margin: 0 auto;
- }
-
- img {
- margin: 0 auto;
- border-radius: $radius-rounded;
- }
-}
-
-.icon.has-update-mark {
- position: relative;
-
- &:after {
- content: "";
- width: $icon-update-mark-size;
- height: $icon-update-mark-size;
- position: absolute;
- top: 1px;
- right: 1px;
- background-color: $icon-update-mark-color;
- border-radius: $radius-rounded;
- }
+export function ErrorLoading({ error }: { error: HttpError }): VNode {
+ const { i18n } = useTranslationContext()
+ return (
+ Got status "{error.info.status}" on {error.info.url}
+
+ );
}
diff --git a/packages/demobank-ui/src/components/LangSelector.tsx b/packages/demobank-ui/src/components/LangSelector.tsx
index ca4411682..c1d0f64ef 100644
--- a/packages/demobank-ui/src/components/LangSelector.tsx
+++ b/packages/demobank-ui/src/components/LangSelector.tsx
@@ -42,11 +42,11 @@ function getLangName(s: keyof LangsNames | string): string {
return String(s);
}
-// FIXME: explain "like py".
-export function LangSelectorLikePy(): VNode {
+export function LangSelector(): VNode {
const [updatingLang, setUpdatingLang] = useState(false);
const { lang, changeLanguage } = useTranslationContext();
const [hidden, setHidden] = useState(true);
+
useEffect(() => {
function bodyKeyPress(event: KeyboardEvent) {
if (event.code === "Escape") setHidden(true);
@@ -62,51 +62,49 @@ export function LangSelectorLikePy(): VNode {
};
}, []);
return (
-
- {
- ev.preventDefault();
- setHidden((h) => !h);
- ev.stopPropagation();
- }}
- >
- {getLangName(lang)}
-
-
-
);
}
diff --git a/packages/demobank-ui/src/components/QR.tsx b/packages/demobank-ui/src/components/QR.tsx
index c1c159ef8..945a08867 100644
--- a/packages/demobank-ui/src/components/QR.tsx
+++ b/packages/demobank-ui/src/components/QR.tsx
@@ -33,7 +33,6 @@ export function QR({ text }: { text: string }): VNode {
return (
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { createHashHistory } from "history";
+import { Fragment, VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
+import { useEffect } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { BankFrame } from "../pages/BankFrame.js";
+import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js";
+import { LoginForm } from "../pages/LoginForm.js";
+import { PublicHistoriesPage } from "../pages/PublicHistoriesPage.js";
+import { RegistrationPage } from "../pages/RegistrationPage.js";
+import { AdminHome } from "../pages/admin/Home.js";
+import { BusinessAccount } from "../pages/business/Home.js";
+import { bankUiSettings } from "../settings.js";
+
+export function Routing(): VNode {
+ const history = createHashHistory();
+ const backend = useBackendContext();
+ const {i18n} = useTranslationContext();
+
+ if (backend.state.status === "loggedOut") {
+ return
+
+ (
+
+
+
{i18n.str`Welcome to ${bankUiSettings.bankName}!`}
+
+
+ {
+ route("/register");
+ }}
+ />
+
+ )}
+ />
+ }
+ />
+ (
+ {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ {bankUiSettings.allowRegistrations &&
+ (
+ {
+ route("/account");
+ }}
+ onCancel={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ }
+
+
+
+ }
+ const { isUserAdministrator, username } = backend.state
+
+ return (
+
+
+ (
+ {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ }
+ />
+ {
+ if (isUserAdministrator) {
+ return {
+ route("/register");
+ }}
+ />;
+ } else {
+ return {
+ route(`/operation/${wopid}`);
+ }}
+ goToBusinessAccount={() => {
+ route("/business");
+ }}
+ onRegister={() => {
+ route("/register");
+ }}
+ />
+ }
+ }}
+ />
+ (
+ {
+ route("/account");
+ }}
+ onRegister={() => {
+ route("/register");
+ }}
+ onLoadNotOk={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+
+
+
+ );
+}
+
+function Redirect({ to }: { to: string }): VNode {
+ useEffect(() => {
+ route(to, true);
+ }, []);
+ return
being redirected to {to}
;
+}
+
+export function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
diff --git a/packages/demobank-ui/src/pages/ShowInputErrorLabel.tsx b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
similarity index 87%
rename from packages/demobank-ui/src/pages/ShowInputErrorLabel.tsx
rename to packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
index dacffe20a..c5840cad9 100644
--- a/packages/demobank-ui/src/pages/ShowInputErrorLabel.tsx
+++ b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
@@ -24,6 +24,6 @@ export function ShowInputErrorLabel({
isDirty: boolean;
}): VNode {
if (message && isDirty)
- return
{message}
;
- return
;
+ return
{message}
;
+ return
;
}
diff --git a/packages/demobank-ui/src/components/Transactions/index.ts b/packages/demobank-ui/src/components/Transactions/index.ts
index 46b38ce74..9df1a70e5 100644
--- a/packages/demobank-ui/src/components/Transactions/index.ts
+++ b/packages/demobank-ui/src/components/Transactions/index.ts
@@ -46,6 +46,8 @@ export namespace State {
status: "ready";
error: undefined;
transactions: Transaction[];
+ onPrev?: () => void;
+ onNext?: () => void;
}
}
diff --git a/packages/demobank-ui/src/components/Transactions/state.ts b/packages/demobank-ui/src/components/Transactions/state.ts
index 09c039055..4b62b005e 100644
--- a/packages/demobank-ui/src/components/Transactions/state.ts
+++ b/packages/demobank-ui/src/components/Transactions/state.ts
@@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see
*/
-import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
+import { AbsoluteTime, Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
import { useTransactions } from "../../hooks/access.js";
import { Props, State, Transaction } from "./index.js";
@@ -34,45 +34,19 @@ export function useComponentState({ account }: Props): State {
}
const transactions = result.data.transactions
- .map((item: unknown) => {
- if (
- !item ||
- typeof item !== "object" ||
- !("direction" in item) ||
- !("creditorIban" in item) ||
- !("debtorIban" in item) ||
- !("date" in item) ||
- !("subject" in item) ||
- !("currency" in item) ||
- !("amount" in item)
- ) {
- //not valid
- return;
- }
- const anyItem = item as any;
- if (
- !(typeof anyItem.creditorIban === "string") ||
- !(typeof anyItem.debtorIban === "string") ||
- !(typeof anyItem.date === "string") ||
- !(typeof anyItem.subject === "string") ||
- !(typeof anyItem.currency === "string") ||
- !(typeof anyItem.amount === "string")
- ) {
- return;
- }
+ .map((tx) => {
- const negative = anyItem.direction === "DBIT";
- const counterpart = negative ? anyItem.creditorIban : anyItem.debtorIban;
+ const negative = tx.direction === "debit";
+ const cp = parsePaytoUri(negative ? tx.creditor_payto_uri : tx.debtor_payto_uri);
+ const counterpart = (cp === undefined || !cp.isKnown ? undefined :
+ cp.targetType === "iban" ? cp.iban :
+ cp.targetType === "x-taler-bank" ? cp.account :
+ cp.targetType === "bitcoin" ? `${cp.targetPath.substring(0, 6)}...` : undefined) ??
+ "unkown";
- let date = anyItem.date ? parseInt(anyItem.date, 10) : 0;
- if (isNaN(date) || !isFinite(date)) {
- date = 0;
- }
- const when: AbsoluteTime = !date
- ? AbsoluteTime.never()
- : AbsoluteTime.fromMilliseconds(date);
- const amount = Amounts.parse(`${anyItem.currency}:${anyItem.amount}`);
- const subject = anyItem.subject;
+ const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
+ const amount = Amounts.parse(tx.amount);
+ const subject = tx.subject;
return {
negative,
counterpart,
@@ -87,5 +61,7 @@ export function useComponentState({ account }: Props): State {
status: "ready",
error: undefined,
transactions,
+ onNext: result.isReachingEnd ? undefined : result.loadMore,
+ onPrev: result.isReachingStart ? undefined : result.loadMorePrev,
};
}
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx
index 34d078c16..696fb59f3 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Transactions/views.tsx
@@ -14,11 +14,13 @@
GNU Taler; see the file COPYING. If not, see
*/
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { State } from "./index.js";
-import { format } from "date-fns";
+import { format, isToday } from "date-fns";
import { Amounts } from "@gnu-taler/taler-util";
+import { useEffect, useRef } from "preact/hooks";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
export function LoadingUriView({ error }: State.LoadingUriError): VNode {
const { i18n } = useTranslationContext();
@@ -30,45 +32,104 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
);
}
-export function ReadyView({ transactions }: State.Ready): VNode {
+export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode {
const { i18n } = useTranslationContext();
+ if (!transactions.length) return
+ const txByDate = transactions.reduce((prev, cur) => {
+ const d = cur.when.t_ms === "never"
+ ? ""
+ : format(cur.when.t_ms, "dd/MM/yyyy")
+ if (!prev[d]) {
+ prev[d] = []
+ }
+ prev[d].push(cur)
+ return prev
+ }, {} as Record
)
return (
-
-
-
-
- {i18n.str`Date`}
- {i18n.str`Amount`}
- {i18n.str`Counterpart`}
- {i18n.str`Subject`}
-
-
-
- {transactions.map((item, idx) => {
- return (
-
-
- {item.when.t_ms === "never"
- ? ""
- : format(item.when.t_ms, "dd/MM/yyyy HH:mm:ss")}
-
-
- {item.negative ? "-" : ""}
- {item.amount ? (
- `${Amounts.stringifyValue(item.amount)} ${
- item.amount.currency
- }`
- ) : (
- <invalid value>
- )}
-
- {item.counterpart}
- {item.subject}
-
- );
- })}
-
-
+
+
+
+
Latest transactions
+
+
+
+
+
+
+ {i18n.str`Date`}
+ {i18n.str`Amount`}
+ {i18n.str`Counterpart`}
+ {i18n.str`Subject`}
+
+
+
+ {Object.entries(txByDate).map(([date, txs], idx) => {
+ return
+
+
+ {date}
+
+
+ {txs.map(item => {
+ const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss")
+ const amount =
+ { }
+
+ return (
+
+ {time}
+
+ Amount
+
+ {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? (
+
+ ) : (
+ <{i18n.str`invalid value`}>
+ )}
+
+ Counterpart
+
+ {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart}
+
+
+
+
+ {item.amount ? (
+ ) : (
+ <{i18n.str`invalid value`}>
+ )}
+
+ {item.counterpart}
+ {item.subject}
+ )
+ })}
+
+
+ })}
+
+
+
+
+
+
+
+ First page
+
+
+ Next
+
+
+
+
);
}
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx
index ea86da518..7cf658681 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -15,16 +15,23 @@
*/
import {
+ LibtoolVersion,
getGlobalLogLevel,
setGlobalLogLevelFromString,
} from "@gnu-taler/taler-util";
-import { TranslationProvider } from "@gnu-taler/web-util/browser";
-import { FunctionalComponent, h } from "preact";
+import { TranslationProvider, useApiContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, FunctionalComponent, VNode, h } from "preact";
import { SWRConfig } from "swr";
-import { BackendStateProvider } from "../context/backend.js";
+import { BackendStateProvider, useBackendContext } from "../context/backend.js";
import { strings } from "../i18n/strings.js";
-import { Routing } from "../pages/Routing.js";
-
+import { Routing } from "./Routing.js";
+import { useEffect, useState } from "preact/hooks";
+import { Loading } from "./Loading.js";
+import { getInitialBackendBaseURL } from "../hooks/backend.js";
+import { BANK_INTEGRATION_PROTOCOL_VERSION, useConfigState } from "../hooks/config.js";
+import { ErrorLoading } from "./ErrorLoading.js";
+import { BankFrame } from "../pages/BankFrame.js";
+import { ConfigStateProvider } from "../context/config.js";
const WITH_LOCAL_STORAGE_CACHE = false;
/**
@@ -48,22 +55,44 @@ const App: FunctionalComponent = () => {
return (
-
-
-
+
+
+
+
+
-
+
);
};
(window as any).setGlobalLogLevelFromString = setGlobalLogLevelFromString;
(window as any).getGlobalLevel = getGlobalLogLevel;
+function VersionCheck({ children }: { children: ComponentChildren }): VNode {
+ const checked = useConfigState()
+
+ if (checked === undefined) {
+ return
+ }
+ if (checked.type === "wrong") {
+ return
+ the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}"
+
+ }
+ if (checked.type === "ok") {
+ return
{children}
+ }
+
+ return
+
+
+}
+
function localStorageProvider(): Map
{
const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
diff --git a/packages/demobank-ui/src/context/backend.ts b/packages/demobank-ui/src/context/backend.ts
index b311ddbb0..eae187c6d 100644
--- a/packages/demobank-ui/src/context/backend.ts
+++ b/packages/demobank-ui/src/context/backend.ts
@@ -34,6 +34,9 @@ const initial: Type = {
logOut() {
null;
},
+ expired() {
+ null;
+ },
logIn(info) {
null;
},
@@ -65,6 +68,7 @@ export const BackendStateProviderTesting = ({
const value: BackendStateHandler = {
state,
logIn: () => {},
+ expired: () => {},
logOut: () => {},
};
diff --git a/packages/demobank-ui/src/scss/_title-bar.scss b/packages/demobank-ui/src/context/config.ts
similarity index 52%
rename from packages/demobank-ui/src/scss/_title-bar.scss
rename to packages/demobank-ui/src/context/config.ts
index 932f8e65d..a2cde18eb 100644
--- a/packages/demobank-ui/src/scss/_title-bar.scss
+++ b/packages/demobank-ui/src/context/config.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -14,37 +14,39 @@
GNU Taler; see the file COPYING. If not, see
*/
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-section.section.is-title-bar {
- padding: $default-padding;
- border-bottom: $light-border;
+export type Type = Required;
- ul {
- li {
- display: inline-block;
- padding: 0 $default-padding * 0.5 0 0;
- font-size: $default-padding;
- color: $title-bar-color;
+const initial: Type = {
+ name: "",
+ version: "0:0:0",
+ currency_fraction_digits: 2,
+ currency_fraction_limit: 2,
+ fiat_currency: "",
+ have_cashout: false,
+};
+const Context = createContext(initial);
- &:after {
- display: inline-block;
- content: "/";
- padding-left: $default-padding * 0.5;
- }
+export const useConfigContext = (): Type => useContext(Context);
- &:last-child {
- padding-right: 0;
- font-weight: 900;
- color: $title-bar-active-color;
+export const ConfigStateProvider = ({
+ value,
+ children,
+}: {
+ value: Type,
+ children: ComponentChildren;
+}): VNode => {
+
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
- &:after {
- display: none;
- }
- }
- }
- }
-}
diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts
index 462287c59..5c55cfade 100644
--- a/packages/demobank-ui/src/declaration.d.ts
+++ b/packages/demobank-ui/src/declaration.d.ts
@@ -74,7 +74,9 @@ type HashCode = string;
type EddsaPublicKey = string;
type EddsaSignature = string;
type WireTransferIdentifierRawP = string;
-type RelativeTime = Duration;
+type RelativeTime = {
+ d_us: number | "forever"
+};
type ImageDataUrl = string;
interface WithId {
@@ -99,20 +101,33 @@ type Amount = string;
type UUID = string;
type Integer = number;
-interface Balance {
- amount: Amount;
- credit_debit_indicator: "credit" | "debit";
-}
-
namespace SandboxBackend {
export interface Config {
// Name of this API, always "circuit".
name: string;
// API version in the form $n:$n:$n
version: string;
- // Contains ratios and fees related to buying
- // and selling the circuit currency.
- ratios_and_fees: RatiosAndFees;
+ // If 'true', the server provides local currency
+ // conversion support.
+ // If missing or false, some parts of the API
+ // are not supported and return 404.
+ have_cashout?: boolean;
+
+ // Fiat currency. That is the currency in which
+ // cash-out operations ultimately wire money.
+ // Only applicable if have_cashout=true.
+ fiat_currency?: string;
+
+ // How many digits should the amounts be rendered
+ // with by default. Small capitals should
+ // be used to render fractions beyond the number
+ // given here (like on gas stations).
+ currency_fraction_digits?: number;
+
+ // How many decimal digits an operation can
+ // have. Wire transfers with more decimal
+ // digits will not be accepted.
+ currency_fraction_limit?: number;
}
interface RatiosAndFees {
// Exchange rate to buy the circuit currency from fiat.
@@ -126,7 +141,7 @@ namespace SandboxBackend {
}
export interface SandboxError {
- error: SandboxErrorDetail;
+ error?: SandboxErrorDetail;
}
interface SandboxErrorDetail {
// String enum classifying the error.
@@ -152,26 +167,12 @@ namespace SandboxBackend {
UtilError = "util-error",
}
- namespace Access {
- interface PublicAccountsResponse {
- publicAccounts: PublicAccount[];
- }
- interface PublicAccount {
- iban: string;
- balance: string;
- // The account name _and_ the username of the
- // Sandbox customer that owns such a bank account.
- accountLabel: string;
- }
- interface BankAccountBalanceResponse {
- // Available balance on the account.
- balance: Balance;
- // payto://-URI of the account. (New)
- paytoUri: string;
- // Number indicating the max debit allowed for the requesting user.
- debitThreshold: Amount;
- }
+ type EmailAddress = string;
+ type PhoneNumber = string;
+
+ namespace CoreBank {
+
interface BankAccountCreateWithdrawalRequest {
// Amount to withdraw.
amount: Amount;
@@ -213,28 +214,24 @@ namespace SandboxBackend {
}
interface BankAccountTransactionInfo {
- creditorIban: string;
- creditorBic: string; // Optional
- creditorName: string;
+ creditor_payto_uri: string;
+ debtor_payto_uri: string;
- debtorIban: string;
- debtorBic: string;
- debtorName: string;
+ amount: Amount;
+ direction: "debit" | "credit";
- amount: number;
- currency: string;
subject: string;
// Transaction unique ID. Matches
// $transaction_id from the URI.
- uid: string;
- direction: "DBIT" | "CRDT";
- date: string; // milliseconds since the Unix epoch
+ row_id: number;
+ date: Timestamp;
}
+
interface CreateBankAccountTransactionCreate {
// Address in the Payto format of the wire transfer receiver.
// It needs at least the 'message' query string parameter.
- paytoUri: string;
+ payto_uri: string;
// Transaction amount (in the $currency:x.y format), optional.
// However, when not given, its value must occupy the 'amount'
@@ -243,11 +240,143 @@ namespace SandboxBackend {
amount?: string;
}
- interface BankRegistrationRequest {
+ interface RegisterAccountRequest {
+ // Username
username: string;
+ // Password.
password: string;
+
+ // Legal name of the account owner
+ name: string;
+
+ // Defaults to false.
+ is_public?: boolean;
+
+ // Is this a taler exchange account?
+ // If true:
+ // - incoming transactions to the account that do not
+ // have a valid reserve public key are automatically
+ // - the account provides the taler-wire-gateway-api endpoints
+ // Defaults to false.
+ is_taler_exchange?: boolean;
+
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ challenge_contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing a bank account
+ // external to the libeufin-bank.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the local currency
+ // back to fiat currency outside libeufin-bank.
+ cashout_payto_uri?: string;
+
+ // Internal payto URI of this bank account.
+ // Used mostly for testing.
+ internal_payto_uri?: string;
}
+ interface ChallengeContactData {
+
+ // E-Mail address
+ email?: EmailAddress;
+
+ // Phone number.
+ phone?: PhoneNumber;
+ }
+
+ interface AccountReconfiguration {
+
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ challenge_contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing a bank account
+ // external to the libeufin-bank.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the local currency
+ // back to fiat currency outside libeufin-bank.
+ cashout_address?: string;
+
+ // Legal name associated with $username.
+ // When missing, the old name is kept.
+ name?: string;
+
+ // If present, change the is_exchange configuration.
+ // See RegisterAccountRequest
+ is_exchange?: boolean;
+ }
+
+
+ interface AccountPasswordChange {
+
+ // New password.
+ new_password: string;
+ }
+ interface PublicAccountsResponse {
+ public_accounts: PublicAccount[];
+ }
+ interface PublicAccount {
+ payto_uri: string;
+
+ balance: Balance;
+
+ // The account name (=username) of the
+ // libeufin-bank account.
+ account_name: string;
+ }
+
+ interface ListBankAccountsResponse {
+ accounts: AccountMinimalData[];
+ }
+ interface Balance {
+ amount: Amount;
+ credit_debit_indicator: "credit" | "debit";
+ }
+ interface AccountMinimalData {
+ // Username
+ username: string;
+
+ // Legal name of the account owner.
+ name: string;
+
+ // current balance of the account
+ balance: Balance;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: Amount;
+ }
+
+ interface AccountData {
+ // Legal name of the account owner.
+ name: string;
+
+ // Available balance on the account.
+ balance: Balance;
+
+ // payto://-URI of the account.
+ payto_uri: string;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: Amount;
+
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing the bank account
+ // where to send cashouts. This field is optional
+ // because not all the accounts are required to participate
+ // in the merchants' circuit. One example is the exchange:
+ // that never cashouts. Registering these accounts can
+ // be done via the access API.
+ cashout_payto_uri?: string;
+ }
+
}
namespace Circuit {
diff --git a/packages/demobank-ui/src/demobank-ui-settings.js b/packages/demobank-ui/src/demobank-ui-settings.js
new file mode 100644
index 000000000..8a0961831
--- /dev/null
+++ b/packages/demobank-ui/src/demobank-ui-settings.js
@@ -0,0 +1,21 @@
+// Values for development environment
+
+/**
+ * Global settings for the demobank UI.
+ */
+localStorage.setItem("bank-base-url", "http://bank.taler.test/");
+
+globalThis.talerDemobankSettings = {
+ backendBaseURL: "http://bank.taler.test/",
+ allowRegistrations: true,
+ showDemoNav: true,
+ simplePasswordForRandomAccounts: true,
+ allowRandomAccountCreation: true,
+ bankName: "Taler DEVELOPMENT Bank",
+ // Names and links for other demo sites to show in the navbar
+ demoSites: [
+ ["Exchange", "https://Exchnage.taler.test/"],
+ ["Bank", "https://bank-ui.taler.test/"],
+ ["Merchant", "https://merchant.taler.test/"],
+ ],
+};
diff --git a/packages/demobank-ui/src/forms/simplest.ts b/packages/demobank-ui/src/forms/simplest.ts
new file mode 100644
index 000000000..54b6b1c65
--- /dev/null
+++ b/packages/demobank-ui/src/forms/simplest.ts
@@ -0,0 +1,66 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString
+} from "@gnu-taler/taler-util";
+import { DoubleColumnForm, FormState } from "@gnu-taler/web-util/browser";
+
+export namespace Data {
+ export interface WithResolution {
+ when: AbsoluteTime;
+ threshold: AmountJson;
+ state: string;
+ }
+ export interface Form extends WithResolution {
+ comment: string;
+ }
+}
+
+const design: DoubleColumnForm = [
+ {
+ title: "Simple form" as TranslatedString,
+ fields: [
+ {
+ type: "textArea",
+ props: {
+ name: "comment",
+ label: "Comments" as TranslatedString,
+ },
+ },
+ ],
+ },
+ {
+ title: "Resolution" as TranslatedString,
+ description: `Current state is and threshold at ` as TranslatedString,
+ fields: [
+ {
+ type: "date",
+ props: {
+ name: "when",
+ label: "Decision Time" as TranslatedString,
+ },
+ },
+ {
+ type: "amount",
+ props: {
+ name: "threshold",
+ label: "New threshold" as TranslatedString,
+ },
+ },
+ ],
+ }
+ ,
+];
+
+function formBehavior(v: Partial): FormState {
+ return {
+ when: {
+ disabled: true,
+ },
+ threshold: {
+ // disabled: v.state === AmlExchangeBackend.AmlState.frozen,
+ },
+ };
+}
+
+
diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts
index b8b6ab899..154c43ae6 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -44,13 +44,13 @@ export function useAccessAPI(): AccessAPI {
const account = state.username;
const createWithdrawal = async (
- data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
+ data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest,
): Promise<
- HttpResponseOk
+ HttpResponseOk
> => {
const res =
- await request(
- `access-api/accounts/${account}/withdrawals`,
+ await request(
+ `accounts/${account}/withdrawals`,
{
method: "POST",
data,
@@ -60,21 +60,21 @@ export function useAccessAPI(): AccessAPI {
return res;
};
const createTransaction = async (
- data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
+ data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
): Promise> => {
const res = await request(
- `access-api/accounts/${account}/transactions`,
+ `accounts/${account}/transactions`,
{
method: "POST",
data,
contentType: "json",
},
);
- await mutateAll(/.*accounts\/.*\/transactions.*/);
+ await mutateAll(/.*accounts\/.*/);
return res;
};
const deleteAccount = async (): Promise> => {
- const res = await request(`access-api/accounts/${account}`, {
+ const res = await request(`accounts/${account}`, {
method: "DELETE",
contentType: "json",
});
@@ -94,7 +94,7 @@ export function useAccessAnonAPI(): AccessAnonAPI {
const { request } = useAuthenticatedBackend();
const abortWithdrawal = async (id: string): Promise> => {
- const res = await request(`access-api/withdrawals/${id}/abort`, {
+ const res = await request(`withdrawals/${id}/abort`, {
method: "POST",
contentType: "json",
});
@@ -104,7 +104,7 @@ export function useAccessAnonAPI(): AccessAnonAPI {
const confirmWithdrawal = async (
id: string,
): Promise> => {
- const res = await request(`access-api/withdrawals/${id}/confirm`, {
+ const res = await request(`withdrawals/${id}/confirm`, {
method: "POST",
contentType: "json",
});
@@ -122,9 +122,10 @@ export function useTestingAPI(): TestingAPI {
const mutateAll = useMatchMutate();
const { request: noAuthRequest } = usePublicBackend();
const register = async (
- data: SandboxBackend.Access.BankRegistrationRequest,
+ data: SandboxBackend.CoreBank.RegisterAccountRequest,
): Promise> => {
- const res = await noAuthRequest(`access-api/testing/register`, {
+ // FIXME: This API is deprecated. The normal account registration API should be used instead.
+ const res = await noAuthRequest(`accounts`, {
method: "POST",
data,
contentType: "json",
@@ -138,18 +139,18 @@ export function useTestingAPI(): TestingAPI {
export interface TestingAPI {
register: (
- data: SandboxBackend.Access.BankRegistrationRequest,
+ data: SandboxBackend.CoreBank.RegisterAccountRequest,
) => Promise>;
}
export interface AccessAPI {
createWithdrawal: (
- data: SandboxBackend.Access.BankAccountCreateWithdrawalRequest,
+ data: SandboxBackend.CoreBank.BankAccountCreateWithdrawalRequest,
) => Promise<
- HttpResponseOk
+ HttpResponseOk
>;
createTransaction: (
- data: SandboxBackend.Access.CreateBankAccountTransactionCreate,
+ data: SandboxBackend.CoreBank.CreateBankAccountTransactionCreate,
) => Promise>;
deleteAccount: () => Promise>;
}
@@ -166,15 +167,15 @@ export interface InstanceTemplateFilter {
export function useAccountDetails(
account: string,
): HttpResponse<
- SandboxBackend.Access.BankAccountBalanceResponse,
+ SandboxBackend.CoreBank.AccountData,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
- HttpResponseOk,
+ HttpResponseOk,
RequestError
- >([`access-api/accounts/${account}`], fetcher, {
+ >([`accounts/${account}`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -186,28 +187,8 @@ export function useAccountDetails(
keepPreviousData: true,
});
- //FIXME: remove optional when libeufin sandbox has implemented the feature
- if (data && typeof data.data.debitThreshold === "undefined") {
- data.data.debitThreshold = "0";
- }
- //FIXME: sandbox server should return amount string
if (data) {
- const isAmount = Amounts.parse(data.data.debitThreshold);
- if (isAmount) {
- //server response with correct format
- return data;
- }
- const { currency } = Amounts.parseOrThrow(data.data.balance.amount);
- const clone = structuredClone(data);
-
- const theNumber = Number.parseInt(data.data.debitThreshold, 10);
- const value = Number.isNaN(theNumber) ? 0 : theNumber;
- clone.data.debitThreshold = Amounts.stringify({
- currency,
- value: value,
- fraction: 0,
- });
- return clone;
+ return data;
}
if (error) return error.cause;
return { loading: true };
@@ -217,15 +198,15 @@ export function useAccountDetails(
export function useWithdrawalDetails(
wid: string,
): HttpResponse<
- SandboxBackend.Access.BankAccountGetWithdrawalResponse,
+ SandboxBackend.CoreBank.BankAccountGetWithdrawalResponse,
SandboxBackend.SandboxError
> {
const { fetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
- HttpResponseOk,
+ HttpResponseOk,
RequestError
- >([`access-api/withdrawals/${wid}`], fetcher, {
+ >([`withdrawals/${wid}`], fetcher, {
refreshInterval: 1000,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -247,15 +228,15 @@ export function useTransactionDetails(
account: string,
tid: string,
): HttpResponse<
- SandboxBackend.Access.BankAccountTransactionInfo,
+ SandboxBackend.CoreBank.BankAccountTransactionInfo,
SandboxBackend.SandboxError
> {
- const { fetcher } = useAuthenticatedBackend();
+ const { paginatedFetcher } = useAuthenticatedBackend();
const { data, error } = useSWR<
- HttpResponseOk,
+ HttpResponseOk,
RequestError
- >([`access-api/accounts/${account}/transactions/${tid}`], fetcher, {
+ >([`accounts/${account}/transactions/${tid}`], paginatedFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@@ -274,13 +255,13 @@ export function useTransactionDetails(
}
interface PaginationFilter {
- page: number;
+ // page: number;
}
export function usePublicAccounts(
args?: PaginationFilter,
): HttpResponsePaginated<
- SandboxBackend.Access.PublicAccountsResponse,
+ SandboxBackend.CoreBank.PublicAccountsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = usePublicBackend();
@@ -292,13 +273,13 @@ export function usePublicAccounts(
error: afterError,
isValidating: loadingAfter,
} = useSWR<
- HttpResponseOk,
+ HttpResponseOk,
RequestError
- >([`access-api/public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher);
+ >([`public-accounts`, page, PAGE_SIZE], paginatedFetcher);
const [lastAfter, setLastAfter] = useState<
HttpResponse<
- SandboxBackend.Access.PublicAccountsResponse,
+ SandboxBackend.CoreBank.PublicAccountsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });
@@ -311,7 +292,7 @@ export function usePublicAccounts(
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
- afterData && afterData.data.publicAccounts.length < PAGE_SIZE;
+ afterData && afterData.data.public_accounts.length < PAGE_SIZE;
const isReachingStart = false;
const pagination = {
@@ -319,7 +300,7 @@ export function usePublicAccounts(
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
- if (afterData.data.publicAccounts.length < MAX_RESULT_SIZE) {
+ if (afterData.data.public_accounts.length < MAX_RESULT_SIZE) {
setPage(page + 1);
}
},
@@ -328,12 +309,12 @@ export function usePublicAccounts(
},
};
- const publicAccounts = !afterData
+ const public_accounts = !afterData
? []
- : (afterData || lastAfter).data.publicAccounts;
- if (loadingAfter) return { loading: true, data: { publicAccounts } };
+ : (afterData || lastAfter).data.public_accounts;
+ if (loadingAfter) return { loading: true, data: { public_accounts } };
if (afterData) {
- return { ok: true, data: { publicAccounts }, ...pagination };
+ return { ok: true, data: { public_accounts }, ...pagination };
}
return { loading: true };
}
@@ -348,28 +329,36 @@ export function useTransactions(
account: string,
args?: PaginationFilter,
): HttpResponsePaginated<
- SandboxBackend.Access.BankAccountTransactionsResponse,
+ SandboxBackend.CoreBank.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
> {
const { paginatedFetcher } = useAuthenticatedBackend();
- const [page, setPage] = useState(1);
+ const [start, setStart] = useState();
const {
data: afterData,
error: afterError,
isValidating: loadingAfter,
} = useSWR<
- HttpResponseOk,
+ HttpResponseOk,
RequestError
>(
- [`access-api/accounts/${account}/transactions`, args?.page, PAGE_SIZE],
- paginatedFetcher,
+ [`accounts/${account}/transactions`, start, PAGE_SIZE],
+ paginatedFetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+ // revalidateOnMount: false,
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ }
);
const [lastAfter, setLastAfter] = useState<
HttpResponse<
- SandboxBackend.Access.BankAccountTransactionsResponse,
+ SandboxBackend.CoreBank.BankAccountTransactionsResponse,
SandboxBackend.SandboxError
>
>({ loading: true });
@@ -385,19 +374,23 @@ export function useTransactions(
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
afterData && afterData.data.transactions.length < PAGE_SIZE;
- const isReachingStart = false;
+ const isReachingStart = start == undefined;
const pagination = {
isReachingEnd,
isReachingStart,
loadMore: () => {
if (!afterData || isReachingEnd) return;
- if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
- setPage(page + 1);
- }
+ // if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
+ const l = afterData.data.transactions[afterData.data.transactions.length-1]
+ setStart(String(l.row_id));
+ // }
},
loadMorePrev: () => {
- null;
+ if (!afterData || isReachingStart) return;
+ // if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
+ setStart(undefined)
+ // }
},
};
diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts
index 4b60d1b6c..889618646 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -40,21 +40,24 @@ import { useCallback, useEffect, useState } from "preact/hooks";
import { useSWRConfig } from "swr";
import { useBackendContext } from "../context/backend.js";
import { bankUiSettings } from "../settings.js";
+import { AccessToken } from "./useCredentialsChecker.js";
/**
* Has the information to reach and
* authenticate at the bank's backend.
*/
-export type BackendState = LoggedIn | LoggedOut;
+export type BackendState = LoggedIn | LoggedOut | Expired;
-export interface BackendCredentials {
- username: string;
- password: string;
-}
-
-interface LoggedIn extends BackendCredentials {
+interface LoggedIn {
status: "loggedIn";
isUserAdministrator: boolean;
+ username: string;
+ token: AccessToken;
+}
+interface Expired {
+ status: "expired";
+ isUserAdministrator: boolean;
+ username: string;
}
interface LoggedOut {
status: "loggedOut";
@@ -64,10 +67,17 @@ export const codecForBackendStateLoggedIn = (): Codec =>
buildCodecForObject()
.property("status", codecForConstString("loggedIn"))
.property("username", codecForString())
- .property("password", codecForString())
+ .property("token", codecForString() as Codec)
.property("isUserAdministrator", codecForBoolean())
.build("BackendState.LoggedIn");
+export const codecForBackendStateExpired = (): Codec =>
+ buildCodecForObject()
+ .property("status", codecForConstString("expired"))
+ .property("username", codecForString())
+ .property("isUserAdministrator", codecForBoolean())
+ .build("BackendState.Expired");
+
export const codecForBackendStateLoggedOut = (): Codec =>
buildCodecForObject()
.property("status", codecForConstString("loggedOut"))
@@ -78,6 +88,7 @@ export const codecForBackendState = (): Codec =>
.discriminateOn("status")
.alternative("loggedIn", codecForBackendStateLoggedIn())
.alternative("loggedOut", codecForBackendStateLoggedOut())
+ .alternative("expired", codecForBackendStateExpired())
.build("BackendState");
export function getInitialBackendBaseURL(): string {
@@ -85,18 +96,27 @@ export function getInitialBackendBaseURL(): string {
typeof localStorage !== "undefined"
? localStorage.getItem("bank-base-url")
: undefined;
+ let result: string;
if (!overrideUrl) {
//normal path
if (!bankUiSettings.backendBaseURL) {
console.error(
"ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
);
- return canonicalizeBaseUrl(window.origin);
+ result = window.origin
+ } else {
+ result = bankUiSettings.backendBaseURL;
}
- return canonicalizeBaseUrl(bankUiSettings.backendBaseURL);
+ } else {
+ // testing/development path
+ result = overrideUrl
+ }
+ try {
+ return canonicalizeBaseUrl(result)
+ } catch (e) {
+ //fall back
+ return canonicalizeBaseUrl(window.origin)
}
- // testing/development path
- return canonicalizeBaseUrl(overrideUrl);
}
export const defaultState: BackendState = {
@@ -106,7 +126,8 @@ export const defaultState: BackendState = {
export interface BackendStateHandler {
state: BackendState;
logOut(): void;
- logIn(info: BackendCredentials): void;
+ expired(): void;
+ logIn(info: {username: string, token: AccessToken}): void;
}
const BACKEND_STATE_KEY = buildStorageKey(
@@ -124,12 +145,22 @@ export function useBackendState(): BackendStateHandler {
BACKEND_STATE_KEY,
defaultState,
);
+ const mutateAll = useMatchMutate();
return {
state,
logOut() {
update(defaultState);
},
+ expired() {
+ if (state.status === "loggedOut") return;
+ const nextState: BackendState = {
+ status: "expired",
+ username: state.username,
+ isUserAdministrator: state.username === "admin",
+ };
+ update(nextState);
+ },
logIn(info) {
//admin is defined by the username
const nextState: BackendState = {
@@ -138,6 +169,7 @@ export function useBackendState(): BackendStateHandler {
isUserAdministrator: info.username === "admin",
};
update(nextState);
+ mutateAll(/.*/)
},
};
}
@@ -150,7 +182,7 @@ interface useBackendType {
fetcher: (endpoint: string) => Promise>;
multiFetcher: (endpoint: string[][]) => Promise[]>;
paginatedFetcher: (
- args: [string, number, number],
+ args: [string, string | undefined, number],
) => Promise>;
sandboxAccountsFetcher: (
args: [string, number, number, string],
@@ -179,13 +211,15 @@ export function usePublicBackend(): useBackendType {
[baseUrl],
);
const paginatedFetcher = useCallback(
- function fetcherImpl([endpoint, page, size]: [
+ function fetcherImpl([endpoint, start, size]: [
string,
- number,
+ string | undefined,
number,
]): Promise> {
+ const delta = -1 * size //descending order
+ const params = start ? { delta, start } : { delta }
return requestHandler(baseUrl, endpoint, {
- params: { page: page || 1, size },
+ params,
});
},
[baseUrl],
@@ -247,35 +281,12 @@ interface InvalidationResult {
error: unknown;
}
-export function useCredentialsChecker() {
- const { request } = useApiContext();
- const baseUrl = getInitialBackendBaseURL();
- //check against account details endpoint
- //while sandbox backend doesn't have a login endpoint
- return async function testLogin(
- username: string,
- password: string,
- ): Promise {
- try {
- await request(baseUrl, `access-api/accounts/${username}/`, {
- basicAuth: { username, password },
- preventCache: true,
- });
- return { valid: true };
- } catch (error) {
- if (error instanceof RequestError) {
- return { valid: false, requestError: true, cause: error.cause };
- }
- return { valid: false, requestError: false, error };
- }
- };
-}
-
export function useAuthenticatedBackend(): useBackendType {
const { state } = useBackendContext();
const { request: requestHandler } = useApiContext();
- const creds = state.status === "loggedIn" ? state : undefined;
+ // FIXME: libeufin returns 400 insteand of 401 if there is no auth token
+ const creds = state.status === "loggedIn" ? state.token : "secret-token:a";
const baseUrl = getInitialBackendBaseURL();
const request = useCallback(
@@ -283,26 +294,28 @@ export function useAuthenticatedBackend(): useBackendType {
path: string,
options: RequestOptions = {},
): Promise> {
- return requestHandler(baseUrl, path, { basicAuth: creds, ...options });
+ return requestHandler(baseUrl, path, { token: creds, ...options });
},
[baseUrl, creds],
);
const fetcher = useCallback(
function fetcherImpl(endpoint: string): Promise> {
- return requestHandler(baseUrl, endpoint, { basicAuth: creds });
+ return requestHandler(baseUrl, endpoint, { token: creds });
},
[baseUrl, creds],
);
const paginatedFetcher = useCallback(
- function fetcherImpl([endpoint, page = 1, size]: [
+ function fetcherImpl([endpoint, start, size]: [
string,
- number,
+ string | undefined,
number,
]): Promise> {
+ const delta = -1 * size //descending order
+ const params = start ? { delta, start } : { delta }
return requestHandler(baseUrl, endpoint, {
- basicAuth: creds,
- params: { page, size },
+ token: creds,
+ params,
});
},
[baseUrl, creds],
@@ -313,7 +326,7 @@ export function useAuthenticatedBackend(): useBackendType {
> {
return Promise.all(
endpoints.map((endpoint) =>
- requestHandler(baseUrl, endpoint, { basicAuth: creds }),
+ requestHandler(baseUrl, endpoint, { token: creds }),
),
);
},
@@ -327,7 +340,7 @@ export function useAuthenticatedBackend(): useBackendType {
string,
]): Promise> {
return requestHandler(baseUrl, endpoint, {
- basicAuth: creds,
+ token: creds,
params: { page: page || 1, size },
});
},
@@ -339,7 +352,7 @@ export function useAuthenticatedBackend(): useBackendType {
HttpResponseOk
> {
return requestHandler(baseUrl, endpoint, {
- basicAuth: creds,
+ token: creds,
params: { account },
});
},
diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts
index 06557b77f..5dba60951 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -33,6 +33,7 @@ import {
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import _useSWR, { SWRHook } from "swr";
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
+import { AccessToken } from "./useCredentialsChecker.js";
const useSWR = _useSWR as unknown as SWRHook;
export function useAdminAccountAPI(): AdminAccountAPI {
@@ -90,7 +91,8 @@ export function useAdminAccountAPI(): AdminAccountAPI {
await mutateAll(/.*/);
logIn({
username: account,
- password: data.new_password,
+ //FIXME: change password api
+ token: data.new_password as AccessToken,
});
}
return res;
@@ -215,14 +217,15 @@ export interface CircuitAccountAPI {
async function getBusinessStatus(
request: ReturnType["request"],
- basicAuth: { username: string; password: string },
+ username: string,
+ token: AccessToken,
): Promise {
try {
const url = getInitialBackendBaseURL();
const result = await request(
url,
- `circuit-api/accounts/${basicAuth.username}`,
- { basicAuth },
+ `circuit-api/accounts/${username}`,
+ { token },
);
return result.ok;
} catch (error) {
@@ -264,10 +267,10 @@ type CashoutEstimators = {
export function useEstimator(): CashoutEstimators {
const { state } = useBackendContext();
const { request } = useApiContext();
- const basicAuth =
- state.status === "loggedOut"
+ const creds =
+ state.status !== "loggedIn"
? undefined
- : { username: state.username, password: state.password };
+ : state.token;
return {
estimateByCredit: async (amount, fee, rate) => {
const zeroBalance = Amounts.zeroOfCurrency(fee.currency);
@@ -282,7 +285,7 @@ export function useEstimator(): CashoutEstimators {
url,
`circuit-api/cashouts/estimates`,
{
- basicAuth,
+ token: creds,
params: {
amount_credit: Amounts.stringify(amount),
},
@@ -313,7 +316,7 @@ export function useEstimator(): CashoutEstimators {
url,
`circuit-api/cashouts/estimates`,
{
- basicAuth,
+ token: creds,
params: {
amount_debit: Amounts.stringify(amount),
},
@@ -337,13 +340,13 @@ export function useBusinessAccountFlag(): boolean | undefined {
const { state } = useBackendContext();
const { request } = useApiContext();
const creds =
- state.status === "loggedOut"
+ state.status !== "loggedIn"
? undefined
- : { username: state.username, password: state.password };
+ : {user: state.username, token: state.token};
useEffect(() => {
if (!creds) return;
- getBusinessStatus(request, creds)
+ getBusinessStatus(request, creds.user, creds.token)
.then((result) => {
setIsBusiness(result);
})
@@ -432,7 +435,7 @@ export function useBusinessAccounts(
HttpResponseOk,
RequestError
>(
- [`circuit-api/accounts`, args?.page, PAGE_SIZE, args?.account],
+ [`accounts`, args?.page, PAGE_SIZE, args?.account],
sandboxAccountsFetcher,
{
refreshInterval: 0,
diff --git a/packages/demobank-ui/src/hooks/config.ts b/packages/demobank-ui/src/hooks/config.ts
new file mode 100644
index 000000000..a3bd294db
--- /dev/null
+++ b/packages/demobank-ui/src/hooks/config.ts
@@ -0,0 +1,59 @@
+import { LibtoolVersion } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, HttpResponseServerError, RequestError, useApiContext } from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { getInitialBackendBaseURL } from "./backend.js";
+
+/**
+ * Protocol version spoken with the bank.
+ *
+ * Uses libtool's current:revision:age versioning.
+ */
+export const BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0";
+
+async function getConfigState(
+ request: ReturnType["request"],
+): Promise {
+ const url = getInitialBackendBaseURL();
+ const result = await request(url, `config`);
+ return result.data;
+}
+
+export type ConfigResult = undefined
+ | { type: "ok", result: Required }
+ | { type: "wrong", result: SandboxBackend.Config }
+ | { type: "error", result: HttpError }
+
+export function useConfigState(): ConfigResult {
+ const [checked, setChecked] = useState()
+ const { request } = useApiContext();
+
+ useEffect(() => {
+ getConfigState(request)
+ .then((result) => {
+ const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version)
+ if (r?.compatible) {
+ const complete: Required = {
+ currency_fraction_digits: result.currency_fraction_digits ?? 2,
+ currency_fraction_limit: result.currency_fraction_limit ?? 2,
+ fiat_currency: "",
+ have_cashout: result.have_cashout ?? false,
+ name: result.name,
+ version: result.version,
+ }
+ setChecked({ type: "ok", result: complete });
+ } else {
+ setChecked({ type: "wrong", result })
+ }
+ })
+ .catch((error: unknown) => {
+ if (error instanceof RequestError) {
+ const result = error.cause
+ setChecked({ type: "error", result });
+ }
+ });
+ }, []);
+
+ return checked;
+}
+
+
diff --git a/packages/demobank-ui/src/hooks/notification.ts b/packages/demobank-ui/src/hooks/notification.ts
deleted file mode 100644
index 9bf621b41..000000000
--- a/packages/demobank-ui/src/hooks/notification.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { memoryMap } from "@gnu-taler/web-util/browser";
-import { StateUpdater, useEffect, useState } from "preact/hooks";
-
-export type NotificationMessage = ErrorNotification | InfoNotification;
-
-//FIXME: this should not be exported since every notification
-// goes throw notify function
-export interface ErrorMessage {
- description?: string;
- title: TranslatedString;
- debug?: string;
-}
-
-interface ErrorNotification {
- type: "error";
- error: ErrorMessage;
-}
-interface InfoNotification {
- type: "info";
- info: TranslatedString;
-}
-
-const storage = memoryMap();
-const NOTIFICATION_KEY = "notification";
-
-export function onNotificationUpdate(
- handler: (newValue: NotificationMessage | undefined) => void,
-) {
- return storage.onUpdate(NOTIFICATION_KEY, () => {
- const newValue = storage.get(NOTIFICATION_KEY);
- handler(newValue);
- });
-}
-
-export function notifyError(error: ErrorMessage) {
- storage.set(NOTIFICATION_KEY, { type: "error", error });
-}
-export function notifyInfo(info: TranslatedString) {
- storage.set(NOTIFICATION_KEY, { type: "info", info });
-}
-
-export function useNotifications(): [
- NotificationMessage | undefined,
- StateUpdater,
-] {
- const [value, setter] = useState();
- useEffect(() => {
- return storage.onUpdate(NOTIFICATION_KEY, () => {
- setter(storage.get(NOTIFICATION_KEY));
- });
- });
- return [value, setter];
-}
diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts
index 46b31bf2a..ad853f9d7 100644
--- a/packages/demobank-ui/src/hooks/settings.ts
+++ b/packages/demobank-ui/src/hooks/settings.ts
@@ -15,8 +15,12 @@
*/
import {
+ AmountString,
Codec,
buildCodecForObject,
+ codecForAmountString,
+ codecForBoolean,
+ codecForNumber,
codecForString,
codecOptional,
} from "@gnu-taler/taler-util";
@@ -24,15 +28,33 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
interface Settings {
currentWithdrawalOperationId: string | undefined;
+ showWithdrawalSuccess: boolean;
+ showDemoDescription: boolean;
+ showInstallWallet: boolean;
+ maxWithdrawalAmount: number;
+ fastWithdrawal: boolean;
+ showDebugInfo: boolean;
}
export const codecForSettings = (): Codec =>
buildCodecForObject()
.property("currentWithdrawalOperationId", codecOptional(codecForString()))
+ .property("showWithdrawalSuccess", (codecForBoolean()))
+ .property("showDemoDescription", (codecForBoolean()))
+ .property("showInstallWallet", (codecForBoolean()))
+ .property("fastWithdrawal", (codecForBoolean()))
+ .property("showDebugInfo", (codecForBoolean()))
+ .property("maxWithdrawalAmount", codecForNumber())
.build("Settings");
const defaultSettings: Settings = {
currentWithdrawalOperationId: undefined,
+ showWithdrawalSuccess: true,
+ showDemoDescription: true,
+ showInstallWallet: true,
+ maxWithdrawalAmount: 25,
+ fastWithdrawal: false,
+ showDebugInfo: false,
};
const DEMOBANK_SETTINGS_KEY = buildStorageKey(
diff --git a/packages/demobank-ui/src/hooks/useCredentialsChecker.ts b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts
new file mode 100644
index 000000000..b3dedb654
--- /dev/null
+++ b/packages/demobank-ui/src/hooks/useCredentialsChecker.ts
@@ -0,0 +1,135 @@
+import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, RequestError, useApiContext } from "@gnu-taler/web-util/browser";
+import { getInitialBackendBaseURL } from "./backend.js";
+
+export function useCredentialsChecker() {
+ const { request } = useApiContext();
+ const baseUrl = getInitialBackendBaseURL();
+ //check against instance details endpoint
+ //while merchant backend doesn't have a login endpoint
+ async function requestNewLoginToken(
+ username: string,
+ password: string,
+ ): Promise {
+ const data: LoginTokenRequest = {
+ scope: "readwrite" as "write", //FIX: different than merchant
+ duration: {
+ // d_us: "forever" //FIX: should return shortest
+ d_us: 60 * 60 * 24 * 7 * 1000 * 1000
+ },
+ refreshable: true,
+ }
+ try {
+ const response = await request(baseUrl, `accounts/${username}/token`, {
+ method: "POST",
+ basicAuth: {
+ username,
+ password,
+ },
+ data,
+ contentType: "json"
+ });
+ return { valid: true, token: `secret-token:${response.data.access_token}` as AccessToken, expiration: response.data.expiration };
+ } catch (error) {
+ if (error instanceof RequestError) {
+ return { valid: false, cause: error.cause };
+ }
+
+ return {
+ valid: false, cause: {
+ type: ErrorType.UNEXPECTED,
+ loading: false,
+ info: {
+ hasToken: true,
+ status: 0,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ exception: error,
+ message: (error instanceof Error ? error.message : "unpexepected error")
+ }
+ };
+ }
+ };
+
+ async function refreshLoginToken(
+ baseUrl: string,
+ token: LoginToken
+ ): Promise {
+
+ if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) {
+ return {
+ valid: false, cause: {
+ type: ErrorType.CLIENT,
+ status: HttpStatusCode.Unauthorized,
+ message: "login token expired, login again.",
+ info: {
+ hasToken: true,
+ status: 401,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ payload: {}
+ },
+ }
+ }
+
+ return requestNewLoginToken(baseUrl, token.token)
+ }
+ return { requestNewLoginToken, refreshLoginToken }
+}
+
+export interface LoginToken {
+ token: AccessToken,
+ expiration: Timestamp,
+}
+// token used to get loginToken
+// must forget after used
+declare const __ac_token: unique symbol;
+export type AccessToken = string & {
+ [__ac_token]: true;
+};
+
+type YesOrNo = "yes" | "no";
+export type LoginResult = {
+ valid: true;
+ token: AccessToken;
+ expiration: Timestamp;
+} | {
+ valid: false;
+ cause: HttpError<{}>;
+}
+
+
+// DELETE /private/instances/$INSTANCE
+export interface LoginTokenRequest {
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ duration?: RelativeTime;
+
+ // Can this token be refreshed?
+ // Defaults to false.
+ refreshable?: boolean;
+}
+export interface LoginTokenSuccessResponse {
+ // The login token that can be used to access resources
+ // that are in scope for some time. Must be prefixed
+ // with "Bearer " when used in the "Authorization" HTTP header.
+ // Will already begin with the RFC 8959 prefix.
+ access_token: AccessToken;
+
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ expiration: Timestamp;
+
+ // Can this token be refreshed?
+ refreshable: boolean;
+}
diff --git a/packages/demobank-ui/src/index.html b/packages/demobank-ui/src/index.html
index e21e1fccc..315985648 100644
--- a/packages/demobank-ui/src/index.html
+++ b/packages/demobank-ui/src/index.html
@@ -16,27 +16,28 @@
@author Sebastian Javier Marchano
-->
-
-
-
-
-
-
-
-
-
-
- Demobank
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ Demobank
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/demobank-ui/src/index.tsx b/packages/demobank-ui/src/index.tsx
index 2e0f740fe..b7d69fd2d 100644
--- a/packages/demobank-ui/src/index.tsx
+++ b/packages/demobank-ui/src/index.tsx
@@ -16,7 +16,7 @@
import App from "./components/app.js";
import { h, render } from "preact";
-import "./scss/main.scss";
+import "./scss/main.css"
const app = document.getElementById("app");
diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx b/packages/demobank-ui/src/pages/AccountPage.tsx
deleted file mode 100644
index 820c59984..000000000
--- a/packages/demobank-ui/src/pages/AccountPage.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see
- */
-
-import { Amounts, HttpStatusCode, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
-import {
- ErrorType,
- HttpResponsePaginated,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { Transactions } from "../components/Transactions/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { useAccountDetails } from "../hooks/access.js";
-import { LoginForm } from "./LoginForm.js";
-import { PaymentOptions } from "./PaymentOptions.js";
-import { notifyError } from "../hooks/notification.js";
-import { useEffect, useState } from "preact/hooks";
-
-interface Props {
- account: string;
- onLoadNotOk: (
- error: HttpResponsePaginated,
- ) => VNode;
-}
-
-export const CopyIcon = (): VNode => (
-
-
-
-
-);
-
-export const CopiedIcon = (): VNode => (
-
-
-
-);
-
-function CopyButton({ getContent }: { getContent: () => string }): VNode {
- const [copied, setCopied] = useState(false);
- function copyText(): void {
- navigator.clipboard.writeText(getContent() || "");
- setCopied(true);
- }
- useEffect(() => {
- if (copied) {
- setTimeout(() => {
- setCopied(false);
- }, 1000);
- }
- }, [copied]);
-
- if (!copied) {
- return (
-
-
-
- );
- }
- return (
-
-
-
-
-
- );
-}
-
-
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-export function AccountPage({ account, onLoadNotOk }: Props): VNode {
- const result = useAccountDetails(account);
- const backend = useBackendContext();
- const { i18n } = useTranslationContext();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- //logout if there is any error, not if loading
- backend.logOut();
- if (result.status === HttpStatusCode.NotFound) {
- notifyError({
- title: i18n.str`Username or account label "${account}" not found`,
- });
- return ;
- }
- return onLoadNotOk(result);
- }
-
- const { data } = result;
- const balance = Amounts.parseOrThrow(data.balance.amount);
- const debitThreshold = Amounts.parseOrThrow(data.debitThreshold);
- const payto = parsePaytoUri(data.paytoUri);
- if (!payto || !payto.isKnown || payto.targetType !== "iban") {
- return (
- Payto from server is not valid "{data.paytoUri}"
- );
- }
- const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
- const limit = balanceIsDebit
- ? Amounts.sub(debitThreshold, balance).amount
- : Amounts.add(balance, debitThreshold).amount;
- return (
-
-
-
-
- Welcome, {account} ({payto.iban} )! stringifyPaytoUri(payto)} />
-
-
-
-
-
-
-
{i18n.str`Bank account balance`}
- {!balance ? (
-
- Waiting server response...
-
- ) : (
-
- {balanceIsDebit ? - : null}
- {`${Amounts.stringifyValue(balance)}`}
-
- {`${balance.currency}`}
-
- )}
-
-
-
-
-
{i18n.str`Payments`}
-
-
-
-
-
-
-
{i18n.str`Latest transactions`}
-
-
-
-
- );
-}
diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts
new file mode 100644
index 000000000..9230fb6b1
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AccountPage/index.ts
@@ -0,0 +1,92 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see
+ */
+
+import { HttpError, HttpResponseOk, HttpResponsePaginated, utils } from "@gnu-taler/web-util/browser";
+import { AbsoluteTime, AmountJson, PaytoUriIBAN, PaytoUriTalerBank } from "@gnu-taler/taler-util";
+import { Loading } from "../../components/Loading.js";
+import { useComponentState } from "./state.js";
+import { ReadyView, InvalidIbanView } from "./views.js";
+import { VNode } from "preact";
+import { LoginForm } from "../LoginForm.js";
+import { ErrorLoading } from "../../components/ErrorLoading.js";
+
+export interface Props {
+ account: string;
+ onLoadNotOk: (
+ error: HttpResponsePaginated,
+ ) => VNode;
+ goToBusinessAccount: () => void;
+ goToConfirmOperation: (id: string) => void;
+}
+
+export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingError {
+ status: "loading-error";
+ error: HttpError;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ account: string,
+ limit: AmountJson,
+ goToBusinessAccount: () => void;
+ goToConfirmOperation: (id: string) => void;
+ }
+
+ export interface InvalidIban {
+ status: "invalid-iban",
+ error: HttpResponseOk;
+ }
+
+ export interface UserNotFound {
+ status: "error-user-not-found",
+ error: HttpError;
+ onRegister?: () => void;
+ }
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap = {
+ loading: Loading,
+ "error-user-not-found": LoginForm,
+ "invalid-iban": InvalidIbanView,
+ "loading-error": ErrorLoading,
+ ready: ReadyView,
+};
+
+export const AccountPage = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts
new file mode 100644
index 000000000..ca7e1d447
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AccountPage/state.ts
@@ -0,0 +1,92 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see
+ */
+
+import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
+import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useBackendContext } from "../../context/backend.js";
+import { useAccountDetails } from "../../hooks/access.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({ account, goToBusinessAccount, goToConfirmOperation }: Props): State {
+ const result = useAccountDetails(account);
+ const backend = useBackendContext();
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ if (!result.ok) {
+ if (result.loading || result.type === ErrorType.TIMEOUT) {
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+ //logout if there is any error, not if loading
+ // backend.logOut();
+ if (result.status === HttpStatusCode.NotFound) {
+ notifyError(i18n.str`Username or account label "${account}" not found`, undefined);
+ return {
+ status: "error-user-not-found",
+ error: result,
+ };
+ }
+ if (result.status === HttpStatusCode.Unauthorized) {
+ notifyError(i18n.str`Authorization denied`, i18n.str`Maybe the session has expired, login again.`);
+ return {
+ status: "error-user-not-found",
+ error: result,
+ };
+ }
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+
+ const { data } = result;
+
+ const balance = Amounts.parseOrThrow(data.balance.amount);
+
+ const debitThreshold = Amounts.parseOrThrow(data.debit_threshold);
+ const payto = parsePaytoUri(data.payto_uri);
+
+ if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) {
+ return {
+ status: "invalid-iban",
+ error: result
+ };
+ }
+
+ const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
+ const limit = balanceIsDebit
+ ? Amounts.sub(debitThreshold, balance).amount
+ : Amounts.add(balance, debitThreshold).amount;
+
+
+ return {
+ status: "ready",
+ goToBusinessAccount,
+ goToConfirmOperation,
+ error: undefined,
+ account,
+ limit,
+ };
+}
diff --git a/packages/demobank-ui/src/scss/_footer.scss b/packages/demobank-ui/src/pages/AccountPage/stories.tsx
similarity index 76%
rename from packages/demobank-ui/src/scss/_footer.scss
rename to packages/demobank-ui/src/pages/AccountPage/stories.tsx
index 112522ed8..f3828a5d6 100644
--- a/packages/demobank-ui/src/scss/_footer.scss
+++ b/packages/demobank-ui/src/pages/AccountPage/stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,17 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-footer.footer {
- .logo {
- img {
- width: auto;
- height: $footer-logo-height;
- }
- }
-}
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
-@include mobile {
- .footer-copyright {
- text-align: center;
- }
-}
+export default {
+ title: "account page",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/demobank-ui/src/scss/_mixins.scss b/packages/demobank-ui/src/pages/AccountPage/test.ts
similarity index 63%
rename from packages/demobank-ui/src/scss/_mixins.scss
rename to packages/demobank-ui/src/pages/AccountPage/test.ts
index b52e590e3..588b84c35 100644
--- a/packages/demobank-ui/src/scss/_mixins.scss
+++ b/packages/demobank-ui/src/pages/AccountPage/test.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,16 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-@mixin transition($t) {
- transition: $t 250ms ease-in-out 50ms;
-}
+import * as tests from "@gnu-taler/web-util/testing";
+import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
-@mixin icon-with-update-mark($icon-base-width) {
- .icon {
- width: $icon-base-width;
-
- &.has-update-mark:after {
- right: ($icon-base-width / 2) - 0.85;
- }
- }
-}
+describe("Account states", () => {
+ it("should do some tests", async () => {
+ });
+});
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx
new file mode 100644
index 000000000..483cb579a
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -0,0 +1,93 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see
+ */
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Attention } from "../../components/Attention.js";
+import { Transactions } from "../../components/Transactions/index.js";
+import { useBusinessAccountDetails } from "../../hooks/circuit.js";
+import { useSettings } from "../../hooks/settings.js";
+import { PaymentOptions } from "../PaymentOptions.js";
+import { State } from "./index.js";
+
+export function InvalidIbanView({ error }: State.InvalidIban) {
+ return (
+ Payto from server is not valid "{error.data.payto_uri}"
+ );
+}
+
+const IS_PUBLIC_ACCOUNT_ENABLED = false
+
+function ShowDemoInfo(): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings();
+ if (!settings.showDemoDescription) return
+ return {
+ updateSettings("showDemoDescription", false);
+ }}>
+ {IS_PUBLIC_ACCOUNT_ENABLED ? (
+
+ This part of the demo shows how a bank that supports Taler
+ directly would work. In addition to using your own bank
+ account, you can also see the transaction history of some{" "}
+ Public Accounts .
+
+ ) : (
+
+ This part of the demo shows how a bank that supports Taler
+ directly would work.
+
+ )}
+
+}
+
+export function ReadyView({ account, limit, goToBusinessAccount, goToConfirmOperation }: State.Ready): VNode<{}> {
+ const { i18n } = useTranslationContext();
+
+ return
+
+
+
+
+
+
+ ;
+}
+
+function MaybeBusinessButton({
+ account,
+ onClick,
+}: {
+ account: string;
+ onClick: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useBusinessAccountDetails(account);
+ if (!result.ok) return ;
+ return (
+
+ {
+ e.preventDefault()
+ onClick()
+ }}
+ >
+ Business Profile
+
+
+ );
+}
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx
deleted file mode 100644
index ce0feebce..000000000
--- a/packages/demobank-ui/src/pages/AdminPage.tsx
+++ /dev/null
@@ -1,1064 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see
- */
-
-import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
-import {
- ErrorType,
- HttpResponsePaginated,
- RequestError,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Cashouts } from "../components/Cashouts/index.js";
-import { useBackendContext } from "../context/backend.js";
-import { useAccountDetails } from "../hooks/access.js";
-import {
- useAdminAccountAPI,
- useBusinessAccountDetails,
- useBusinessAccounts,
-} from "../hooks/circuit.js";
-import {
- buildRequestErrorMessage,
- PartialButDefined,
- RecursivePartial,
- undefinedIfEmpty,
- validateIBAN,
- WithIntermediate,
-} from "../utils.js";
-import { ErrorBannerFloat } from "./BankFrame.js";
-import { ShowCashoutDetails } from "./BusinessAccount.js";
-import { handleNotOkResult } from "./HomePage.js";
-import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { ErrorMessage, notifyInfo } from "../hooks/notification.js";
-
-const charset =
- "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
-const upperIdx = charset.indexOf("A");
-
-function randomPassword(): string {
- const random = Array.from({ length: 16 }).map(() => {
- return charset.charCodeAt(Math.random() * charset.length);
- });
- // first char can't be upper
- const charIdx = charset.indexOf(String.fromCharCode(random[0]));
- random[0] =
- charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0];
- return String.fromCharCode(...random);
-}
-
-interface Props {
- onRegister: () => void;
-}
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-export function AdminPage({ onRegister }: Props): VNode {
- const [account, setAccount] = useState();
- const [showDetails, setShowDetails] = useState();
- const [showCashouts, setShowCashouts] = useState();
- const [updatePassword, setUpdatePassword] = useState();
- const [removeAccount, setRemoveAccount] = useState();
- const [showCashoutDetails, setShowCashoutDetails] = useState<
- string | undefined
- >();
-
- const [createAccount, setCreateAccount] = useState(false);
-
- const result = useBusinessAccounts({ account });
- const { i18n } = useTranslationContext();
-
- if (result.loading) return
;
- if (!result.ok) {
- return handleNotOkResult(i18n, onRegister)(result);
- }
-
- const { customers } = result.data;
-
- if (showCashoutDetails) {
- return (
- {
- setShowCashoutDetails(undefined);
- }}
- />
- );
- }
-
- if (showCashouts) {
- return (
-
-
-
- Cashout for account {showCashouts}
-
-
-
{
- setShowCashouts(id);
- setShowCashouts(undefined);
- }}
- />
-
- {
- e.preventDefault();
- setShowCashouts(undefined);
- }}
- />
-
-
- );
- }
-
- if (showDetails) {
- return (
- {
- setUpdatePassword(showDetails);
- setShowDetails(undefined);
- }}
- onUpdateSuccess={() => {
- notifyInfo(i18n.str`Account updated`);
- setShowDetails(undefined);
- }}
- onClear={() => {
- setShowDetails(undefined);
- }}
- />
- );
- }
- if (removeAccount) {
- return (
- {
- notifyInfo(i18n.str`Account removed`);
- setRemoveAccount(undefined);
- }}
- onClear={() => {
- setRemoveAccount(undefined);
- }}
- />
- );
- }
- if (updatePassword) {
- return (
- {
- notifyInfo(i18n.str`Password changed`);
- setUpdatePassword(undefined);
- }}
- onClear={() => {
- setUpdatePassword(undefined);
- }}
- />
- );
- }
- if (createAccount) {
- return (
- setCreateAccount(false)}
- onCreateSuccess={(password) => {
- notifyInfo(
- i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
- );
- setCreateAccount(false);
- }}
- />
- );
- }
-
- return (
-
-
-
- Admin panel
-
-
-
-
-
-
-
- {
- e.preventDefault();
-
- setCreateAccount(true);
- }}
- />
-
-
-
-
-
-
- {!customers.length ? (
-
- ) : (
-
- {i18n.str`Accounts:`}
-
-
- )}
-
-
- );
-}
-
-function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
- const { i18n } = useTranslationContext();
- const r = useBackendContext();
- const account = r.state.status === "loggedIn" ? r.state.username : "admin";
- const result = useAccountDetails(account);
-
- if (!result.ok) {
- return handleNotOkResult(i18n, onRegister)(result);
- }
- const { data } = result;
- const balance = Amounts.parseOrThrow(data.balance.amount);
- const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
- const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
- const limit = balanceIsDebit
- ? Amounts.sub(debitThreshold, balance).amount
- : Amounts.add(balance, debitThreshold).amount;
- if (!balance) return ;
- return (
-
-
-
-
{i18n.str`Bank account balance`}
- {!balance ? (
-
- Waiting server response...
-
- ) : (
-
- {balanceIsDebit ? - : null}
- {`${Amounts.stringifyValue(balance)}`}
-
- {`${balance.currency}`}
-
- )}
-
-
- {
- notifyInfo(i18n.str`Wire transfer created!`);
- }}
- />
-
- );
-}
-
-const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
-const EMAIL_REGEX =
- /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
-
-function initializeFromTemplate(
- account: SandboxBackend.Circuit.CircuitAccountData | undefined,
-): WithIntermediate {
- const emptyAccount = {
- cashout_address: undefined,
- iban: undefined,
- name: undefined,
- username: undefined,
- contact_data: undefined,
- };
- const emptyContact = {
- email: undefined,
- phone: undefined,
- };
-
- const initial: PartialButDefined =
- structuredClone(account) ?? emptyAccount;
- if (typeof initial.contact_data === "undefined") {
- initial.contact_data = emptyContact;
- }
- initial.contact_data.email;
- return initial as any;
-}
-
-export function UpdateAccountPassword({
- account,
- onClear,
- onUpdateSuccess,
- onLoadNotOk,
-}: {
- onLoadNotOk: (
- error: HttpResponsePaginated,
- ) => VNode;
- onClear: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useBusinessAccountDetails(account);
- const { changePassword } = useAdminAccountAPI();
- const [password, setPassword] = useState();
- const [repeat, setRepeat] = useState();
- const [error, saveError] = useState();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return account not found
;
- }
- return onLoadNotOk(result);
- }
-
- const errors = undefinedIfEmpty({
- password: !password ? i18n.str`required` : undefined,
- repeat: !repeat
- ? i18n.str`required`
- : password !== repeat
- ? i18n.str`password doesn't match`
- : undefined,
- });
-
- return (
-
-
-
- Update password for {account}
-
-
- {error && (
-
saveError(undefined)} />
- )}
-
-
-
- );
-}
-
-function CreateNewAccount({
- onClose,
- onCreateSuccess,
-}: {
- onClose: () => void;
- onCreateSuccess: (password: string) => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { createAccount } = useAdminAccountAPI();
- const [submitAccount, setSubmitAccount] = useState<
- SandboxBackend.Circuit.CircuitAccountData | undefined
- >();
- const [error, saveError] = useState();
- return (
-
-
-
- New account
-
-
- {error && (
-
saveError(undefined)} />
- )}
-
-
-
{
- setSubmitAccount(a);
- }}
- />
-
-
-
-
- {
- e.preventDefault();
- onClose();
- }}
- />
-
-
- {
- e.preventDefault();
-
- if (!submitAccount) return;
- try {
- const account: SandboxBackend.Circuit.CircuitAccountRequest =
- {
- cashout_address: submitAccount.cashout_address,
- contact_data: submitAccount.contact_data,
- internal_iban: submitAccount.iban,
- name: submitAccount.name,
- username: submitAccount.username,
- password: randomPassword(),
- };
-
- await createAccount(account);
- onCreateSuccess(account.password);
- } catch (error) {
- if (error instanceof RequestError) {
- saveError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Forbidden
- ? i18n.str`The rights to perform the operation are not sufficient`
- : status === HttpStatusCode.BadRequest
- ? i18n.str`Input data was invalid`
- : status === HttpStatusCode.Conflict
- ? i18n.str`At least one registration detail was not available`
- : undefined,
- }),
- );
- } else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
-
-
-
-
-
- );
-}
-
-export function ShowAccountDetails({
- account,
- onClear,
- onUpdateSuccess,
- onLoadNotOk,
- onChangePassword,
-}: {
- onLoadNotOk: (
- error: HttpResponsePaginated,
- ) => VNode;
- onClear?: () => void;
- onChangePassword: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useBusinessAccountDetails(account);
- const { updateAccount } = useAdminAccountAPI();
- const [update, setUpdate] = useState(false);
- const [submitAccount, setSubmitAccount] = useState<
- SandboxBackend.Circuit.CircuitAccountData | undefined
- >();
- const [error, saveError] = useState();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return account not found
;
- }
- return onLoadNotOk(result);
- }
-
- return (
-
-
-
- Business account details
-
-
- {error && (
-
saveError(undefined)} />
- )}
-
-
setSubmitAccount(a)}
- />
-
-
-
-
- {onClear ? (
- {
- e.preventDefault();
- onClear();
- }}
- />
- ) : undefined}
-
-
-
- {
- e.preventDefault();
- onChangePassword();
- }}
- />
-
-
- {
- e.preventDefault();
-
- if (!update) {
- setUpdate(true);
- } else {
- if (!submitAccount) return;
- try {
- await updateAccount(account, {
- cashout_address: submitAccount.cashout_address,
- contact_data: submitAccount.contact_data,
- });
- onUpdateSuccess();
- } catch (error) {
- if (error instanceof RequestError) {
- saveError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Forbidden
- ? i18n.str`The rights to change the account are not sufficient`
- : status === HttpStatusCode.NotFound
- ? i18n.str`The username was not found`
- : undefined,
- }),
- );
- } else {
- saveError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }
- }}
- />
-
-
-
-
-
-
- );
-}
-
-function RemoveAccount({
- account,
- onClear,
- onUpdateSuccess,
- onLoadNotOk,
-}: {
- onLoadNotOk: (
- error: HttpResponsePaginated,
- ) => VNode;
- onClear: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useAccountDetails(account);
- const { deleteAccount } = useAdminAccountAPI();
- const [error, saveError] = useState();
-
- if (!result.ok) {
- if (result.loading || result.type === ErrorType.TIMEOUT) {
- return onLoadNotOk(result);
- }
- if (result.status === HttpStatusCode.NotFound) {
- return account not found
;
- }
- return onLoadNotOk(result);
- }
-
- const balance = Amounts.parse(result.data.balance.amount);
- if (!balance) {
- return there was an error reading the balance
;
- }
- const isBalanceEmpty = Amounts.isZero(balance);
- return (
-
-
-
- Remove account: {account}
-
-
- {!isBalanceEmpty && (
-
saveError(undefined)}
- />
- )}
- {error && (
- saveError(undefined)} />
- )}
-
-
-
-
-
- );
-}
-/**
- * Create valid account object to update or create
- * Take template as initial values for the form
- * Purpose indicate if all field al read only (show), part of them (update)
- * or none (create)
- * @param param0
- * @returns
- */
-function AccountForm({
- template,
- purpose,
- onChange,
-}: {
- template: SandboxBackend.Circuit.CircuitAccountData | undefined;
- onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
- purpose: "create" | "update" | "show";
-}): VNode {
- const initial = initializeFromTemplate(template);
- const [form, setForm] = useState(initial);
- const [errors, setErrors] = useState<
- RecursivePartial | undefined
- >(undefined);
- const { i18n } = useTranslationContext();
-
- function updateForm(newForm: typeof initial): void {
- const parsed = !newForm.cashout_address
- ? undefined
- : parsePaytoUri(newForm.cashout_address);
-
- const errors = undefinedIfEmpty>({
- cashout_address: !newForm.cashout_address
- ? i18n.str`required`
- : !parsed
- ? i18n.str`does not follow the pattern`
- : !parsed.isKnown || parsed.targetType !== "iban"
- ? i18n.str`only "IBAN" target are supported`
- : !IBAN_REGEX.test(parsed.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(parsed.iban, i18n),
- contact_data: undefinedIfEmpty({
- email: !newForm.contact_data?.email
- ? i18n.str`required`
- : !EMAIL_REGEX.test(newForm.contact_data.email)
- ? i18n.str`it should be an email`
- : undefined,
- phone: !newForm.contact_data?.phone
- ? i18n.str`required`
- : !newForm.contact_data.phone.startsWith("+")
- ? i18n.str`should start with +`
- : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
- ? i18n.str`phone number can't have other than numbers`
- : undefined,
- }),
- iban: !newForm.iban
- ? undefined //optional field
- : !IBAN_REGEX.test(newForm.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(newForm.iban, i18n),
- name: !newForm.name ? i18n.str`required` : undefined,
- username: !newForm.username ? i18n.str`required` : undefined,
- });
- setErrors(errors);
- setForm(newForm);
- onChange(errors === undefined ? (newForm as any) : undefined);
- }
-
- return (
-
- );
-}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index dc61f1302..6ab6ba3e4 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -14,283 +14,362 @@
GNU Taler; see the file COPYING. If not, see
*/
-import { Logger, TranslatedString } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { StateUpdater, useEffect, useState } from "preact/hooks";
-import talerLogo from "../assets/logo-white.svg";
-import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
+import { Amounts, Logger, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util";
+import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import logo from "../assets/logo-2021.svg";
+import { Attention } from "../components/Attention.js";
+import { CopyButton } from "../components/CopyButton.js";
+import { LangSelector } from "../components/LangSelector.js";
import { useBackendContext } from "../context/backend.js";
-import { useBusinessAccountDetails } from "../hooks/circuit.js";
-import { bankUiSettings } from "../settings.js";
+import { useAccountDetails } from "../hooks/access.js";
import { useSettings } from "../hooks/settings.js";
-import { ErrorMessage, onNotificationUpdate } from "../hooks/notification.js";
+import { bankUiSettings } from "../settings.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
-const IS_PUBLIC_ACCOUNT_ENABLED = false;
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
const versionText = VERSION
? GIT_HASH
- ? `Version ${VERSION} (${GIT_HASH.substring(0, 8)})`
+ ?
+ Version {VERSION} ({GIT_HASH.substring(0, 8)})
+
: VERSION
: "";
-const logger = new Logger("BankFrame");
-
-function MaybeBusinessButton({
- account,
- onClick,
-}: {
- account: string;
- onClick: () => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useBusinessAccountDetails(account);
- if (!result.ok) return ;
- return (
- {
- e.preventDefault();
- onClick();
- }}
- >{i18n.str`Business Profile`}
- );
-}
export function BankFrame({
children,
- goToBusinessAccount,
+ account,
}: {
+ account?: string,
children: ComponentChildren;
- goToBusinessAccount?: () => void;
}): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendContext();
const [settings, updateSettings] = useSettings();
+ const [open, setOpen] = useState(false)
+
+ const [error, resetError] = useErrorBoundary();
+
+ useEffect(() => {
+ if (error) {
+ const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString
+ if (error instanceof Error) {
+ notifyException(i18n.str`Internal error, please report.`, error)
+ } else {
+ notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString)
+ }
+ resetError()
+ }
+ }, [error])
const demo_sites = [];
- for (const i in bankUiSettings.demoSites)
- demo_sites.push(
-
- {bankUiSettings.demoSites[i][0]}
- ,
- );
-
- return (
-
-
-
-
-
-
- );
-}
-
-function maybeDemoContent(content: VNode): VNode {
- if (bankUiSettings.showDemoNav) {
- return content;
+ if (bankUiSettings.demoSites) {
+ for (const i in bankUiSettings.demoSites)
+ demo_sites.push(
+
+ {bankUiSettings.demoSites[i][0]}
+ ,
+ );
}
- return ;
-}
-export function ErrorBannerFloat({
- error,
- onClear,
-}: {
- error: ErrorMessage;
- onClear?: () => void;
-}): VNode {
- return (
-
-
-
- );
-}
+ return (
+
+
+
+
+
+
+ {bankUiSettings.demoSites &&
+
+
+ {/* */}
+ {bankUiSettings.demoSites.map(([name, url]) => {
+ return
{name}
+ })}
+
+
+ }
+
-function ErrorBanner({
- error,
- onClear,
-}: {
- error: ErrorMessage;
- onClear?: () => void;
-}): VNode {
- return (
-
- );
-}
-
-function StatusBanner(): VNode | null {
- const [info, setInfo] = useState
();
- const [error, setError] = useState();
- useEffect(() => {
- return onNotificationUpdate((newValue) => {
- if (newValue === undefined) {
- setInfo(undefined);
- setError(undefined);
- } else {
- if (newValue.type === "error") {
- setError(newValue.error);
- } else {
- setInfo(newValue.info);
- }
- }
- });
- }, []);
- return (
-
- {!info ? undefined : (
-
+
+ {open &&
+
{
+ setOpen(false)
+ }}>
+
+
+
+
+ }
+
+
+ {account &&
+
+ }
+
+
+
+
+
+
+
+
+
+
);
}
+function MaybeShowDebugInfo({ info }: { info: any }): VNode {
+ const [settings] = useSettings()
+ if (settings.showDebugInfo) {
+ return
+ {info}
+
+ }
+ return
+}
+
+
+function StatusBanner(): VNode {
+ const notifs = useNotifications()
+ if (notifs.length === 0) return
+ return
{
+ notifs.map(n => {
+ switch (n.message.type) {
+ case "error":
+ return
{
+ n.remove()
+ }}>
+ {n.message.description &&
+
+ {n.message.description}
+
+ }
+
+ {/*
+ show debug info
+
+ {n.message.debug &&
+
+ {n.message.debug}
+
+ } */}
+
+ case "info":
+ return
{
+ n.remove();
+ }} />
+ }
+ })}
+
+
+}
+
function TestingTag(): VNode {
const testingUrl = localStorage.getItem("bank-base-url");
if (!testingUrl) return
;
return (
-
+
Testing with {testingUrl}{" "}
stop testing
-
+
);
}
+
+function Footer() {
+ const { i18n } = useTranslationContext()
+ return (
+
+ );
+}
+
+function WelcomeAccount({ account }: { account: string }): VNode {
+ const { i18n } = useTranslationContext();
+
+ const result = useAccountDetails(account);
+ if (!result.ok) return
+
+ const payto = parsePaytoUri(result.data.payto_uri)
+ if (!payto) return
+
+ const accountNumber = !payto.isKnown ? undefined : payto.targetType === "iban" ? payto.iban : payto.targetType === "x-taler-bank" ? payto.account : undefined;
+ return
+ Welcome, {account} {accountNumber !== undefined ?
+
+ ({accountNumber} result.data.payto_uri} />)
+
+ : }!
+
+
+}
+
+function AccountBalance({ account }: { account: string }): VNode {
+ const result = useAccountDetails(account);
+ if (!result.ok) return
+
+ return
+}
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx
index 93a9bdfae..95144f086 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -17,6 +17,7 @@
import {
HttpStatusCode,
Logger,
+ TranslatedString,
parseWithdrawUri,
stringifyWithdrawUri,
} from "@gnu-taler/taler-util";
@@ -24,18 +25,18 @@ import {
ErrorType,
HttpResponse,
HttpResponsePaginated,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { Loading } from "../components/Loading.js";
-import { useBackendContext } from "../context/backend.js";
import { getInitialBackendBaseURL } from "../hooks/backend.js";
-import { notifyError, notifyInfo } from "../hooks/notification.js";
import { useSettings } from "../hooks/settings.js";
-import { AccountPage } from "./AccountPage.js";
-import { AdminPage } from "./AdminPage.js";
+import { AccountPage } from "./AccountPage/index.js";
import { LoginForm } from "./LoginForm.js";
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
+import { route } from "preact-router";
const logger = new Logger("AccountPage");
@@ -51,73 +52,66 @@ const logger = new Logger("AccountPage");
*/
export function HomePage({
onRegister,
- onPendingOperationFound,
+ account,
+ goToConfirmOperation,
+ goToBusinessAccount,
}: {
- onPendingOperationFound: (id: string) => void;
+ account: string,
onRegister: () => void;
+ goToBusinessAccount: () => void;
+ goToConfirmOperation: (id: string) => void;
}): VNode {
- const backend = useBackendContext();
- const [settings] = useSettings();
const { i18n } = useTranslationContext();
- if (backend.state.status === "loggedOut") {
- return
;
- }
-
- if (settings.currentWithdrawalOperationId) {
- onPendingOperationFound(settings.currentWithdrawalOperationId);
- return
;
- }
-
- if (backend.state.isUserAdministrator) {
- return
;
- }
-
return (
);
}
export function WithdrawalOperationPage({
operationId,
- onLoadNotOk,
onContinue,
}: {
operationId: string;
- onLoadNotOk: () => void;
onContinue: () => void;
}): VNode {
//FIXME: libeufin sandbox should return show to create the integration api endpoint
//or return withdrawal uri from response
+ const baseUrl = getInitialBackendBaseURL()
const uri = stringifyWithdrawUri({
- bankIntegrationApiBaseUrl: `${getInitialBackendBaseURL()}/integration-api`,
+ bankIntegrationApiBaseUrl: `${baseUrl}/taler-integration`,
withdrawalOperationId: operationId,
});
const parsedUri = parseWithdrawUri(uri);
const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings();
if (!parsedUri) {
- notifyError({
- title: i18n.str`The Withdrawal URI is not valid: "${uri}"`,
- });
+ notifyError(
+ i18n.str`The Withdrawal URI is not valid`,
+ uri as TranslatedString
+ );
return
;
}
return (
{
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onContinue()
+ }}
/>
);
}
export function handleNotOkResult(
i18n: ReturnType["i18n"],
- onRegister?: () => void,
): (
result:
| HttpResponsePaginated
@@ -125,53 +119,53 @@ export function handleNotOkResult(
) => VNode {
return function handleNotOkResult2(
result:
- | HttpResponsePaginated
- | HttpResponse,
+ | HttpResponsePaginated
+ | HttpResponse,
): VNode {
if (result.loading) return ;
if (!result.ok) {
switch (result.type) {
case ErrorType.TIMEOUT: {
- notifyError({
- title: i18n.str`Request timeout, try again later.`,
- });
+ notifyError(i18n.str`Request timeout, try again later.`, undefined);
break;
}
case ErrorType.CLIENT: {
if (result.status === HttpStatusCode.Unauthorized) {
- notifyError({
- title: i18n.str`Wrong credentials`,
- });
- return ;
+ notifyError(i18n.str`Wrong credentials`, undefined);
+ return ;
}
const errorData = result.payload;
- notifyError({
- title: i18n.str`Could not load due to a client error`,
- description: errorData.error.description,
+ notify({
+ type: "error",
+ title: i18n.str`Could not load due to a request error`,
+ description: i18n.str`Request to url "${result.info.url}" returned ${result.info.status}`,
debug: JSON.stringify(result),
});
break;
}
case ErrorType.SERVER: {
- notifyError({
+ notify({
+ type: "error",
title: i18n.str`Server returned with error`,
- description: result.payload.error.description,
+ description: result.payload?.error?.description as TranslatedString,
debug: JSON.stringify(result.payload),
});
break;
}
case ErrorType.UNREADABLE: {
- notifyError({
+ notify({
+ type: "error",
title: i18n.str`Unexpected error.`,
- description: `Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
+ description: i18n.str`Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
debug: JSON.stringify(result),
});
break;
}
case ErrorType.UNEXPECTED: {
- notifyError({
+ notify({
+ type: "error",
title: i18n.str`Unexpected error.`,
- description: `Diagnostic from ${result.info?.url} is "${result.message}"`,
+ description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`,
debug: JSON.stringify(result),
});
break;
@@ -180,7 +174,7 @@ export function handleNotOkResult(
assertUnreachable(result);
}
}
-
+ // route("/")
return error
;
}
return
;
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index d2cb1bd8e..3ea94b899 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,199 +14,249 @@
GNU Taler; see the file COPYING. If not, see
*/
-import { HttpStatusCode } from "@gnu-taler/taler-util";
-import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { ErrorType, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useBackendContext } from "../context/backend.js";
-import { useCredentialsChecker } from "../hooks/backend.js";
-import { ErrorMessage } from "../hooks/notification.js";
+import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
import { bankUiSettings } from "../settings.js";
import { undefinedIfEmpty } from "../utils.js";
-import { ErrorBannerFloat } from "./BankFrame.js";
-import { USERNAME_REGEX } from "./RegistrationPage.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+
/**
* Collect and submit login data.
*/
export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
const backend = useBackendContext();
- const [username, setUsername] = useState();
+ const currentUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined
+ const [username, setUsername] = useState(currentUser);
const [password, setPassword] = useState();
const { i18n } = useTranslationContext();
- const testLogin = useCredentialsChecker();
- const [error, saveError] = useState();
+ const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
+
+
+ /**
+ * Register form may be shown in the initialization step.
+ * If this is an error when usgin the app the registration
+ * callback is not set
+ */
+ const isSessionExpired = !onRegister
+
+ // useEffect(() => {
+ // if (backend.state.status === "loggedIn") {
+ // backend.expired()
+ // }
+ // },[])
const ref = useRef(null);
useEffect(function focusInput() {
+ //FIXME: show invalidate session and allow relogin
+ if (isSessionExpired) {
+ localStorage.removeItem("backend-state");
+ window.location.reload()
+ }
ref.current?.focus();
}, []);
+ const [busy, setBusy] = useState>()
const errors = undefinedIfEmpty({
username: !username
? i18n.str`Missing username`
- : !USERNAME_REGEX.test(username)
- ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ // : !USERNAME_REGEX.test(username)
+ // ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
: undefined,
password: !password ? i18n.str`Missing password` : undefined,
- });
+ }) ?? busy;
+
+ function saveError({ title, description, debug }: { title: TranslatedString, description?: TranslatedString, debug?: any }) {
+ notifyError(title, description, debug)
+ }
+
+ async function doLogout() {
+ backend.logOut()
+ }
+
+ async function doLogin() {
+ if (!username || !password) return;
+ setBusy({})
+ const result = await requestNewLoginToken(username, password);
+ if (result.valid) {
+ backend.logIn({ username, token: result.token });
+ } else {
+ const { cause } = result;
+ switch (cause.type) {
+ case ErrorType.CLIENT: {
+ if (cause.status === HttpStatusCode.Unauthorized) {
+ saveError({
+ title: i18n.str`Wrong credentials for "${username}"`,
+ });
+ } else
+ if (cause.status === HttpStatusCode.NotFound) {
+ saveError({
+ title: i18n.str`Account not found`,
+ });
+ } else {
+ saveError({
+ title: i18n.str`Could not load due to a request error`,
+ description: i18n.str`Request to url "${cause.info.url}" returned ${cause.info.status}`,
+ debug: JSON.stringify(cause.payload),
+ });
+ }
+ break;
+ }
+ case ErrorType.SERVER: {
+ saveError({
+ title: i18n.str`Server had a problem, try again later or report.`,
+ // description: cause.payload.error.description,
+ debug: JSON.stringify(cause.payload),
+ });
+ break;
+ }
+ case ErrorType.TIMEOUT: {
+ saveError({
+ title: i18n.str`Request timeout, try again later.`,
+ });
+ break;
+ }
+ case ErrorType.UNREADABLE: {
+ saveError({
+ title: i18n.str`Unexpected error.`,
+ description: `Response from ${cause.info?.url} is unreadable, http status: ${cause.status}` as TranslatedString,
+ debug: JSON.stringify(cause),
+ });
+ break;
+ }
+ default: {
+ saveError({
+ title: i18n.str`Unexpected error, please report.`,
+ description: `Diagnostic from ${cause.info?.url} is "${cause.message}"` as TranslatedString,
+ debug: JSON.stringify(cause),
+ });
+ break;
+ }
+ }
+ // backend.logOut();
+ }
+ setPassword(undefined);
+ setBusy(undefined)
+ }
return (
-
- {i18n.str`Welcome to ${bankUiSettings.bankName}!`}
- {error && (
- saveError(undefined)} />
- )}
-
);
}
diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts
new file mode 100644
index 000000000..b347fd942
--- /dev/null
+++ b/packages/demobank-ui/src/pages/OperationState/index.ts
@@ -0,0 +1,122 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see
+ */
+
+import { AbsoluteTime, AmountJson, WithdrawUriResult } from "@gnu-taler/taler-util";
+import { HttpError, utils } from "@gnu-taler/web-util/browser";
+import { ErrorLoading } from "../../components/ErrorLoading.js";
+import { Loading } from "../../components/Loading.js";
+import { useComponentState } from "./state.js";
+import { AbortedView, ConfirmedView, InvalidPaytoView, InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView, ReadyView } from "./views.js";
+
+export interface Props {
+ currency: string;
+ onClose: () => void;
+}
+
+export type State = State.Loading |
+ State.LoadingError |
+ State.Ready |
+ State.Aborted |
+ State.Confirmed |
+ State.InvalidPayto |
+ State.InvalidWithdrawal |
+ State.InvalidReserve |
+ State.NeedConfirmation;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingError {
+ status: "loading-error";
+ error: HttpError;
+ }
+
+ /**
+ * Need to open the wallet
+ */
+ export interface Ready {
+ status: "ready";
+ error: undefined;
+ uri: WithdrawUriResult,
+ onClose: () => void;
+ onAbort: () => void;
+ }
+
+ export interface InvalidPayto {
+ status: "invalid-payto",
+ error: undefined;
+ payto: string | null;
+ onClose: () => void;
+ }
+ export interface InvalidWithdrawal {
+ status: "invalid-withdrawal",
+ error: undefined;
+ onClose: () => void;
+ uri: string,
+ }
+ export interface InvalidReserve {
+ status: "invalid-reserve",
+ error: undefined;
+ onClose: () => void;
+ reserve: string | null;
+ }
+ export interface NeedConfirmation {
+ status: "need-confirmation",
+ onAbort: () => void;
+ onConfirm: () => void;
+ error: undefined;
+ busy: boolean,
+ }
+ export interface Aborted {
+ status: "aborted",
+ error: undefined;
+ onClose: () => void;
+ }
+ export interface Confirmed {
+ status: "confirmed",
+ error: undefined;
+ onClose: () => void;
+ }
+
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap = {
+ loading: Loading,
+ "invalid-payto": InvalidPaytoView,
+ "invalid-withdrawal": InvalidWithdrawalView,
+ "invalid-reserve": InvalidReserveView,
+ "need-confirmation": NeedConfirmationView,
+ "aborted": AbortedView,
+ "confirmed": ConfirmedView,
+ "loading-error": ErrorLoading,
+ ready: ReadyView,
+};
+
+export const OperationState = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts
new file mode 100644
index 000000000..4be680377
--- /dev/null
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -0,0 +1,265 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see
+ */
+
+import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { RequestError, notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser";
+import { useEffect, useState } from "preact/hooks";
+import { useAccessAPI, useAccessAnonAPI, useWithdrawalDetails } from "../../hooks/access.js";
+import { getInitialBackendBaseURL } from "../../hooks/backend.js";
+import { useSettings } from "../../hooks/settings.js";
+import { buildRequestErrorMessage } from "../../utils.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({ currency, onClose }: Props): utils.RecursiveState {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+ const { createWithdrawal } = useAccessAPI();
+ const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI();
+ const [busy, setBusy] = useState>()
+
+ const amount = settings.maxWithdrawalAmount
+
+ async function doSilentStart() {
+ //FIXME: if amount is not enough use balance
+ const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
+
+ try {
+ const result = await createWithdrawal({
+ amount: Amounts.stringify(parsedAmount),
+ });
+ const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
+ if (!uri) {
+ return notifyError(
+ i18n.str`Server responded with an invalid withdraw URI`,
+ i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
+ } else {
+ updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The operation was rejected due to insufficient funds`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
+ const withdrawalOperationId = settings.currentWithdrawalOperationId
+ useEffect(() => {
+ if (withdrawalOperationId === undefined) {
+ doSilentStart()
+ }
+ }, [settings.fastWithdrawal, amount])
+
+ const baseUrl = getInitialBackendBaseURL()
+
+ if (!withdrawalOperationId) {
+ return {
+ status: "loading",
+ error: undefined
+ }
+ }
+
+ const wid = withdrawalOperationId
+
+ async function doAbort() {
+ try {
+ setBusy({})
+ await abortWithdrawal(wid);
+ onClose();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+
+ async function doConfirm() {
+ try {
+ setBusy({})
+ await confirmWithdrawal(wid);
+ if (!settings.showWithdrawalSuccess) {
+ notifyInfo(i18n.str`Wire transfer completed!`)
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
+ : status === HttpStatusCode.UnprocessableEntity
+ ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+ const bankIntegrationApiBaseUrl = `${baseUrl}/taler-integration`
+ const uri = stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl,
+ withdrawalOperationId,
+ });
+ const parsedUri = parseWithdrawUri(uri);
+ if (!parsedUri) {
+ return {
+ status: "invalid-withdrawal",
+ error: undefined,
+ uri,
+ onClose,
+ }
+ }
+
+ return (): utils.RecursiveState => {
+ const result = useWithdrawalDetails(withdrawalOperationId);
+ const shouldCreateNewOperation = !result.ok && !result.loading && result.info.status === HttpStatusCode.NotFound
+
+ useEffect(() => {
+ if (shouldCreateNewOperation) {
+ doSilentStart()
+ }
+ }, [])
+ if (!result.ok) {
+ if (result.loading) {
+ return {
+ status: "loading",
+ error: undefined
+ }
+ }
+ if (result.info.status === HttpStatusCode.NotFound) {
+ return {
+ status: "loading",
+ error: undefined,
+ }
+ }
+ return {
+ status: "loading-error",
+ error: result
+ }
+ }
+ const { data } = result;
+ if (data.aborted) {
+ return {
+ status: "aborted",
+ error: undefined,
+ onClose: async () => {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ }
+ }
+
+ if (data.confirmation_done) {
+ if (!settings.showWithdrawalSuccess) {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ }
+ return {
+ status: "confirmed",
+ error: undefined,
+ onClose: async () => {
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ }
+ }
+
+ if (!data.selection_done) {
+ return {
+ status: "ready",
+ error: undefined,
+ uri: parsedUri,
+ onClose: async () => {
+ await doAbort()
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ onAbort: doAbort,
+ }
+ }
+
+ if (!data.selected_reserve_pub) {
+ return {
+ status: "invalid-reserve",
+ error: undefined,
+ reserve: data.selected_reserve_pub,
+ onClose,
+ }
+ }
+
+ const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account)
+
+ if (!account) {
+ return {
+ status: "invalid-payto",
+ error: undefined,
+ payto: data.selected_exchange_account,
+ onClose,
+ }
+ }
+
+
+ // goToConfirmOperation(withdrawalOperationId)
+ return {
+ status: "need-confirmation",
+ error: undefined,
+ onAbort: async () => {
+ await doAbort()
+ updateSettings("currentWithdrawalOperationId", undefined)
+ onClose()
+ },
+ busy: !!busy,
+ onConfirm: doConfirm
+ }
+ }
+
+}
diff --git a/packages/demobank-ui/src/scss/_tiles.scss b/packages/demobank-ui/src/pages/OperationState/stories.tsx
similarity index 75%
rename from packages/demobank-ui/src/scss/_tiles.scss
rename to packages/demobank-ui/src/pages/OperationState/stories.tsx
index e69d995f0..03917a8fb 100644
--- a/packages/demobank-ui/src/scss/_tiles.scss
+++ b/packages/demobank-ui/src/pages/OperationState/stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,6 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-.is-tiles-wrapper {
- margin-bottom: $default-padding;
-}
+import * as tests from "@gnu-taler/web-util/testing";
+import { ReadyView } from "./views.js";
+
+export default {
+ title: "operation status page",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/demobank-ui/src/scss/_modal.scss b/packages/demobank-ui/src/pages/OperationState/test.ts
similarity index 62%
rename from packages/demobank-ui/src/scss/_modal.scss
rename to packages/demobank-ui/src/pages/OperationState/test.ts
index b3a31ebf1..f4d6cf4b2 100644
--- a/packages/demobank-ui/src/scss/_modal.scss
+++ b/packages/demobank-ui/src/pages/OperationState/test.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
+ (C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -19,17 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-.modal-card {
- width: $modal-card-width;
-}
+import * as tests from "@gnu-taler/web-util/testing";
+import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
-.modal-card-foot {
- background-color: $modal-card-foot-background-color;
-}
-
-@include mobile {
- .modal .animation-content .modal-card {
- width: $modal-card-width-mobile;
- margin: 0 auto;
- }
-}
+describe("Withdrawal operation states", () => {
+ it("should do some tests", async () => {
+ });
+});
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx
new file mode 100644
index 000000000..2cb7385db
--- /dev/null
+++ b/packages/demobank-ui/src/pages/OperationState/views.tsx
@@ -0,0 +1,376 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see
+ */
+
+import { stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useMemo, useState } from "preact/hooks";
+import { QR } from "../../components/QR.js";
+import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { useSettings } from "../../hooks/settings.js";
+import { undefinedIfEmpty } from "../../utils.js";
+import { State } from "./index.js";
+
+export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) {
+ return (
+ Payto from server is not valid "{payto}"
+ );
+}
+export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) {
+ return (
+ Withdrawal uri from server is not valid "{uri}"
+ );
+}
+export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) {
+ return (
+ Reserve from server is not valid "{reserve}"
+ );
+}
+
+export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State.NeedConfirmation) {
+ const { i18n } = useTranslationContext()
+
+ const captchaNumbers = useMemo(() => {
+ return {
+ a: Math.floor(Math.random() * 10),
+ b: Math.floor(Math.random() * 10),
+ };
+ }, []);
+ const [captchaAnswer, setCaptchaAnswer] = useState();
+ const answer = parseInt(captchaAnswer ?? "", 10);
+ const errors = undefinedIfEmpty({
+ answer: !captchaAnswer
+ ? i18n.str`Answer the question before continue`
+ : Number.isNaN(answer)
+ ? i18n.str`The answer should be a number`
+ : answer !== captchaNumbers.a + captchaNumbers.b
+ ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
+ : undefined,
+ }) ?? (busy ? {} as Record : undefined);
+
+ return (
+
+
+
+ Confirm the withdrawal operation
+
+
+
+
+
{
+ e.preventDefault()
+ }}
+ >
+
+
{i18n.str`What is`}
+
+ {captchaNumbers.a} + {captchaNumbers.b}
+
+ ?
+
+
+
+ {
+ setCaptchaAnswer(e.currentTarget.value)
+ }}
+ />
+
+
+
+
+
+
+ Cancel
+ {
+ e.preventDefault()
+ onConfirm()
+ }}
+ >
+ Transfer
+
+
+
+
+
+
+ {/*
+
+
Wire transfer details
+
+
+
+ {((): VNode => {
+ switch (details.account.targetType) {
+ case "iban": {
+ const p = details.account as PaytoUriIBAN
+ const name = p.params["receiver-name"]
+ return
+
+
Exchange account
+ {p.iban}
+
+ {name &&
+
+
Exchange name
+ {p.params["receiver-name"]}
+
+ }
+
+ }
+ case "x-taler-bank": {
+ const p = details.account as PaytoUriTalerBank
+ const name = p.params["receiver-name"]
+ return
+
+
Exchange account
+ {p.account}
+
+ {name &&
+
+
Exchange name
+ {p.params["receiver-name"]}
+
+ }
+
+ }
+ default:
+ return
+
Exchange account
+ {details.account.targetPath}
+
+
+ }
+ })()}
+
+
Withdrawal identification
+ {details.reserve}
+
+
+
Amount
+ To be added
+ // {/* Amounts.stringifyValue(details.amount)
+
+
+
+
*/}
+
+
+
+
+
+ );
+}
+export function AbortedView({ error, onClose }: State.Aborted) {
+ return (
+ aborted
+ );
+}
+
+export function ConfirmedView({ error, onClose }: State.Confirmed) {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+ return (
+
+
+
+
+
+
+
+ Withdrawal confirmed
+
+
+
+
+ The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.
+
+
+
+
+
+
+
+
+
+ Do not show this again
+
+
+ {
+ updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess);
+ }}>
+
+
+
+
+
+ {
+ e.preventDefault();
+ onClose()
+ }}>
+ Close
+
+
+
+
+ );
+}
+
+export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> {
+ const { i18n } = useTranslationContext();
+
+ useEffect(() => {
+ //Taler Wallet WebExtension is listening to headers response and tab updates.
+ //In the SPA there is no header response with the Taler URI so
+ //this hack manually triggers the tab update after the QR is in the DOM.
+ // WebExtension will be using
+ // https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
+ document.title = `${document.title} ${uri.withdrawalOperationId}`;
+ }, []);
+ const talerWithdrawUri = stringifyWithdrawUri(uri);
+ return
+
+ {
+ onClose()
+ }}
+ >
+ Cancel
+
+
+
+
+
+
+ On this device
+
+
+
+
+ If you are using a desktop browser you can open the popup now or click the link if you have the "Inject Taler support" option enabled.
+
+
+
+
+
+
+
+
+
+ On a mobile phone
+
+
+
+
+ Scan the QR code with your mobile device.
+
+
+
+
+
+
+
+
+
+
+
+}
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 3552da7b4..f60ba3270 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -15,10 +15,9 @@
*/
import { AmountJson } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { notifyInfo } from "../hooks/notification.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
import { useSettings } from "../hooks/settings.js";
@@ -27,60 +26,97 @@ import { useSettings } from "../hooks/settings.js";
* Let the user choose a payment option,
* then specify the details trigger the action.
*/
-export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
+export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJson, goToConfirmOperation: (id: string) => void }): VNode {
const { i18n } = useTranslationContext();
- const [settings, updateSettings] = useSettings();
+ const [settings] = useSettings();
- const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
- "charge-wallet",
- );
+ const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
return (
-
-
-
-
{
- setTab("charge-wallet");
- }}
- >
- {i18n.str`Withdraw `}
-
-
{
- setTab("wire-transfer");
- }}
- >
- {i18n.str`Wire transfer`}
-
+
+
+
+
+ Send money to
+
+
+
{tab === "charge-wallet" && (
-
-
{i18n.str`Obtain digital cash`}
- {
- updateSettings("currentWithdrawalOperationId", id);
- }}
- />
-
+ {
+ setTab(undefined)
+ }}
+ />
)}
{tab === "wire-transfer" && (
-
-
{i18n.str`Transfer to bank account`}
-
{
- notifyInfo(i18n.str`Wire transfer created!`);
- }}
- />
-
+ {
+ notifyInfo(i18n.str`Wire transfer created!`);
+ setTab(undefined)
+ }}
+ onCancel={() => {
+ setTab(undefined)
+ }}
+ />
)}
-
-
- );
+
+
+
+ )
}
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index d8c1644b1..52dbd4ff6 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -17,42 +17,51 @@
import {
AmountJson,
Amounts,
- buildPayto,
HttpStatusCode,
Logger,
+ TranslatedString,
+ buildPayto,
parsePaytoUri,
- stringifyPaytoUri,
+ stringifyPaytoUri
} from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { h, VNode, Fragment, Ref } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
-import { notifyError } from "../hooks/notification.js";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAccessAPI } from "../hooks/access.js";
import {
buildRequestErrorMessage,
undefinedIfEmpty,
validateIBAN,
} from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { useConfigState } from "../hooks/config.js";
+import { useConfigContext } from "../context/config.js";
const logger = new Logger("PaytoWireTransferForm");
export function PaytoWireTransferForm({
focus,
+ title,
onSuccess,
+ onCancel,
limit,
}: {
+ title: TranslatedString,
focus?: boolean;
onSuccess: () => void;
+ onCancel: (() => void) | undefined;
limit: AmountJson;
}): VNode {
const [isRawPayto, setIsRawPayto] = useState(false);
- const [iban, setIban] = useState
(undefined);
- const [subject, setSubject] = useState(undefined);
- const [amount, setAmount] = useState(undefined);
+ // FIXME: remove this
+ const [iban, setIban] = useState();
+ const [subject, setSubject] = useState();
+ const [amount, setAmount] = useState();
const [rawPaytoInput, rawPaytoInputSetter] = useState(
undefined,
@@ -70,295 +79,372 @@ export function PaytoWireTransferForm({
const errorsWire = undefinedIfEmpty({
iban: !iban
- ? i18n.str`Missing IBAN`
+ ? i18n.str`required`
: !IBAN_REGEX.test(iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(iban, i18n),
- subject: !subject ? i18n.str`Missing subject` : undefined,
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : validateIBAN(iban, i18n),
+ subject: !subject ? i18n.str`required` : undefined,
amount: !trimmedAmountStr
- ? i18n.str`Missing amount`
+ ? i18n.str`required`
: !parsedAmount
- ? i18n.str`Amount is not valid`
- : Amounts.isZero(parsedAmount)
- ? i18n.str`Should be greater than 0`
- : Amounts.cmp(limit, parsedAmount) === -1
- ? i18n.str`balance is not enough`
- : undefined,
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedAmount)
+ ? i18n.str`should be greater than 0`
+ : Amounts.cmp(limit, parsedAmount) === -1
+ ? i18n.str`balance is not enough`
+ : undefined,
});
const { createTransaction } = useAccessAPI();
- if (!isRawPayto)
- return (
-
-
{
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- {i18n.str`Receiver IBAN:`}
- {
- setIban(e.currentTarget.value);
- }}
- />
-
- {i18n.str`Transfer subject:`}
- {
- setSubject(e.currentTarget.value);
- }}
- />
-
- {i18n.str`Amount:`}
-
-
- {
- setAmount(e.currentTarget.value);
- }}
- />
-
-
-
- {
- e.preventDefault();
- if (!(iban && subject && amount)) {
- return;
- }
- const ibanPayto = buildPayto("iban", iban, undefined);
- ibanPayto.params.message = encodeURIComponent(subject);
- const paytoUri = stringifyPaytoUri(ibanPayto);
-
- try {
- await createTransaction({
- paytoUri,
- amount: `${limit.currency}:${amount}`,
- });
- onSuccess();
- setAmount(undefined);
- setIban(undefined);
- setSubject(undefined);
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.BadRequest
- ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- />
- {
- e.preventDefault();
- setAmount(undefined);
- setIban(undefined);
- setSubject(undefined);
- }}
- />
-
-
-
- {
- setIsRawPayto(true);
- e.preventDefault();
- }}
- >
- {i18n.str`Want to try the raw payto://-format?`}
-
-
-
- );
-
const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
const errorsPayto = undefinedIfEmpty({
rawPaytoInput: !rawPaytoInput
? i18n.str`required`
: !parsed
- ? i18n.str`does not follow the pattern`
- : !parsed.params.amount
- ? i18n.str`use the "amount" parameter to specify the amount to be transferred`
- : Amounts.parse(parsed.params.amount) === undefined
- ? i18n.str`the amount is not valid`
- : !parsed.params.message
- ? i18n.str`use the "message" parameter to specify a reference text for the transfer`
- : !parsed.isKnown || parsed.targetType !== "iban"
- ? i18n.str`only "IBAN" target are supported`
- : !IBAN_REGEX.test(parsed.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(parsed.iban, i18n),
+ ? i18n.str`does not follow the pattern`
+ : !parsed.isKnown || parsed.targetType !== "iban"
+ ? i18n.str`only "IBAN" target are supported`
+ : !parsed.params.amount
+ ? i18n.str`use the "amount" parameter to specify the amount to be transferred`
+ : Amounts.parse(parsed.params.amount) === undefined
+ ? i18n.str`the amount is not valid`
+ : !parsed.params.message
+ ? i18n.str`use the "message" parameter to specify a reference text for the transfer`
+ : !IBAN_REGEX.test(parsed.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : validateIBAN(parsed.iban, i18n),
});
- return (
-
-
{i18n.str`Transfer money to account identified by payto:// URI:`}
-
{
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
-
- {i18n.str`payto URI:`}
- {
- rawPaytoInputSetter(e.currentTarget.value);
- }}
- />
-
-
-
- Hint:
-
- payto://iban/[receiver-iban]?message=[subject]&amount=[
- {limit.currency}
- :X.Y]
-
-
-
-
- {
- if (!rawPaytoInput) {
- logger.error("Didn't get any raw Payto string!");
- return;
- }
+ async function doSend() {
+ let payto_uri: string | undefined;
- try {
- await createTransaction({
- paytoUri: rawPaytoInput,
- });
- onSuccess();
- rawPaytoInputSetter(undefined);
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.BadRequest
- ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
+ if (rawPaytoInput) {
+ payto_uri = rawPaytoInput
+ } else {
+ if (!iban || !subject) return;
+ const ibanPayto = buildPayto("iban", iban, undefined);
+ ibanPayto.params.message = encodeURIComponent(subject);
+ payto_uri = stringifyPaytoUri(ibanPayto);
+ }
+
+ try {
+ await createTransaction({
+ payto_uri,
+ amount: `${limit.currency}:${amount}`,
+ });
+ onSuccess();
+ setAmount(undefined);
+ setIban(undefined);
+ setSubject(undefined);
+ rawPaytoInputSetter(undefined)
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.BadRequest
+ ? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+
+ }
+
+ return (
+ {/**
+ * FIXME: Scan a qr code
+ */}
+
+
+
{
+ e.preventDefault()
+ }}
+ >
+
+ {!isRawPayto ?
+
+
+
+
{i18n.str`Recipient`}
+
+ {
+ setIban(e.currentTarget.value.toUpperCase());
+ }}
+ />
+
+
+
+ IBAN of the recipient's account
+
+
+
+
+
{i18n.str`Transfer subject`}
+
+ {
+ setSubject(e.currentTarget.value);
+ }}
+ />
+
+
+
some text to identify the transfer
+
+
+
+
{i18n.str`Amount`}
+
{
+ setAmount(d)
+ }}
+ />
+
+ amount to transfer
+
+
+
:
+
+
+
{i18n.str`payto URI:`}
+
+ {
+ rawPaytoInputSetter(e.currentTarget.value);
+ }}
+ />
+
+
+
+
+ }
+
+
+ {onCancel ?
+
- {i18n.str`Use wire-transfer form?`}
-
-
-
+ Cancel
+
+ :
+ }
+
{
+ e.preventDefault()
+ doSend()
+ }}
+ >
+ Send
+
+
+
+
+ )
+
+}
+
+/**
+ * Show the element when the load ended
+ * @param element
+ */
+export function doAutoFocus(element: HTMLElement | null) {
+ if (element) {
+ setTimeout(() => {
+ element.focus()
+ element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ inline: "center"
+ })
+ }, 100)
+ }
+}
+
+const FRAC_SEPARATOR = "."
+
+export function InputAmount(
+ {
+ currency,
+ name,
+ value,
+ error,
+ left,
+ onChange,
+ }: {
+ error?: string;
+ currency: string;
+ name: string;
+ left?: boolean | undefined,
+ value: string | undefined;
+ onChange?: (s: string) => void;
+ },
+ ref: Ref,
+): VNode {
+ const cfg = useConfigContext()
+ return (
+
+
+
+ {currency}
+
+
{
+ if (!onChange) return;
+ const l = e.currentTarget.value.length
+ const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR)
+ if (sep_pos !== -1 && l - sep_pos - 1 > cfg.currency_fraction_limit) {
+ e.currentTarget.value = e.currentTarget.value.substring(0, sep_pos + cfg.currency_fraction_limit + 1)
+ }
+ onChange(e.currentTarget.value);
+ }}
+ />
+
+
);
}
+
+export function RenderAmount({ value, negative }: { value: AmountJson, negative?: boolean }): VNode {
+ const cfg = useConfigContext()
+ const str = Amounts.stringifyValue(value)
+ const sep_pos = str.indexOf(FRAC_SEPARATOR)
+ if (sep_pos !== -1 && str.length - sep_pos - 1 > cfg.currency_fraction_digits) {
+ const limit = sep_pos + cfg.currency_fraction_digits + 1
+ const normal = str.substring(0, limit)
+ const small = str.substring(limit)
+ return
+ {negative ? "-" : undefined}
+ {value.currency} {normal} {small}
+
+ }
+ return
+ {negative ? "-" : undefined}
+ {value.currency} {str}
+
+}
\ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
index 03bdb78b7..680368919 100644
--- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
@@ -36,8 +36,8 @@ export function PublicHistoriesPage({}: Props): VNode {
const result = usePublicAccounts();
const [showAccount, setShowAccount] = useState(
- result.ok && result.data.publicAccounts.length > 0
- ? result.data.publicAccounts[0].accountLabel
+ result.ok && result.data.public_accounts.length > 0
+ ? result.data.public_accounts[0].account_name
: undefined,
);
@@ -51,9 +51,9 @@ export function PublicHistoriesPage({}: Props): VNode {
const accountsBar = [];
// Ask story of all the public accounts.
- for (const account of data.publicAccounts) {
- logger.trace("Asking transactions for", account.accountLabel);
- const isSelected = account.accountLabel == showAccount;
+ for (const account of data.public_accounts) {
+ logger.trace("Asking transactions for", account.account_name);
+ const isSelected = account.account_name == showAccount;
accountsBar.push(
setShowAccount(account.accountLabel)}
+ onClick={() => setShowAccount(account.account_name)}
>
- {account.accountLabel}
+ {account.account_name}
,
);
- txs[account.accountLabel] = ;
+ txs[account.account_name] = ;
}
return (
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index c27984569..e07525ab4 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -17,17 +17,19 @@
import {
HttpStatusCode,
stringifyWithdrawUri,
+ TranslatedString,
WithdrawUriResult,
} from "@gnu-taler/taler-util";
import {
+ notify,
+ notifyError,
RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useEffect } from "preact/hooks";
import { QR } from "../components/QR.js";
import { useAccessAnonAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { buildRequestErrorMessage } from "../utils.js";
export function QrCodeSection({
@@ -49,47 +51,87 @@ export function QrCodeSection({
const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
const { abortWithdrawal } = useAccessAnonAPI();
+
+ async function doAbort() {
+ try {
+ await abortWithdrawal(withdrawUri.withdrawalOperationId);
+ onAborted();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
return (
-
- {i18n.str`Charge your GNU Taler wallet`}
-
-
-
- Continue with GNU Taler
-
-
{i18n.str`Or scan this QR code with your mobile to receive the coin in another device:`}
-
-
{
- e.preventDefault();
- try {
- await abortWithdrawal(withdrawUri.withdrawalOperationId);
- onAborted();
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Conflict
- ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
- }}
- >{i18n.str`Cancel`}
+
+
+
+
+ If you have a Taler wallet installed in this device
+
+
+
+ You will see the details of the operation in your wallet including the fees (if applies).
+ If you still don't have one you can install it from here .
+
+
+
-
-
+
+
+
+
+
+ Or if you have the wallet in another device
+
+
+ Scan the QR below to start the withdrawal
+
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
);
}
+
+
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index ded48564f..9ac93bb34 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -13,26 +13,31 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see
*/
-import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
+import { HttpStatusCode, Logger, TranslatedString } from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { useTestingAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { bankUiSettings } from "../settings.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
+import { getRandomPassword, getRandomUsername } from "./rnd.js";
+import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
const logger = new Logger("RegistrationPage");
export function RegistrationPage({
onComplete,
+ onCancel
}: {
onComplete: () => void;
+ onCancel: () => void;
}): VNode {
const { i18n } = useTranslationContext();
if (!bankUiSettings.allowRegistrations) {
@@ -40,168 +45,357 @@ export function RegistrationPage({
{i18n.str`Currently, the bank is not accepting new registrations!`}
);
}
- return
;
+ return
;
}
-export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
+export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/;
+export const PHONE_REGEX = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/;
+export const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
/**
* Collect and submit registration data.
*/
-function RegistrationForm({ onComplete }: { onComplete: () => void }): VNode {
+function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, onCancel: () => void }): VNode {
const backend = useBackendContext();
const [username, setUsername] = useState
();
+ const [name, setName] = useState();
const [password, setPassword] = useState();
+ const [phone, setPhone] = useState();
+ const [email, setEmail] = useState();
const [repeatPassword, setRepeatPassword] = useState();
+ const { requestNewLoginToken } = useCredentialsChecker()
const { register } = useTestingAPI();
const { i18n } = useTranslationContext();
const errors = undefinedIfEmpty({
+ // name: !name
+ // ? i18n.str`Missing name`
+ // : undefined,
username: !username
? i18n.str`Missing username`
: !USERNAME_REGEX.test(username)
- ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
- : undefined,
+ ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ : undefined,
+ phone: !phone
+ ? undefined
+ : !PHONE_REGEX.test(phone)
+ ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ : undefined,
+ email: !email
+ ? undefined
+ : !EMAIL_REGEX.test(email)
+ ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ : undefined,
password: !password ? i18n.str`Missing password` : undefined,
repeatPassword: !repeatPassword
? i18n.str`Missing password`
: repeatPassword !== password
- ? i18n.str`Passwords don't match`
- : undefined,
+ ? i18n.str`Passwords don't match`
+ : undefined,
});
+ async function doRegistrationStep() {
+ if (!username || !password) return;
+ try {
+ await register({ name: name ?? "", username, password });
+ const resp = await requestNewLoginToken(username, password)
+ setUsername(undefined);
+ if (resp.valid) {
+ backend.logIn({ username, token: resp.token });
+ }
+ onComplete();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`That username is already taken`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setPassword(undefined);
+ setRepeatPassword(undefined);
+ }
+
+ async function delay(ms: number): Promise {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(undefined);
+ }, ms)
+ })
+ }
+ async function doRandomRegistration(tries: number = 3) {
+ const user = getRandomUsername();
+ const pass = getRandomPassword();
+ try {
+ setUsername(undefined);
+ setPassword(undefined);
+ setRepeatPassword(undefined);
+ const username = `_${user.first}-${user.second}_`
+ await register({ username, name: `${user.first} ${user.second}`, password: pass });
+ const resp = await requestNewLoginToken(username, pass)
+ if (resp.valid) {
+ backend.logIn({ username, token: resp.token });
+ }
+ onComplete();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ if (tries > 0) {
+ await delay(200)
+ await doRandomRegistration(tries - 1)
+ } else {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`Could not create a random user`
+ : undefined,
+ }),
+ );
+ }
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
return (
- {i18n.str`Welcome to ${bankUiSettings.bankName}!`}
-
-
-
+
+
+
+
{i18n.str`Account registration`}
+
+
+
+
{
e.preventDefault();
}}
autoCapitalize="none"
autoCorrect="off"
>
-
+
+
+
+
+ Repeat password
+ *
+
+
+
+ {
+ setRepeatPassword(e.currentTarget.value);
+ }}
+ />
+
+
+
+
+
+
+ Name
+
+
+ {
+ setName(e.currentTarget.value);
+ }}
+ />
+ {/* */}
+
+
+
+ {/*
+
+ Phone
+
+
+ {
+ setPhone(e.currentTarget.value);
+ }}
+ />
+
+
+
+
+
+ Email
+
+
+ {
+ setEmail(e.currentTarget.value);
+ }}
+ />
+
+
+
*/}
+
+
+ {
- e.preventDefault();
- setUsername(undefined);
- setPassword(undefined);
- setRepeatPassword(undefined);
- onComplete();
+ e.preventDefault()
+ onCancel()
}}
>
- {i18n.str`Cancel`}
+ Cancel
+
+ {
+ e.preventDefault()
+ doRegistrationStep()
+ }}
+ >
+ Register
+
+
+ {bankUiSettings.allowRandomAccountCreation &&
+
+ {
+ e.preventDefault()
+ doRandomRegistration()
+ }}
+ >
+ Create a random user
+
+
+ }
-
+
+
);
}
diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx
deleted file mode 100644
index f176c73db..000000000
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see
- */
-
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { createHashHistory } from "history";
-import { VNode, h } from "preact";
-import { Route, Router, route } from "preact-router";
-import { useEffect, useMemo, useState } from "preact/hooks";
-import { BankFrame } from "./BankFrame.js";
-import { BusinessAccount } from "./BusinessAccount.js";
-import { HomePage, WithdrawalOperationPage } from "./HomePage.js";
-import { PublicHistoriesPage } from "./PublicHistoriesPage.js";
-import { RegistrationPage } from "./RegistrationPage.js";
-
-export function Routing(): VNode {
- const history = createHashHistory();
-
- return (
- {
- route("/business");
- }}
- >
-
- (
- {
- route("/account");
- }}
- onLoadNotOk={() => {
- route("/account");
- }}
- />
- )}
- />
- }
- />
- (
- {
- route("/account");
- }}
- />
- )}
- />
- (
- {
- route(`/operation/${wopid}`);
- }}
- onRegister={() => {
- route("/register");
- }}
- />
- )}
- />
- (
- {
- route("/account");
- }}
- onRegister={() => {
- route("/register");
- }}
- onLoadNotOk={() => {
- route("/account");
- }}
- />
- )}
- />
-
-
-
- );
-}
-
-function Redirect({ to }: { to: string }): VNode {
- useEffect(() => {
- route(to, true);
- }, []);
- return being redirected to {to}
;
-}
-
-export function assertUnreachable(x: never): never {
- throw new Error("Didn't expect to get here");
-}
diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
new file mode 100644
index 000000000..6acf0361e
--- /dev/null
+++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
@@ -0,0 +1,167 @@
+import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
+import { useState } from "preact/hooks";
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { buildRequestErrorMessage } from "../utils.js";
+import { AccountForm } from "./admin/AccountForm.js";
+
+export function ShowAccountDetails({
+ account,
+ onClear,
+ onUpdateSuccess,
+ onLoadNotOk,
+ onChangePassword,
+}: {
+ onLoadNotOk: (
+ error: HttpResponsePaginated,
+ ) => VNode;
+ onClear?: () => void;
+ onChangePassword: () => void;
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useBusinessAccountDetails(account);
+ const { updateAccount } = useAdminAccountAPI();
+ const [update, setUpdate] = useState(false);
+ const [submitAccount, setSubmitAccount] = useState<
+ SandboxBackend.Circuit.CircuitAccountData | undefined
+ >();
+
+ if (!result.ok) {
+ if (result.loading || result.type === ErrorType.TIMEOUT) {
+ return onLoadNotOk(result);
+ }
+ if (result.status === HttpStatusCode.NotFound) {
+ return account not found
;
+ }
+ return onLoadNotOk(result);
+ }
+
+ async function doUpdate() {
+ if (!update) {
+ setUpdate(true);
+ } else {
+ if (!submitAccount) return;
+ try {
+ await updateAccount(account, {
+ cashout_address: submitAccount.cashout_address,
+ contact_data: submitAccount.contact_data,
+ });
+ onUpdateSuccess();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The rights to change the account are not sufficient`
+ : status === HttpStatusCode.NotFound
+ ? i18n.str`The username was not found`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+ }
+
+ return (
+
+
+
+
+ {update ?
+ Update account
+ :
+ Account details
+ }
+
+
+
+
+
+ change the account details
+
+
+ {
+ setUpdate(!update)
+ }}>
+
+
+
+
+
+
+
setSubmitAccount(a)}
+ >
+
+
+
+
+
+
+ {onClear ? (
+ {
+ e.preventDefault();
+ onClear();
+ }}
+ />
+ ) : undefined}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
new file mode 100644
index 000000000..46f4fe0ef
--- /dev/null
+++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
@@ -0,0 +1,177 @@
+import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
+import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
+import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+
+export function UpdateAccountPassword({
+ account,
+ onCancel,
+ onUpdateSuccess,
+ onLoadNotOk,
+ focus,
+}: {
+ onLoadNotOk: (
+ error: HttpResponsePaginated,
+ ) => VNode;
+ onCancel: () => void;
+ focus?: boolean,
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useBusinessAccountDetails(account);
+ const { changePassword } = useAdminAccountAPI();
+ const [password, setPassword] = useState();
+ const [repeat, setRepeat] = useState();
+
+ if (!result.ok) {
+ if (result.loading || result.type === ErrorType.TIMEOUT) {
+ return onLoadNotOk(result);
+ }
+ if (result.status === HttpStatusCode.NotFound) {
+ return account not found
;
+ }
+ return onLoadNotOk(result);
+ }
+
+ const errors = undefinedIfEmpty({
+ password: !password ? i18n.str`required` : undefined,
+ repeat: !repeat
+ ? i18n.str`required`
+ : password !== repeat
+ ? i18n.str`password doesn't match`
+ : undefined,
+ });
+
+ async function doChangePassword() {
+ if (!!errors || !password) return;
+ try {
+ const r = await changePassword(account, {
+ new_password: password,
+ });
+ onUpdateSuccess();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(buildRequestErrorMessage(i18n, error.cause));
+ } else {
+ notifyError(i18n.str`Operation failed, please report`, (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString)
+ }
+ }
+ }
+
+ return (
+
+
+
+ Update password for account "{account}"
+
+
+
{
+ e.preventDefault()
+ }}
+ >
+
+
+
+
+
+ {i18n.str`New password`}
+
+
+ {
+ setPassword(e.currentTarget.value)
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+
+
+ {/*
+ user
+
*/}
+
+
+
+
+ {i18n.str`Type it again`}
+
+
+ {
+ setRepeat(e.currentTarget.value)
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+
+
+
+ repeat the same password
+
+
+
+
+
+
+
+
+ {onCancel ?
+
+ Cancel
+
+ :
+ }
+
{
+ e.preventDefault()
+ doChangePassword()
+ }}
+ >
+ Change
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 4c4a38e57..da299b1c8 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -19,40 +19,49 @@ import {
Amounts,
HttpStatusCode,
Logger,
+ TranslatedString,
+ WithdrawUriResult,
parseWithdrawUri,
} from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { Ref, VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
+import { forwardRef } from "preact/compat";
import { useEffect, useRef, useState } from "preact/hooks";
import { useAccessAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { forwardRef } from "preact/compat";
+import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js";
+import { useSettings } from "../hooks/settings.js";
+import { OperationState } from "./OperationState/index.js";
+import { Attention } from "../components/Attention.js";
const logger = new Logger("WalletWithdrawForm");
-const RefAmount = forwardRef(Amount);
+const RefAmount = forwardRef(InputAmount);
-export function WalletWithdrawForm({
- focus,
- limit,
- onSuccess,
-}: {
+
+function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
limit: AmountJson;
focus?: boolean;
- onSuccess: (operationId: string) => void;
+ goToConfirmOperation: (operationId: string) => void;
+ onCancel: () => void;
}): VNode {
const { i18n } = useTranslationContext();
- const { createWithdrawal } = useAccessAPI();
+ const [settings, updateSettings] = useSettings()
- const [amountStr, setAmountStr] = useState("5.00");
- const ref = useRef(null);
- useEffect(() => {
- if (focus) ref.current?.focus();
- }, [focus]);
+ const { createWithdrawal } = useAccessAPI();
+ const [amountStr, setAmountStr] = useState(`${settings.maxWithdrawalAmount}`);
+
+ if (!!settings.currentWithdrawalOperationId) {
+ return
+
+ To complete or cancel the operation click here
+
+
+ }
const trimmedAmountStr = amountStr?.trim();
@@ -65,142 +74,186 @@ export function WalletWithdrawForm({
trimmedAmountStr == null
? i18n.str`required`
: !parsedAmount
- ? i18n.str`invalid`
- : Amounts.cmp(limit, parsedAmount) === -1
- ? i18n.str`balance is not enough`
- : undefined,
+ ? i18n.str`invalid`
+ : Amounts.cmp(limit, parsedAmount) === -1
+ ? i18n.str`balance is not enough`
+ : undefined,
});
- return (
- {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
-
- {i18n.str`Amount to withdraw:`}
-
- {
- setAmountStr(v);
- }}
- error={errors?.amount}
- ref={ref}
- />
-
-
-
-
{
- e.preventDefault();
- if (!parsedAmount) return;
- try {
- const result = await createWithdrawal({
- amount: Amounts.stringify(parsedAmount),
- });
- const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
- if (!uri) {
- return notifyError({
- title: i18n.str`Server responded with an invalid withdraw URI`,
- description: i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`,
- });
- } else {
- onSuccess(uri.withdrawalOperationId);
- }
- } catch (error) {
- if (error instanceof RequestError) {
- notifyError(
- buildRequestErrorMessage(i18n, error.cause, {
- onClientError: (status) =>
- status === HttpStatusCode.Forbidden
- ? i18n.str`The operation was rejected due to insufficient funds`
- : undefined,
- }),
- );
- } else {
- notifyError({
- title: i18n.str`Operation failed, please report`,
- description:
- error instanceof Error
- ? error.message
- : JSON.stringify(error),
- });
- }
- }
+ async function doStart() {
+ if (!parsedAmount) return;
+ try {
+ const result = await createWithdrawal({
+ amount: Amounts.stringify(parsedAmount),
+ });
+ const uri = parseWithdrawUri(result.data.taler_withdraw_uri);
+ if (!uri) {
+ return notifyError(
+ i18n.str`Server responded with an invalid withdraw URI`,
+ i18n.str`Withdraw URI: ${result.data.taler_withdraw_uri}`);
+ } else {
+ updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId)
+ goToConfirmOperation(uri.withdrawalOperationId);
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Forbidden
+ ? i18n.str`The operation was rejected due to insufficient funds`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ }
+
+ return
{
+ e.preventDefault()
+ }}
+ >
+
+
+
+ {i18n.str`Amount`}
+ {
+ setAmountStr(v);
}}
+ error={errors?.amount}
+ ref={focus ? doAutoFocus : undefined}
/>
+
+
+
+
+ {
+ e.preventDefault();
+ setAmountStr("50.00")
+ }}
+ >
+ 50.00
+
+ {
+ e.preventDefault();
+ setAmountStr("25.00")
+ }}
+ >
+
+ 25.00
+
+
+
+ {
+ e.preventDefault();
+ setAmountStr("10.00")
+ }}
+ >
+ 10.00
+
+ {
+ e.preventDefault();
+ setAmountStr("5.00")
+ }}
+ >
+ 5.00
+
+
+
+
+
+
+ Cancel
+ {
+ e.preventDefault()
+ doStart()
+ }}
+ >
+ Continue
+
+
+
+
+}
+
+
+export function WalletWithdrawForm({
+ focus,
+ limit,
+ onCancel,
+ goToConfirmOperation,
+}: {
+ limit: AmountJson;
+ focus?: boolean;
+ goToConfirmOperation: (operationId: string) => void;
+ onCancel: () => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
+
+ return (
+
+
Prepare your wallet
+
+ After using your wallet you will need to confirm or cancel the operation on this site.
-
+
+
+
+ {settings.showInstallWallet &&
+
{
+ updateSettings("showInstallWallet", false);
+ }}>
+
+ If you don't have one yet you can follow the instruction here
+
+
+ }
+
+ {!settings.fastWithdrawal ?
+
+ :
+
+ }
+
+
);
}
-export function Amount(
- {
- currency,
- value,
- error,
- onChange,
- }: {
- error?: string;
- currency: string;
- value: string | undefined;
- onChange?: (s: string) => void;
- },
- ref: Ref
,
-): VNode {
- return (
-
- );
-}
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index cdb612155..ddcd2492d 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -15,26 +15,41 @@
*/
import {
+ AmountJson,
+ Amounts,
HttpStatusCode,
Logger,
- WithdrawUriResult,
+ PaytoUri,
+ PaytoUriIBAN,
+ PaytoUriTalerBank,
+ TranslatedString,
+ WithdrawUriResult
} from "@gnu-taler/taler-util";
import {
RequestError,
+ notify,
+ notifyError,
+ notifyInfo,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useMemo, useState } from "preact/hooks";
+import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAccessAnonAPI } from "../hooks/access.js";
-import { notifyError } from "../hooks/notification.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+import { useSettings } from "../hooks/settings.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
const logger = new Logger("WithdrawalConfirmationQuestion");
interface Props {
onAborted: () => void;
withdrawUri: WithdrawUriResult;
+ details: {
+ account: PaytoUri,
+ reserve: string,
+ amount: AmountJson,
+ }
}
/**
* Additional authentication required to complete the operation.
@@ -42,9 +57,11 @@ interface Props {
*/
export function WithdrawalConfirmationQuestion({
onAborted,
+ details,
withdrawUri,
}: Props): VNode {
const { i18n } = useTranslationContext();
+ const [settings, updateSettings] = useSettings()
const captchaNumbers = useMemo(() => {
return {
@@ -56,139 +73,263 @@ export function WithdrawalConfirmationQuestion({
const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI();
const [captchaAnswer, setCaptchaAnswer] = useState();
const answer = parseInt(captchaAnswer ?? "", 10);
+ const [busy, setBusy] = useState>()
const errors = undefinedIfEmpty({
answer: !captchaAnswer
? i18n.str`Answer the question before continue`
: Number.isNaN(answer)
- ? i18n.str`The answer should be a number`
- : answer !== captchaNumbers.a + captchaNumbers.b
- ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
- : undefined,
- });
+ ? i18n.str`The answer should be a number`
+ : answer !== captchaNumbers.a + captchaNumbers.b
+ ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
+ : undefined,
+ }) ?? busy;
+
+ async function doTransfer() {
+ try {
+ setBusy({})
+ await confirmWithdrawal(
+ withdrawUri.withdrawalOperationId,
+ );
+ if (!settings.showWithdrawalSuccess) {
+ notifyInfo(i18n.str`Wire transfer completed!`)
+ }
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
+ : status === HttpStatusCode.UnprocessableEntity
+ ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+
+ async function doCancel() {
+ try {
+ setBusy({})
+ await abortWithdrawal(withdrawUri.withdrawalOperationId);
+ onAborted();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ notify(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
+ : undefined,
+ }),
+ );
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+ }
+ setBusy(undefined)
+ }
+
return (
- {i18n.str`Confirm Withdrawal`}
-
-
-
{
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
-