aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/demobank-ui/src/components')
-rw-r--r--packages/demobank-ui/src/components/Attention.tsx59
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx5
-rw-r--r--packages/demobank-ui/src/components/CopyButton.tsx60
-rw-r--r--packages/demobank-ui/src/components/ErrorLoading.tsx29
-rw-r--r--packages/demobank-ui/src/components/LangSelector.tsx78
-rw-r--r--packages/demobank-ui/src/components/QR.tsx5
-rw-r--r--packages/demobank-ui/src/components/Routing.tsx167
-rw-r--r--packages/demobank-ui/src/components/ShowInputErrorLabel.tsx29
-rw-r--r--packages/demobank-ui/src/components/Transactions/index.ts2
-rw-r--r--packages/demobank-ui/src/components/Transactions/state.ts52
-rw-r--r--packages/demobank-ui/src/components/Transactions/views.tsx139
-rw-r--r--packages/demobank-ui/src/components/app.tsx59
12 files changed, 546 insertions, 138 deletions
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 <div class={`group attention-${type} mt-2`}>
+ <div class="rounded-md group-[.attention-info]:bg-blue-50 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow">
+ <div class="flex">
+ <div >
+ <svg xmlns="http://www.w3.org/2000/svg" stroke="none" viewBox="0 0 24 24" fill="currentColor" class="w-8 h-8 group-[.attention-info]:text-blue-400 group-[.attention-warning]:text-yellow-400 group-[.attention-danger]:text-red-400 group-[.attention-success]:text-green-400">
+ {(() => {
+ switch (type) {
+ case "info":
+ return <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" />
+ case "warning":
+ return <path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ case "danger":
+ return <path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" />
+ case "success":
+ return <path fill-rule="evenodd" d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.832 0-1.612.453-1.918 1.227z" />
+ default:
+ assertUnreachable(type)
+ }
+ })()}
+ </svg>
+ </div>
+ <div class="ml-3 w-full">
+ <h3 class="text-sm group-hover:text-white font-bold group-[.attention-info]:text-blue-800 group-[.attention-success]:text-green-800 group-[.attention-warning]:text-yellow-800 group-[.attention-danger]:text-red-800">
+ {title}
+ </h3>
+ <div class="mt-2 text-sm group-[.attention-info]:text-blue-700 group-[.attention-warning]:text-yellow-700 group-[.attention-danger]:text-red-700 group-[.attention-success]:text-green-700">
+ {children}
+ </div>
+ </div>
+ {onClose &&
+ <div>
+ <button type="button" class="font-semibold items-center rounded bg-transparent px-2 py-1 text-xs text-gray-900 hover:bg-gray-50"
+ onClick={(e) => {
+ e.preventDefault();
+ onClose();
+ }}
+ >
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
+ </svg>
+ </button>
+ </div>
+ }
+ </div>
+ </div>
+
+ </div>
+}
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")
: "-"}
</td>
- <td>{Amounts.stringifyValue(item.amount_debit)}</td>
- <td>{Amounts.stringifyValue(item.amount_credit)}</td>
+ <td><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} /></td>
+ <td><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} /></td>
<td>{item.status}</td>
<td>
<a
diff --git a/packages/demobank-ui/src/components/CopyButton.tsx b/packages/demobank-ui/src/components/CopyButton.tsx
new file mode 100644
index 000000000..b36de770e
--- /dev/null
+++ b/packages/demobank-ui/src/components/CopyButton.tsx
@@ -0,0 +1,60 @@
+import { h, VNode } from "preact";
+import { useEffect, useState } from "preact/hooks";
+
+
+
+export function CopyIcon(): VNode {
+ return (
+ <svg height="16" viewBox="0 0 16 16" width="16" stroke="currentColor" strokeWidth="1.5">
+ <path
+ fill-rule="evenodd"
+ d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
+ />
+ <path
+ fill-rule="evenodd"
+ d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
+ />
+ </svg>
+ )
+};
+
+export function CopiedIcon(): VNode {
+ return (
+ <svg height="16" viewBox="0 0 16 16" width="16" stroke="currentColor" strokeWidth="1.5">
+ <path
+ fill-rule="evenodd"
+ d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
+ />
+ </svg>
+ )
+};
+
+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 (
+ <button class="text-white" onClick={copyText} style={{ width: 16, height: 16, fontSize: "initial" }}>
+ <CopyIcon />
+ </button>
+ );
+ }
+ return (
+ <div class="text-white" content="Copied" style={{ display: "inline-block" }}>
+ <button disabled style={{ width: 16, height: 16, fontSize: "initial" }}>
+ <CopiedIcon />
+ </button>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/packages/demobank-ui/src/components/ErrorLoading.tsx b/packages/demobank-ui/src/components/ErrorLoading.tsx
new file mode 100644
index 000000000..ee62671ce
--- /dev/null
+++ b/packages/demobank-ui/src/components/ErrorLoading.tsx
@@ -0,0 +1,29 @@
+/*
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { 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";
+
+export function ErrorLoading({ error }: { error: HttpError<SandboxBackend.SandboxError> }): VNode {
+ const { i18n } = useTranslationContext()
+ return (<Attention type="danger" title={error.message as TranslatedString}>
+ <p class="text-sm font-medium text-red-800">Got status "{error.info.status}" on {error.info.url}</p>
+ </Attention>
+ );
+}
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 (
- <Fragment>
- <a
- href="#"
- class="pure-button"
- name="language"
- onClick={(ev) => {
- ev.preventDefault();
- setHidden((h) => !h);
- ev.stopPropagation();
- }}
- >
- {getLangName(lang)}
- </a>
- <div
- id="lang"
- class={hidden ? "hide" : ""}
- style={{
- display: "inline-block",
- }}
- >
- <div style="position: relative; overflow: visible;">
- <div
- class="nav"
- style="position: absolute; max-height: 60vh; overflow-y: auto; margin-left: -120px; margin-top: 20px"
- >
+ <div>
+ <div class="relative mt-2">
+ <button type="button" class="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
+ onClick={() => {
+ setHidden((h) => !h);
+ }}>
+ <span class="flex items-center">
+ <img src="https://taler.net/images/languageicon.svg" alt="" class="h-5 w-5 flex-shrink-0 rounded-full" />
+ <span class="ml-3 block truncate">{getLangName(lang)}</span>
+ </span>
+ <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
+ <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd" />
+ </svg>
+ </span>
+ </button>
+
+ {!hidden &&
+ <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" tabIndex={-1} role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3">
{Object.keys(messages)
.filter((l) => l !== lang)
- .map((l) => (
- <a
- key={l}
- href="#"
- class="navbtn langbtn"
- value={l}
+ .map((lang) => (
+ <li class="text-gray-900 hover:bg-indigo-600 hover:text-white cursor-pointer relative select-none py-2 pl-3 pr-9" role="option"
onClick={() => {
- changeLanguage(l);
+ changeLanguage(lang);
setUpdatingLang(false);
+ setHidden(true)
}}
>
- {getLangName(l)}
- </a>
+ <span class="font-normal block truncate">{getLangName(lang)}</span>
+
+ <span class="text-indigo-600 absolute inset-y-0 right-0 flex items-center pr-4">
+ {/* <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+ <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd" />
+ </svg> */}
+ </span>
+ </li>
))}
- <br />
- </div>
- </div>
+
+ </ul>
+ }
+
</div>
- </Fragment>
+ </div>
);
}
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 (
<div
style={{
- width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "left",
@@ -41,9 +40,7 @@ export function QR({ text }: { text: string }): VNode {
>
<div
style={{
- width: "50%",
- minWidth: 200,
- maxWidth: 300,
+ width: "100%",
marginRight: "auto",
marginLeft: "auto",
}}
diff --git a/packages/demobank-ui/src/components/Routing.tsx b/packages/demobank-ui/src/components/Routing.tsx
new file mode 100644
index 000000000..aafc95687
--- /dev/null
+++ b/packages/demobank-ui/src/components/Routing.tsx
@@ -0,0 +1,167 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { 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 <BankFrame >
+ <Router history={history}>
+ <Route
+ path="/login"
+ component={() => (
+ <Fragment>
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h2>
+ </div>
+
+ <LoginForm
+ onRegister={() => {
+ route("/register");
+ }}
+ />
+ </Fragment>
+ )}
+ />
+ <Route
+ path="/public-accounts"
+ component={() => <PublicHistoriesPage />}
+ />
+ <Route
+ path="/operation/:wopid"
+ component={({ wopid }: { wopid: string }) => (
+ <WithdrawalOperationPage
+ operationId={wopid}
+ onContinue={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ {bankUiSettings.allowRegistrations &&
+ <Route
+ path="/register"
+ component={() => (
+ <RegistrationPage
+ onComplete={() => {
+ route("/account");
+ }}
+ onCancel={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ }
+ <Route default component={Redirect} to="/login" />
+ </Router>
+ </BankFrame>
+ }
+ const { isUserAdministrator, username } = backend.state
+
+ return (
+ <BankFrame account={backend.state.username}>
+ <Router history={history}>
+ <Route
+ path="/operation/:wopid"
+ component={({ wopid }: { wopid: string }) => (
+ <WithdrawalOperationPage
+ operationId={wopid}
+ onContinue={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ <Route
+ path="/public-accounts"
+ component={() => <PublicHistoriesPage />}
+ />
+ <Route
+ path="/account"
+ component={() => {
+ if (isUserAdministrator) {
+ return <AdminHome
+ onRegister={() => {
+ route("/register");
+ }}
+ />;
+ } else {
+ return <HomePage
+ account={username}
+ goToConfirmOperation={(wopid) => {
+ route(`/operation/${wopid}`);
+ }}
+ goToBusinessAccount={() => {
+ route("/business");
+ }}
+ onRegister={() => {
+ route("/register");
+ }}
+ />
+ }
+ }}
+ />
+ <Route
+ path="/business"
+ component={() => (
+ <BusinessAccount
+ account={username}
+ onClose={() => {
+ route("/account");
+ }}
+ onRegister={() => {
+ route("/register");
+ }}
+ onLoadNotOk={() => {
+ route("/account");
+ }}
+ />
+ )}
+ />
+ <Route default component={Redirect} to="/account" />
+ </Router>
+ </BankFrame>
+ );
+}
+
+function Redirect({ to }: { to: string }): VNode {
+ useEffect(() => {
+ route(to, true);
+ }, []);
+ return <div>being redirected to {to}</div>;
+}
+
+export function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
diff --git a/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
new file mode 100644
index 000000000..c5840cad9
--- /dev/null
+++ b/packages/demobank-ui/src/components/ShowInputErrorLabel.tsx
@@ -0,0 +1,29 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Fragment, h, VNode } from "preact";
+
+export function ShowInputErrorLabel({
+ isDirty,
+ message,
+}: {
+ message: string | undefined;
+ isDirty: boolean;
+}): VNode {
+ if (message && isDirty)
+ return <div class="text-base" style={{ color: "red" }}>{message}</div>;
+ return <div class="text-base" style={{ }}> </div>;
+}
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 <http://www.gnu.org/licenses/>
*/
-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 <http://www.gnu.org/licenses/>
*/
-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 <div />
+ 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<string, typeof transactions>)
return (
- <div class="results">
- <table class="pure-table pure-table-striped">
- <thead>
- <tr>
- <th>{i18n.str`Date`}</th>
- <th>{i18n.str`Amount`}</th>
- <th>{i18n.str`Counterpart`}</th>
- <th>{i18n.str`Subject`}</th>
- </tr>
- </thead>
- <tbody>
- {transactions.map((item, idx) => {
- return (
- <tr key={idx}>
- <td>
- {item.when.t_ms === "never"
- ? ""
- : format(item.when.t_ms, "dd/MM/yyyy HH:mm:ss")}
- </td>
- <td>
- {item.negative ? "-" : ""}
- {item.amount ? (
- `${Amounts.stringifyValue(item.amount)} ${
- item.amount.currency
- }`
- ) : (
- <span style={{ color: "grey" }}>&lt;invalid value&gt;</span>
- )}
- </td>
- <td>{item.counterpart}</td>
- <td>{item.subject}</td>
- </tr>
- );
- })}
- </tbody>
- </table>
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900"><i18n.Translate>Latest transactions</i18n.Translate></h1>
+ </div>
+ </div>
+ <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Date`}</th>
+ <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Amount`}</th>
+ <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Counterpart`}</th>
+ <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Subject`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Object.entries(txByDate).map(([date, txs], idx) => {
+ return <Fragment>
+ <tr class="border-t border-gray-200">
+ <th colSpan={4} scope="colgroup" class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3">
+ {date}
+ </th>
+ </tr>
+ {txs.map(item => {
+ const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss")
+ const amount = <Fragment>
+ { }
+ </Fragment>
+ return (<tr key={idx}>
+ <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <div class="font-medium text-gray-900">{time}</div>
+ <dl class="font-normal sm:hidden">
+ <dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt>
+ <dd class="mt-1 truncate text-gray-700">
+ {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? (
+ <RenderAmount value={item.amount} />
+ ) : (
+ <span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
+ )}</dd>
+
+ <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt>
+ <dd class="mt-1 truncate text-gray-500 sm:hidden">
+ {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart}
+ </dd>
+ </dl>
+ </td>
+ <td data-negative={item.negative ? "true" : "false"}
+ class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
+ {item.amount ? (<RenderAmount value={item.amount} negative={item.negative} />
+ ) : (
+ <span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
+ )}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td>
+ <td class="px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td>
+ </tr>)
+ })}
+ </Fragment>
+
+ })}
+ </tbody>
+
+ </table>
+
+ <nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination">
+ <div class="flex flex-1 justify-between sm:justify-end">
+ <button
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ disabled={!onPrev}
+ onClick={onPrev}
+ >
+ <i18n.Translate>First page</i18n.Translate>
+ </button>
+ <button
+ class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
+ disabled={!onNext}
+ onClick={onNext}
+ >
+ <i18n.Translate>Next</i18n.Translate>
+ </button>
+ </div>
+ </nav>
+ </div>
</div>
);
}
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 (
<TranslationProvider source={strings}>
<BackendStateProvider>
- <SWRConfig
- value={{
- provider: WITH_LOCAL_STORAGE_CACHE
- ? localStorageProvider
- : undefined,
- }}
- >
- <Routing />
- </SWRConfig>
+ <VersionCheck>
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ }}
+ >
+ <Routing />
+ </SWRConfig>
+ </VersionCheck>
</BackendStateProvider>
- </TranslationProvider>
+ </TranslationProvider >
);
};
(window as any).setGlobalLogLevelFromString = setGlobalLogLevelFromString;
(window as any).getGlobalLevel = getGlobalLogLevel;
+function VersionCheck({ children }: { children: ComponentChildren }): VNode {
+ const checked = useConfigState()
+
+ if (checked === undefined) {
+ return <Loading />
+ }
+ if (checked.type === "wrong") {
+ return <BankFrame>
+ the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}"
+ </BankFrame>
+ }
+ if (checked.type === "ok") {
+ return <ConfigStateProvider value={checked.result}>{children}</ConfigStateProvider>
+ }
+
+ return <BankFrame>
+ <ErrorLoading error={checked.result} />
+ </BankFrame>
+}
+
function localStorageProvider(): Map<unknown, unknown> {
const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));