aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx19
-rw-r--r--packages/demobank-ui/src/components/Transactions/views.tsx32
-rw-r--r--packages/demobank-ui/src/context/config.ts1
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx6
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx9
-rw-r--r--packages/web-util/src/components/Attention.tsx62
-rw-r--r--packages/web-util/src/components/Button.tsx3
-rw-r--r--packages/web-util/src/components/GlobalNotificationBanner.tsx11
-rw-r--r--packages/web-util/src/hooks/useNotifications.ts47
9 files changed, 113 insertions, 77 deletions
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx
index db1fdbfc5..90ee6bc2f 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -148,16 +148,15 @@ export function ReadyView({
locale: dateLocale,
});
return (
- <tr
+ <a
+ name="cashout details"
key={idx}
- class="border-b border-gray-200 hover:bg-gray-200 last:border-none"
+ class="table-row border-b border-gray-200 hover:bg-gray-200 last:border-none"
+ // class="table-row"
+ href={routeCashoutDetails.url({
+ cid: String(item.id),
+ })}
>
- <a
- name="cashout details"
- href={routeCashoutDetails.url({
- cid: String(item.id),
- })}
- >
<td class="relative py-2 pl-2 pr-2 text-sm ">
<div class="font-medium text-gray-900">
{creationTime}
@@ -201,10 +200,10 @@ export function ReadyView({
</td>
<td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
+
{item.subject}
</td>
- </a>
- </tr>
+ </a>
);
})}
</Fragment>
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx
index ba400b37a..cdf134b2f 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Transactions/views.tsx
@@ -16,11 +16,10 @@
import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useBankCoreApiContext } from "../../context/config.js";
import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
import { State } from "./index.js";
-import { useAccountDetails } from "../../hooks/access.js";
export function ReadyView({
transactions,
@@ -31,21 +30,24 @@ export function ReadyView({
const { i18n, dateLocale } = useTranslationContext();
const { config } = useBankCoreApiContext()
- if (!transactions.length) return <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>Transactions history</i18n.Translate>
- </h1>
+ if (!transactions.length) {
+ return <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>Transactions history</i18n.Translate>
+ </h1>
+ </div>
</div>
- </div>
- <Attention type="info" title={i18n.str`No moves in your account yet.`}>
- <i18n.Translate>
- You can start sending a wire transfer or withdrawing to your wallet.
- </i18n.Translate>
- </Attention>
- </div>;
+ <Attention type="low" title={i18n.str`No transactions yet.`}>
+ <i18n.Translate>
+ You can start sending a wire transfer or withdrawing to your wallet.
+ </i18n.Translate>
+ </Attention>
+ </div>;
+ }
+
const txByDate = transactions.reduce(
(prev, cur) => {
const d =
diff --git a/packages/demobank-ui/src/context/config.ts b/packages/demobank-ui/src/context/config.ts
index 529108275..e968b7ff4 100644
--- a/packages/demobank-ui/src/context/config.ts
+++ b/packages/demobank-ui/src/context/config.ts
@@ -239,6 +239,7 @@ class CacheAwareTalerCoreBankHttpClient extends TalerCoreBankHttpClient {
if (resp.type === "ok") {
await revalidateAccountDetails();
await revalidateCashouts();
+ await revalidateTransactions();
}
return resp;
}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index b914aa360..b6bfe1cfb 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -144,7 +144,11 @@ export function BankFrame({
</Header>
</div>
- <GlobalNotificationsBanner />
+ <div class="fixed z-20 w-full">
+ <div class="mx-auto w-4/5">
+ <GlobalNotificationsBanner />
+ </div>
+ </div>
<main class="-mt-32 flex-1">
{account && routeAccountDetails && (
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index 09c0a8785..f0ca447e1 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -15,25 +15,22 @@
*/
import {
- HttpStatusCode,
- TranslatedString,
- assertUnreachable,
+ HttpStatusCode
} from "@gnu-taler/taler-util";
import {
Button,
LocalNotificationBanner,
ShowInputErrorLabel,
- useLocalNotification,
useLocalNotificationHandler,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { useBankCoreApiContext } from "../context/config.js";
import { useBackendState } from "../hooks/backend.js";
+import { RouteDefinition } from "../route.js";
import { undefinedIfEmpty } from "../utils.js";
import { doAutoFocus } from "./PaytoWireTransferForm.js";
-import { EmptyObject, RouteDefinition } from "../route.js";
/**
* Collect and submit login data.
diff --git a/packages/web-util/src/components/Attention.tsx b/packages/web-util/src/components/Attention.tsx
index b85230a1b..50378e85a 100644
--- a/packages/web-util/src/components/Attention.tsx
+++ b/packages/web-util/src/components/Attention.tsx
@@ -1,36 +1,51 @@
-import { TranslatedString, assertUnreachable } from "@gnu-taler/taler-util";
+import { Duration, TranslatedString, assertUnreachable } from "@gnu-taler/taler-util";
import { ComponentChildren, Fragment, VNode, h } from "preact";
interface Props {
- type?: "info" | "success" | "warning" | "danger",
+ type?: "info" | "success" | "warning" | "danger" | "low",
onClose?: () => void,
title: TranslatedString,
children?: ComponentChildren,
+ timeout?: Duration,
}
-export function Attention({ type = "info", title, children, onClose }: Props): VNode {
+export function Attention({ type = "info", title, children, onClose, timeout = Duration.getForever() }: Props): VNode {
return <div class={`group attention-${type} mt-2 shadow-lg`}>
- <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">
+ <style>{`
+ .progress {
+ animation: notificationTimeoutBar 3s ease-in-out;
+ animation-fill-mode:both;
+ }
+
+ @keyframes notificationTimeoutBar {
+ 0% { width: 0; }
+ 100% { width: 100%; }
+ }
+ `}</style>
+
+ <div data-timed={timeout.d_ms !== "forever"} class="rounded-md data-[timed=true]:rounded-b-none group-[.attention-info]:bg-blue-50 group-[.attention-low]:bg-gray-100 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>
+ {type === "low" ? undefined :
+ <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">
+ <h3 class="text-sm 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">
@@ -53,6 +68,11 @@ export function Attention({ type = "info", title, children, onClose }: Props): V
}
</div>
</div>
+ {timeout.d_ms === "forever" ? undefined :
+ <div class="meter group-[.attention-info]:bg-blue-50 group-[.attention-low]:bg-gray-100 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 h-1 relative overflow-hidden -mt-1">
+ <span class="w-full h-full block"><span class="h-full block progress group-[.attention-info]:bg-blue-600 group-[.attention-low]:bg-gray-600 group-[.attention-warning]:bg-yellow-600 group-[.attention-danger]:bg-red-600 group-[.attention-success]:bg-green-600"></span></span>
+ </div>
+ }
</div>
}
diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx
index 758efafcf..26b778eec 100644
--- a/packages/web-util/src/components/Button.tsx
+++ b/packages/web-util/src/components/Button.tsx
@@ -45,8 +45,7 @@ export function Button<T extends OperationResult<A, B>, A, B>({
handler.onClick().then((resp) => {
if (resp) {
if (resp.type === "ok") {
- // @ts-expect-error this is an operationOk
- const result: OperationOk<any> = resp.body
+ const result: OperationOk<any> = resp
// @ts-expect-error this is an operationOk
const msg = handler.onOperationSuccess(result)
if (msg) {
diff --git a/packages/web-util/src/components/GlobalNotificationBanner.tsx b/packages/web-util/src/components/GlobalNotificationBanner.tsx
index c8049acc3..b0a06f7e1 100644
--- a/packages/web-util/src/components/GlobalNotificationBanner.tsx
+++ b/packages/web-util/src/components/GlobalNotificationBanner.tsx
@@ -1,28 +1,27 @@
import { Fragment, VNode, h } from "preact"
-import { Attention, useNotifications } from "../index.browser.js"
+import { Attention, GLOBAL_NOTIFICATION_TIMEOUT, useNotifications } from "../index.browser.js"
export function GlobalNotificationsBanner(): VNode {
const notifs = useNotifications()
if (notifs.length === 0) return <Fragment />
- return <div class="fixed z-20 w-full p-4"> {
+ return <Fragment> {
notifs.map(n => {
switch (n.message.type) {
case "error":
return <Attention type="danger" title={n.message.title} onClose={() => {
n.remove()
- }}>
+ }} timeout={GLOBAL_NOTIFICATION_TIMEOUT}>
{n.message.description &&
<div class="mt-2 text-sm text-red-700">
{n.message.description}
</div>
}
- {/* <MaybeShowDebugInfo info={n.message.debug} /> */}
</Attention>
case "info":
return <Attention type="success" title={n.message.title} onClose={() => {
n.remove();
- }} />
+ }} timeout={GLOBAL_NOTIFICATION_TIMEOUT} />
}
})}
- </div>
+ </Fragment>
}
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 33e0cdf53..9f955f92d 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -1,4 +1,4 @@
-import { OperationFail, OperationOk, OperationResult, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
+import { Duration, OperationFail, OperationOk, OperationResult, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
import { ButtonHandler } from "../components/Button.js";
import { InternationalizationAPI, memoryMap, useTranslationContext } from "../index.browser.js";
@@ -19,10 +19,28 @@ export interface InfoNotification {
const storage = memoryMap<Map<string, NotificationMessage>>();
const NOTIFICATION_KEY = "notification";
+export const GLOBAL_NOTIFICATION_TIMEOUT: Duration = { d_ms: 3 * 1000 }
+
+function removeFromStorage(n: NotificationMessage) {
+ const h = hash(n)
+ const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
+ const newState = new Map(mem);
+ newState.delete(h);
+ storage.set(NOTIFICATION_KEY, newState);
+}
+
+
export function notify(notif: NotificationMessage): void {
const currentState: Map<string, NotificationMessage> =
storage.get(NOTIFICATION_KEY) ?? new Map();
const newState = currentState.set(hash(notif), notif);
+
+ if (GLOBAL_NOTIFICATION_TIMEOUT.d_ms !== "forever") {
+ setTimeout(() => {
+ removeFromStorage(notif)
+ }, GLOBAL_NOTIFICATION_TIMEOUT.d_ms);
+ }
+
storage.set(NOTIFICATION_KEY, newState);
}
export function notifyError(
@@ -73,10 +91,7 @@ export function useNotifications(): Notification[] {
return {
message,
remove: () => {
- const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
- const newState = new Map(mem);
- newState.delete(hash(message));
- storage.set(NOTIFICATION_KEY, newState);
+ removeFromStorage(message)
},
};
});
@@ -124,7 +139,7 @@ export type ErrorNotificationHandler = (cb: (notify: typeof errorMap) => Promise
* @returns
*/
export function useLocalNotification(): [Notification | undefined, (n: NotificationMessage) => void, ErrorNotificationHandler] {
- const {i18n} = useTranslationContext();
+ const { i18n } = useTranslationContext();
const [value, setter] = useState<NotificationMessage>();
const notif = !value ? undefined : {
@@ -154,12 +169,12 @@ export function useLocalNotification(): [Notification | undefined, (n: Notificat
return [notif, setter, errorHandling]
}
-type HandlerMaker = <T extends OperationResult<A, B>,A,B>(
+type HandlerMaker = <T extends OperationResult<A, B>, A, B>(
onClick: () => Promise<T | undefined>,
- onOperationSuccess: ((result:T extends OperationOk<any> ? T :never) => void) | ((result:T extends OperationOk<any> ? T :never) => TranslatedString | undefined),
+ onOperationSuccess: ((result: T extends OperationOk<any> ? T : never) => void) | ((result: T extends OperationOk<any> ? T : never) => TranslatedString | undefined),
onOperationFail: (d: T extends OperationFail<any> ? T : never) => TranslatedString,
onOperationComplete?: () => void,
-) => ButtonHandler<T,A,B>;
+) => ButtonHandler<T, A, B>;
export function useLocalNotificationHandler(): [Notification | undefined, HandlerMaker, (n: NotificationMessage) => void] {
const [value, setter] = useState<NotificationMessage>();
@@ -169,20 +184,20 @@ export function useLocalNotificationHandler(): [Notification | undefined, Handle
setter(undefined);
},
}
-
- function makeHandler<T extends OperationResult<A, B>,A,B>(
+
+ function makeHandler<T extends OperationResult<A, B>, A, B>(
onClick: () => Promise<T | undefined>,
- onOperationSuccess: ((result:T extends OperationOk<any> ? T :never) => void) | ((result:T extends OperationOk<any> ? T :never) => TranslatedString | undefined),
+ onOperationSuccess: ((result: T extends OperationOk<any> ? T : never) => void) | ((result: T extends OperationOk<any> ? T : never) => TranslatedString | undefined),
onOperationFail: (d: T extends OperationFail<any> ? T : never) => TranslatedString,
- onOperationComplete?: () => void,
- ): ButtonHandler<T,A,B> {
+ onOperationComplete?: () => void,
+ ): ButtonHandler<T, A, B> {
return { onClick, onNotification: setter, onOperationFail, onOperationSuccess, onOperationComplete }
}
-
+
return [notif, makeHandler, setter]
}
-export function buildRequestErrorMessage( i18n: InternationalizationAPI, cause: TalerError): ErrorNotification {
+export function buildRequestErrorMessage(i18n: InternationalizationAPI, cause: TalerError): ErrorNotification {
let result: ErrorNotification;
switch (cause.errorDetail.code) {
case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {