taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/libeufin-bank-webui/src/components/Transactions/index.ts | 18++++++++++++++++--
Mpackages/libeufin-bank-webui/src/components/Transactions/state.ts | 17++++++++++++-----
Mpackages/libeufin-bank-webui/src/components/Transactions/views.tsx | 17++++++++++++++---
Mpackages/libeufin-bank-webui/src/hooks/account.ts | 57+++++++++++++++++++++++++++++++++++----------------------
Mpackages/libeufin-bank-webui/src/pages/PublicHistoriesPage.tsx | 71+++++++++++++++++++++++++++++++++++++++++++++++------------------------
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>