commit b8d685c7fbbdacab592fa414d89f56dd7507bf51
parent 0412802078c5e547c4121985c228924f37f53434
Author: Sebastian <sebasjm@taler-systems.com>
Date: Thu, 16 Apr 2026 14:59:56 -0300
fix #10959
Diffstat:
4 files changed, 203 insertions(+), 134 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx
@@ -118,7 +118,7 @@ export function Timeline({ events: e }: Props) {
<Tooltip
text={i18n.str`This wire transfer has been notified by the payment service provider.`}
>
- <i class="timeline-marker is-icon is-danger timeline-marker is-icon icon mdi mdi-cash" />
+ <i class="timeline-marker is-icon timeline-marker is-icon icon mdi mdi-cash" />
</Tooltip>
);
case "refund-created":
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx
@@ -29,14 +29,10 @@ const TALER_SCREEN_ID = 71;
export interface Props {
transfers: PaginatedResult<TalerMerchantApi.TransferDetails[]>;
incomings: PaginatedResult<TalerMerchantApi.ExpectedTransferEntry[]>;
- onSelectedToConfirm: (wid: TalerMerchantApi.ExpectedTransferEntry) => void;
+ onDetails: (wid: number) => void;
}
-export function ListPage({
- transfers,
- incomings,
- onSelectedToConfirm,
-}: Props): VNode {
+export function ListPage({ transfers, incomings, onDetails }: Props): VNode {
return (
<section class="section is-main-section">
{!incomings.body.length ? undefined : (
@@ -47,7 +43,7 @@ export function ListPage({
}))}
onLoadMoreBefore={incomings.loadFirst}
onLoadMoreAfter={incomings.loadNext}
- onSelectedToConfirm={onSelectedToConfirm}
+ onDetails={onDetails}
/>
)}
{/* // ) : ( */}
@@ -58,6 +54,7 @@ export function ListPage({
}))}
onLoadMoreBefore={transfers.loadFirst}
onLoadMoreAfter={transfers.loadNext}
+ onDetails={onDetails}
/>
{/* // )} */}
</section>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx
@@ -19,13 +19,22 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util";
import {
+ AccessToken,
+ Amounts,
+ assertUnreachable,
+ HttpStatusCode,
+ TalerMerchantApi,
+} from "@gnu-taler/taler-util";
+import {
+ ButtonBetterBulma,
+ LocalNotificationBannerBulma,
RenderAmountBulma,
+ useLocalNotificationBetter,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useSessionContext } from "../../../../context/session.js";
import { WithId } from "../../../../declaration.js";
import {
@@ -33,6 +42,7 @@ import {
usePreference,
} from "../../../../hooks/preference.js";
import { Tooltip } from "../../../../components/Tooltip.js";
+import { revalidateInstanceIncomingTransfers } from "../../../../hooks/transfer.js";
const TALER_SCREEN_ID = 72;
@@ -40,132 +50,188 @@ interface TablePropsIncoming {
transfers: (TalerMerchantApi.ExpectedTransferEntry & WithId)[];
onLoadMoreBefore?: () => void;
onLoadMoreAfter?: () => void;
- onSelectedToConfirm: (d: TalerMerchantApi.ExpectedTransferEntry) => void;
+ onDetails: (d: number) => void;
}
export function CardTableIncoming({
transfers,
onLoadMoreAfter,
onLoadMoreBefore,
- onSelectedToConfirm,
+ onDetails,
}: TablePropsIncoming): VNode {
const { i18n } = useTranslationContext();
const [preferences] = usePreference();
const { config } = useSessionContext();
+ const { state: session, lib } = useSessionContext();
+
+ const [notification, safeFunctionHandler] = useLocalNotificationBetter();
+
+ const confirm = safeFunctionHandler(
+ i18n.str`inform wire transfer`,
+ (at: AccessToken, wt: TalerMerchantApi.ExpectedTransferEntry) =>
+ lib.instance.informWireTransfer(at, {
+ credit_amount: wt.expected_credit_amount!,
+ exchange_url: wt.exchange_url,
+ payto_uri: wt.payto_uri,
+ wtid: wt.wtid,
+ }),
+ );
+ confirm.onSuccess = () => {
+ revalidateInstanceIncomingTransfers();
+ };
+ confirm.onFail = (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`Unauthorized.`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`Not found.`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`Wire transfer already confirmed.`;
+ default:
+ assertUnreachable(fail);
+ }
+ };
return (
- <div class="card has-table">
- <header class="card-header">
- <p class="card-header-title">
- <i class="icon mdi mdi-star" />
- <i18n.Translate>New wire transfers</i18n.Translate>
- </p>
- </header>
- <div class="card-content">
- <div class=" has-pagination">
- <div class="table-wrapper has-mobile-cards">
- {transfers.length > 0 ? (
- <div class="">
- {onLoadMoreBefore && (
- <Tooltip
- text={i18n.str`Load more wire transfers preceding the first one`}
- >
- <button
- type="button"
- class="button is-fullwidth"
- onClick={onLoadMoreBefore}
+ <Fragment>
+ <LocalNotificationBannerBulma notification={notification} />
+
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <i class="icon mdi mdi-star" />
+ <i18n.Translate>New wire transfers</i18n.Translate>
+ </p>
+ </header>
+ <div class="card-content">
+ <div class=" has-pagination">
+ <div class="table-wrapper has-mobile-cards">
+ {transfers.length > 0 ? (
+ <div class="">
+ {onLoadMoreBefore && (
+ <Tooltip
+ text={i18n.str`Load more wire transfers preceding the first one`}
>
- <i18n.Translate>Load first page</i18n.Translate>
- </button>
- </Tooltip>
- )}
- <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
- <thead>
- <tr>
- <th>
- <i18n.Translate>Date</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>Amount</i18n.Translate>
- </th>
- <th>
- <i18n.Translate>ID</i18n.Translate>
- </th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- {transfers.map((i, idx) => {
- return (
- <tr key={i.id}>
- <td>
- {i.execution_time
- ? i.execution_time.t_s == "never"
- ? i18n.str`never`
- : format(
- i.execution_time.t_s * 1000,
- datetimeFormatForPreferences(preferences),
- )
- : i18n.str`unknown`}
- </td>
- <td>
- {i.expected_credit_amount ? (
- <RenderAmountBulma
- value={Amounts.parseOrThrow(
- i.expected_credit_amount,
- )}
- specMap={config.currencies}
- />
- ) : (
- <span>
- <i18n.Translate>
- To be determined.
- </i18n.Translate>
- </span>
- )}
- </td>
- <td title={i.wtid}>{i.wtid}</td>
- <td class="is-actions-cell right-sticky">
- {!i.expected_credit_amount ? undefined : (
- <div class="buttons is-right">
- <Tooltip
- text={i18n.str`Show details about the incoming wire transfer.`}
- >
- <a
- class="button is-info is-small"
- onClick={() => onSelectedToConfirm(i)}
+ <button
+ type="button"
+ class="button is-fullwidth"
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>Load first page</i18n.Translate>
+ </button>
+ </Tooltip>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Date</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Amount</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {transfers.map((i, idx) => {
+ return (
+ <tr key={i.id}>
+ <td
+ onClick={(): void =>
+ onDetails(i.expected_transfer_serial_id!)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.execution_time
+ ? i.execution_time.t_s == "never"
+ ? i18n.str`never`
+ : format(
+ i.execution_time.t_s * 1000,
+ datetimeFormatForPreferences(preferences),
+ )
+ : i18n.str`unknown`}
+ </td>
+ <td
+ onClick={(): void =>
+ onDetails(i.expected_transfer_serial_id!)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.expected_credit_amount ? (
+ <RenderAmountBulma
+ value={Amounts.parseOrThrow(
+ i.expected_credit_amount,
+ )}
+ specMap={config.currencies}
+ />
+ ) : (
+ <span>
+ <i18n.Translate>
+ To be determined.
+ </i18n.Translate>
+ </span>
+ )}
+ </td>
+ <td
+ title={i.wtid}
+ onClick={(): void =>
+ onDetails(i.expected_transfer_serial_id!)
+ }
+ style={{ cursor: "pointer" }}
+ >
+ {i.wtid}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ {!i.expected_credit_amount ? undefined : (
+ <div class="buttons is-right">
+ <Tooltip
+ text={i18n.str`You confirm that the incoming wire transfer has arrived into your bank account.`}
>
- <i18n.Translate>Details</i18n.Translate>
- </a>
- </Tooltip>
- </div>
- )}
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
- {onLoadMoreAfter && (
- <Tooltip
- text={i18n.str`Load more transfers after the last one`}
- >
- <button
- type="button"
- class="button is-fullwidth"
- onClick={onLoadMoreAfter}
+ <ButtonBetterBulma
+ class="button is-success is-small"
+ onClick={
+ !session.token
+ ? undefined
+ : confirm.withArgs(session.token, i)
+ }
+ type="button"
+ >
+ Confirm
+ </ButtonBetterBulma>
+ </Tooltip>
+ </div>
+ )}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {onLoadMoreAfter && (
+ <Tooltip
+ text={i18n.str`Load more transfers after the last one`}
>
- <i18n.Translate>Load next page</i18n.Translate>
- </button>
- </Tooltip>
- )}
- </div>
- ) : (
- <EmptyTable />
- )}
+ <button
+ type="button"
+ class="button is-fullwidth"
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>Load next page</i18n.Translate>
+ </button>
+ </Tooltip>
+ )}
+ </div>
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
</div>
</div>
</div>
- </div>
+ </Fragment>
);
}
@@ -173,12 +239,14 @@ interface TablePropsVerified {
transfers: (TalerMerchantApi.TransferDetails & WithId)[];
onLoadMoreBefore?: () => void;
onLoadMoreAfter?: () => void;
+ onDetails: (d: number) => void;
}
export function CardTableVerified({
transfers,
onLoadMoreAfter,
onLoadMoreBefore,
+ onDetails,
}: TablePropsVerified): VNode {
const { i18n } = useTranslationContext();
const { config } = useSessionContext();
@@ -211,7 +279,7 @@ export function CardTableVerified({
</button>
</Tooltip>
)}
- <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <table class="table is-fullwidth is-striped is-hoverable">
<thead>
<tr>
<th>
@@ -223,9 +291,7 @@ export function CardTableVerified({
<th>
<i18n.Translate>ID</i18n.Translate>
</th>
- <th>
- <i18n.Translate>Expected</i18n.Translate>
- </th>
+ <th></th>
</tr>
</thead>
<tbody>
@@ -255,11 +321,21 @@ export function CardTableVerified({
{i.wtid}
</td>
<td>
- {i.expected ? (
- <i class="icon mdi mdi-check" />
- ) : (
- <i class="icon mdi mdi-close" />
- )}
+ <div class="buttons is-right">
+ <Tooltip
+ text={i18n.str`Show details about the incoming wire transfer.`}
+ >
+ <button
+ class="button is-info is-small"
+ onClick={() => {
+ onDetails(i.transfer_serial_id);
+ }}
+ type="button"
+ >
+ <i18n.Translate>Details</i18n.Translate>
+ </button>
+ </Tooltip>
+ </div>
</td>
</tr>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx
@@ -46,6 +46,7 @@ import {
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { ListPage } from "./ListPage.js";
+import { useSessionContext } from "../../../../context/session.js";
interface Props {
onTransferDetails: (id: number) => void;
@@ -163,7 +164,6 @@ function ListTransferWithBank({
setForm={setForm}
incomings={incoming}
transfers={confirmed}
- onSelect={() => undefined}
/>
);
}
@@ -174,7 +174,6 @@ function ListTransferInternal({
accounts,
form,
setForm,
- onSelect,
onTransferDetails,
}: {
onTransferDetails: (id: number) => void;
@@ -183,7 +182,6 @@ function ListTransferInternal({
accounts: string[];
setForm: StateUpdater<Form>;
form: Form;
- onSelect: () => void;
}): VNode {
const { i18n } = useTranslationContext();
@@ -221,9 +219,7 @@ function ListTransferInternal({
<ListPage
transfers={transfers}
incomings={incomings}
- onSelectedToConfirm={(e) =>
- onTransferDetails(e.expected_transfer_serial_id!)
- }
+ onDetails={(id) => onTransferDetails(id)}
/>
</Fragment>
);