commit 75aff9ad57999bb8f89725cb07fd863235becae6
parent 605906ec4b4c0b84aff6ceb63cb02733d7c30935
Author: Florian Dold <dold@taler.net>
Date: Mon, 24 Aug 2026 02:29:59 +0200
bank web UI: load complete anonymous transaction histories
Diffstat:
5 files changed, 124 insertions(+), 56 deletions(-)
diff --git a/packages/libeufin-bank-webui/src/components/Transactions/index.ts b/packages/libeufin-bank-webui/src/components/Transactions/index.ts
@@ -29,7 +29,7 @@ import {
import { VNode } from "preact";
import { useComponentState } from "./state.js";
-import { ReadyView } from "./views.js";
+import { FailedView, ReadyView } from "./views.js";
export interface Props {
account: string;
@@ -40,9 +40,14 @@ export interface Props {
amount?: string;
}>
| undefined;
+ anonymous?: boolean;
}
-export type State = State.Loading | State.LoadingUriError | State.Ready;
+export type State =
+ | State.Loading
+ | State.LoadingUriError
+ | State.Failed
+ | State.Ready;
export namespace State {
export interface Loading {
@@ -56,6 +61,13 @@ export namespace State {
title: TranslatedString;
}
+ export interface Failed {
+ status: "failed";
+ error: unknown;
+ title: TranslatedString;
+ onRetry(): void;
+ }
+
export interface BaseInfo {
error: undefined;
}
@@ -76,6 +88,7 @@ export namespace State {
}
export interface Transaction {
+ id: number;
negative: boolean;
counterpart: string;
when: AbsoluteTime;
@@ -86,6 +99,7 @@ export interface Transaction {
const viewMapping: utils.StateViewMap<State> = {
loading: Loading,
"loading-error": ErrorLoading,
+ failed: FailedView,
ready: ReadyView,
};
diff --git a/packages/libeufin-bank-webui/src/components/Transactions/state.ts b/packages/libeufin-bank-webui/src/components/Transactions/state.ts
@@ -21,15 +21,19 @@ import {
Result,
TalerError,
} from "@gnu-taler/taler-util";
-import { useTransactions } from "../../hooks/account.js";
+import {
+ revalidateTransactions,
+ useTransactions,
+} from "../../hooks/account.js";
import { Props, State, Transaction } from "./index.js";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
export function useComponentState({
account,
routeCreateWireTransfer,
+ anonymous,
}: Props): State {
- const result = useTransactions(account);
+ const result = useTransactions(account, undefined, anonymous);
const { i18n } = useTranslationContext();
if (!result) {
return {
@@ -46,8 +50,10 @@ export function useComponentState({
}
if (result.type === "fail") {
return {
- status: "loading",
- error: undefined,
+ status: "failed",
+ error: result,
+ title: i18n.str`Failed to load transactions.`,
+ onRetry: () => void revalidateTransactions(),
};
}
@@ -63,8 +69,9 @@ export function useComponentState({
const amount = Amounts.parse(tx.amount);
const subject = tx.subject;
return {
+ id: tx.row_id,
negative,
- counterpart,
+ counterpart: counterpart ?? i18n.str`Unknown account`,
when,
amount,
subject,
diff --git a/packages/libeufin-bank-webui/src/components/Transactions/views.tsx b/packages/libeufin-bank-webui/src/components/Transactions/views.tsx
@@ -25,6 +25,17 @@ import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { State } from "./index.js";
+export function FailedView({ title, onRetry }: State.Failed): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Attention type="danger" title={title}>
+ <button type="button" class="font-semibold underline" onClick={onRetry}>
+ <i18n.Translate>Try again</i18n.Translate>
+ </button>
+ </Attention>
+ );
+}
+
const TALER_SCREEN_ID = 4;
export function ReadyView({
@@ -102,9 +113,9 @@ export function ReadyView({
</tr>
</thead>
<tbody>
- {Object.entries(txByDate).map(([date, txs], idx) => {
+ {Object.entries(txByDate).map(([date, txs]) => {
return (
- <Fragment key={idx}>
+ <Fragment key={date}>
<tr class="border-t border-gray-200">
<th
colSpan={4}
@@ -117,7 +128,7 @@ export function ReadyView({
{txs.map((item) => {
return (
<tr
- key={idx}
+ key={item.id}
class="border-b border-gray-200 last:border-none"
>
<td class="relative py-2 pl-2 pr-2 text-sm ">
diff --git a/packages/libeufin-bank-webui/src/hooks/account.ts b/packages/libeufin-bank-webui/src/hooks/account.ts
@@ -23,7 +23,7 @@ import {
TalerError,
TalerHttpError,
} from "@gnu-taler/taler-util";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
import { useSessionState } from "./session.js";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
@@ -169,6 +169,8 @@ export function usePublicAccounts(
) {
const [offset, setOffset] = useState<number | undefined>(initial);
+ useEffect(() => setOffset(initial), [filterAccount, initial]);
+
const {
lib: { bank: api },
} = useBankCoreApiContext();
@@ -261,6 +263,7 @@ export function revalidateTransactions() {
export function useTransactions(
account: string,
initial?: number,
+ anonymous = false,
):
| TalerError<{
requestUrl: string;
@@ -272,41 +275,51 @@ export function useTransactions(
| undefined {
const { state: credentials } = useSessionState();
const token =
- credentials.status !== "loggedIn" ? undefined : credentials.token;
+ anonymous || credentials.status !== "loggedIn"
+ ? undefined
+ : credentials.token;
const [offset, setOffset] = useState<number | undefined>(initial);
+
+ useEffect(() => setOffset(initial), [account, initial]);
const {
lib: { bank: api },
} = useBankCoreApiContext();
async function fetcher([username, token, txid]: [
string,
- AccessToken,
+ AccessToken | undefined,
number | undefined,
]) {
- return await api.getTransactions(
- { username, token },
- {
- limit: PAGINATED_LIST_REQUEST,
- offset: txid ? String(txid) : undefined,
- order: "dec",
- },
- );
+ const params = {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: txid ? String(txid) : undefined,
+ order: "dec" as const,
+ };
+ return token === undefined
+ ? api.getPublicTransactions(username, params)
+ : api.getTransactions({ username, token }, params);
}
const { data, error } = useSWR<
- TalerCoreBankResultByMethod<"getTransactions">,
+ Awaited<ReturnType<typeof api.getTransactions>>,
TalerHttpError
- >([account, token, offset, "getTransactions"], fetcher, {
- refreshInterval: 10000,
- refreshWhenHidden: false,
- refreshWhenOffline: false,
- // revalidateOnMount: false,
- revalidateIfStale: false,
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- shouldRetryOnError: true,
- });
+ >(
+ anonymous || token !== undefined
+ ? [account, token, offset, "getTransactions"]
+ : null,
+ fetcher,
+ {
+ refreshInterval: 10000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+ // revalidateOnMount: false,
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ shouldRetryOnError: true,
+ },
+ );
if (error) return error;
if (data === undefined) return undefined;
if (data.type !== "ok") return data;
diff --git a/packages/libeufin-bank-webui/src/pages/PublicHistoriesPage.tsx b/packages/libeufin-bank-webui/src/pages/PublicHistoriesPage.tsx
@@ -15,11 +15,18 @@
*/
import { TalerError } from "@gnu-taler/taler-util";
-import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ Attention,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Transactions } from "../components/Transactions/index.js";
-import { usePublicAccounts } from "../hooks/account.js";
+import {
+ revalidatePublicAccounts,
+ usePublicAccounts,
+} from "../hooks/account.js";
const TALER_SCREEN_ID = 108;
@@ -31,30 +38,43 @@ export function PublicHistoriesPage(): VNode {
// TODO: implemented filter by account name
const result = usePublicAccounts(undefined);
- const firstAccount =
- result &&
- !(result instanceof TalerError) &&
- result.type === "ok" &&
- result.body.length > 0
- ? result.body[0].username
- : undefined;
+ const [showAccount, setShowAccount] = useState<string>();
- const [showAccount, setShowAccount] = useState(firstAccount);
-
- if (!result || result instanceof TalerError|| result.type === "fail") {
+ if (!result) {
return <Loading />;
}
+ if (result instanceof TalerError || result.type === "fail") {
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`Failed to load public accounts.`}
+ >
+ <button
+ type="button"
+ class="font-semibold underline"
+ onClick={() => void revalidatePublicAccounts()}
+ >
+ <i18n.Translate>Try again</i18n.Translate>
+ </button>
+ </Attention>
+ );
+ }
const { body: accountList } = result;
+ const selectedAccount = accountList.some(
+ ({ username }) => username === showAccount,
+ )
+ ? showAccount
+ : accountList[0]?.username;
- const txs: Record<string, h.JSX.Element> = {};
const accountsBar = [];
// Ask story of all the public accounts.
for (const account of accountList) {
- const isSelected = account.username == showAccount;
+ const isSelected = account.username === selectedAccount;
accountsBar.push(
<li
+ key={account.username}
class={
isSelected
? "pure-menu-selected pure-menu-item"
@@ -65,18 +85,15 @@ export function PublicHistoriesPage(): VNode {
href="#"
name={`show account ${account.username}`}
class="pure-menu-link"
- onClick={() => setShowAccount(account.username)}
+ onClick={(event) => {
+ event.preventDefault();
+ setShowAccount(account.username);
+ }}
>
{account.username}
</a>
</li>,
);
- txs[account.username] = (
- <Transactions
- account={account.username}
- routeCreateWireTransfer={undefined}
- />
- );
}
return (
@@ -86,10 +103,16 @@ export function PublicHistoriesPage(): VNode {
<article>
<div class="pure-menu pure-menu-horizontal" name="accountMenu">
<ul class="pure-menu-list">{accountsBar}</ul>
- {typeof showAccount !== "undefined" ? (
- txs[showAccount]
+ {selectedAccount !== undefined ? (
+ <Transactions
+ account={selectedAccount}
+ routeCreateWireTransfer={undefined}
+ anonymous
+ />
) : (
- <p>No public transactions found.</p>
+ <p>
+ <i18n.Translate>No public transactions found.</i18n.Translate>
+ </p>
)}
<br />
</div>