taler-typescript-core

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

commit effe8b91907c38ad8841c79f8491e43599101952
parent 5c66655f9b95bf9628f18630d135439c88a82b88
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 12 Jun 2025 12:24:18 -0300

fix #10032

Diffstat:
Mpackages/aml-backoffice-ui/src/Routing.tsx | 22++++++++++++++++------
Mpackages/aml-backoffice-ui/src/components/ErrorLoadingWithDebug.tsx | 9+++++++--
Mpackages/aml-backoffice-ui/src/hooks/transfers.ts | 3+++
Mpackages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 60++++++++++--------------------------------------------------
Mpackages/aml-backoffice-ui/src/pages/Transfers.tsx | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mpackages/taler-util/src/http-client/exchange-client.ts | 6+++---
6 files changed, 107 insertions(+), 78 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -16,11 +16,10 @@ import { decodeCrockFromURI, - encodeCrockForURI, urlPattern, useCurrentLocation, useNavigationContext, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -34,16 +33,14 @@ import { CaseUpdate } from "./pages/CaseUpdate.js"; import { Accounts } from "./pages/Cases.js"; import { Dashboard } from "./pages/Dashboard.js"; import { HandleAccountNotReady } from "./pages/HandleAccountNotReady.js"; -import { MeasureList } from "./pages/MeasureList.js"; -import { NewMeasure } from "./pages/NewMeasure.js"; import { Officer } from "./pages/Officer.js"; import { Search } from "./pages/Search.js"; +import { ShowCollectedInfo } from "./pages/ShowCollectedInfo.js"; import { Transfers } from "./pages/Transfers.js"; import { AmlDecisionRequestWizard, WizardSteps, } from "./pages/decision/AmlDecisionRequestWizard.js"; -import { ShowCollectedInfo } from "./pages/ShowCollectedInfo.js"; export function Routing(): VNode { const session = useOfficer(); @@ -136,6 +133,10 @@ export const privatePages = { // measuresNew: urlPattern(/\/measures\/new/, () => "#/measures/new"), // measures: urlPattern(/\/measures/, () => "#/measures"), search: urlPattern(/\/search/, () => "#/search"), + transfersForAccount: urlPattern<{ cid: string }>( + /\/transfers\/(?<cid>[a-zA-Z0-9]+)/, + ({ cid }) => `#/transfers/${cid}`, + ), transfers: urlPattern(/\/transfers/, () => "#/transfers"), accounts: urlPattern(/\/accounts/, () => "#/accounts"), caseUpdate: urlPattern<{ cid: string; type: string }>( @@ -151,7 +152,7 @@ export const privatePages = { function PrivateRouting(): VNode { const { navigateTo } = useNavigationContext(); const location = useCurrentLocation(privatePages); - const [,, startNewRequest] = useCurrentDecisionRequest(); + const [, , startNewRequest] = useCurrentDecisionRequest(); useEffect(() => { if (location.name === undefined) { navigateTo(privatePages.dashboard.url({})); @@ -298,6 +299,7 @@ function PrivateRouting(): VNode { return ( <CaseDetails account={location.values.cid} + routeToShowTransfers={privatePages.transfersForAccount} routeToShowCollectedInfo={privatePages.showCollectedInfo} onNewDecision={(r) => { startNewRequest(r); @@ -337,6 +339,14 @@ function PrivateRouting(): VNode { case "transfers": { return <Transfers routeToCaseById={privatePages.caseDetails} />; } + case "transfersForAccount": { + return ( + <Transfers + routeToCaseById={privatePages.caseDetails} + account={location.values.cid} + /> + ); + } case "showCollectedInfo": { return ( <ShowCollectedInfo diff --git a/packages/aml-backoffice-ui/src/components/ErrorLoadingWithDebug.tsx b/packages/aml-backoffice-ui/src/components/ErrorLoadingWithDebug.tsx @@ -18,7 +18,12 @@ import { ErrorLoading } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { usePreferences } from "../hooks/preferences.js"; -export function ErrorLoadingWithDebug({ error }: { error: TalerError }): VNode { +export function ErrorLoadingWithDebug({ error }: { error: Error }): VNode { const [pref] = usePreferences(); - return <ErrorLoading error={error} showDetail={pref.showDebugInfo} />; + if (error instanceof TalerError) { + return <ErrorLoading error={error} showDetail={pref.showDebugInfo} />; + } else { + console.error(error) + return <ErrorLoading error={TalerError.fromException(error)} showDetail={pref.showDebugInfo} />; + } } diff --git a/packages/aml-backoffice-ui/src/hooks/transfers.ts b/packages/aml-backoffice-ui/src/hooks/transfers.ts @@ -117,6 +117,7 @@ export function useTransferList({ offset, limit: PAGINATED_LIST_REQUEST, threshold, + account }); } case "debit": { @@ -125,6 +126,7 @@ export function useTransferList({ offset, limit: PAGINATED_LIST_REQUEST, threshold, + account }); } case "kyc-auth": { @@ -133,6 +135,7 @@ export function useTransferList({ offset, limit: PAGINATED_LIST_REQUEST, threshold, + account }); } } diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -62,56 +62,6 @@ import { CurrentMeasureTable, MeasureInfo, Mesaures } from "./MeasuresTable.js"; import { Officer } from "./Officer.js"; import { RulesInfo } from "./RulesInfo.js"; -// export type AmlEvent = -// | AmlFormEvent -// | KycCollectionEvent -// | KycExpirationEvent; - -// type AmlFormEvent = { -// type: "aml-form"; -// when: AbsoluteTime; -// title: TranslatedString; -// justification: Justification; -// metadata: FormMetadata; -// state: TalerExchangeApi.AmlState; -// threshold: AmountJson; -// }; -// type KycCollectionEvent = { -// type: "kyc-collection"; -// when: AbsoluteTime; -// title: TranslatedString; -// values: object; -// provider?: string; -// }; -// type KycExpirationEvent = { -// type: "kyc-expiration"; -// when: AbsoluteTime; -// title: TranslatedString; -// fields: string[]; -// }; - -// type WithTime = { when: AbsoluteTime }; - -// function selectSooner(a: WithTime, b: WithTime) { -// return AbsoluteTime.cmp(a.when, b.when); -// } - -// export function getEventsFromAmlHistory( -// events: TalerExchangeApi.KycAttributeCollectionEvent[], -// i18n: InternationalizationAPI, -// ): AmlEvent[] { -// const ke = events.map((event) => { -// return { -// type: "kyc-collection", -// title: i18n.str`User filled a form`, -// when: AbsoluteTime.fromProtocolTimestamp(event.collection_time), -// values: !event.attributes ? {} : event.attributes, -// provider: event.provider_name, -// } as AmlEvent; -// }); -// return ke.sort(selectSooner); -// } - type NewDecision = { request: Omit<Omit<AmlDecisionRequest, "justification">, "officer_sig">; askInformation: boolean; @@ -121,10 +71,12 @@ export function CaseDetails({ account, routeToShowCollectedInfo, onNewDecision, + routeToShowTransfers, }: { onNewDecision: (d: Partial<DecisionRequest>) => void; routeToShowCollectedInfo: RouteDefinition<{ cid: string; rowId: string }>; account: string; + routeToShowTransfers: RouteDefinition<{ cid: string }>; }) { const { i18n } = useTranslationContext(); const details = useAccountInformation(account); @@ -188,6 +140,14 @@ export function CaseDetails({ > <i18n.Translate>New decision</i18n.Translate> </button> + <a + href={routeToShowTransfers.url({ + cid: account, + })} + class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" + > + <i18n.Translate>Show transfers</i18n.Translate> + </a> </div> ); } diff --git a/packages/aml-backoffice-ui/src/pages/Transfers.tsx b/packages/aml-backoffice-ui/src/pages/Transfers.tsx @@ -7,6 +7,8 @@ import { encodeCrock, hashNormalizedPaytoUri, HttpStatusCode, + PaytoHash, + stringifyPaytoUri, TalerError, } from "@gnu-taler/taler-util"; import { @@ -28,8 +30,10 @@ import { Officer } from "./Officer.js"; export function Transfers({ routeToCaseById, + account, }: { routeToCaseById: RouteDefinition<{ cid: string }>; + account?: PaytoHash; }): VNode { const { i18n, dateLocale } = useTranslationContext(); const { config } = useExchangeApiContext(); @@ -85,14 +89,15 @@ export function Transfers({ const resp = useTransferList({ direction, threshold, + account, }); - const isDebit = direction === "debit"; - + const theMoneyIsDirectedToTheExchange = direction === "debit"; + const searchingForAnAccount = account !== undefined; if (!resp) { return <Loading />; } - if (resp instanceof TalerError) { + if (resp instanceof Error) { return <ErrorLoadingWithDebug error={resp} />; } if (resp.type === "fail") { @@ -143,19 +148,40 @@ export function Transfers({ return ( <div class="px-4 mt-8"> <div class="sm:flex sm:items-center"> - <div class="sm:flex-auto"> + <div class="sm:flex-auto"> + {searchingForAnAccount ? ( + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Transfers history for account</i18n.Translate> + {' '} + <a + href={routeToCaseById.url({ + cid: account, + })} + class="text-indigo-600 hover:text-indigo-900 font-mono" + > + {account} + </a> + </h1> + ) : ( <h1 class="text-base font-semibold leading-6 text-gray-900"> <i18n.Translate>Transfers history</i18n.Translate> </h1> - </div> + )} + </div> </div> <FormUI design={design} model={form.model} /> <Attention type="low" title={i18n.str`No transfers yet.`}> - <i18n.Translate> - There are no transfer reported to the exchange with the current - threshold. - </i18n.Translate> + {searchingForAnAccount ? ( + <i18n.Translate> + There are no transfer for this account. + </i18n.Translate> + ) : ( + <i18n.Translate> + There are no transfer reported to the exchange with the current + threshold. + </i18n.Translate> + )} </Attention> </div> ); @@ -184,9 +210,24 @@ export function Transfers({ <div class="px-4 mt-8"> <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>Transfers history</i18n.Translate> - </h1> + {searchingForAnAccount ? ( + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Transfers history for account</i18n.Translate> + {' '} + <a + href={routeToCaseById.url({ + cid: account, + })} + class="text-indigo-600 hover:text-indigo-900 font-mono" + > + {account} + </a> + </h1> + ) : ( + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Transfers history</i18n.Translate> + </h1> + )} </div> </div> <FormUI design={design} model={form.model} /> @@ -251,10 +292,16 @@ export function Transfers({ <i18n.Translate>Amount</i18n.Translate> </dt> <dd class="mt-1 truncate text-gray-700"> - {isDebit ? i18n.str`sent` : i18n.str`received`}{" "} + {theMoneyIsDirectedToTheExchange + ? i18n.str`sent` + : i18n.str`received`}{" "} {item.amount ? ( <span - data-negative={isDebit ? "true" : "false"} + data-negative={ + theMoneyIsDirectedToTheExchange + ? "true" + : "false" + } class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600" > <RenderAmount @@ -279,7 +326,9 @@ export function Transfers({ })} class="text-indigo-600 hover:text-indigo-900 font-mono" > - {isDebit ? i18n.str`to` : i18n.str`from`}{" "} + {theMoneyIsDirectedToTheExchange + ? i18n.str`to` + : i18n.str`from`}{" "} {item.payto_uri} </a> </dd> @@ -291,13 +340,15 @@ export function Transfers({ </dl> </td> <td - data-negative={isDebit ? "true" : "false"} + data-negative={ + theMoneyIsDirectedToTheExchange ? "true" : "false" + } class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 " > {item.amount ? ( <RenderAmount value={Amounts.parseOrThrow(item.amount)} - negative={isDebit} + negative={theMoneyIsDirectedToTheExchange} withColor spec={config.config.currency_specification} /> diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -1174,7 +1174,7 @@ export class TalerExchangeHttpClient { url.searchParams.set("threshold", Amounts.stringify(params.threshold)); } if (params.account) { - url.searchParams.set("h_payto", Amounts.stringify(params.account)); + url.searchParams.set("h_payto", params.account); } const resp = await this.fetch(url, { @@ -1225,7 +1225,7 @@ export class TalerExchangeHttpClient { url.searchParams.set("threshold", Amounts.stringify(params.threshold)); } if (params.account) { - url.searchParams.set("h_payto", Amounts.stringify(params.account)); + url.searchParams.set("h_payto", params.account); } const resp = await this.fetch(url, { @@ -1276,7 +1276,7 @@ export class TalerExchangeHttpClient { url.searchParams.set("threshold", Amounts.stringify(params.threshold)); } if (params.account) { - url.searchParams.set("h_payto", Amounts.stringify(params.account)); + url.searchParams.set("h_payto", params.account); } const resp = await this.fetch(url, {