import { AbsoluteTime, 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"; export type NotificationMessage = ErrorNotification | InfoNotification; export interface ErrorNotification { type: "error"; title: TranslatedString; ack?: boolean; timeout?: boolean; description?: TranslatedString; debug?: any; when: AbsoluteTime; } export interface InfoNotification { type: "info"; title: TranslatedString; ack?: boolean; timeout?: boolean; when: AbsoluteTime; } const storage = memoryMap>(); const NOTIFICATION_KEY = "notification"; export const GLOBAL_NOTIFICATION_TIMEOUT = Duration.fromSpec({ seconds: 5, }); function updateInStorage(n: NotificationMessage) { const h = hash(n); const mem = storage.get(NOTIFICATION_KEY) ?? new Map(); const newState = new Map(mem); newState.set(h, n); storage.set(NOTIFICATION_KEY, newState); } export function notify(notif: NotificationMessage): void { const currentState: Map = storage.get(NOTIFICATION_KEY) ?? new Map(); const newState = currentState.set(hash(notif), notif); if (GLOBAL_NOTIFICATION_TIMEOUT.d_ms !== "forever") { setTimeout(() => { notif.timeout = true; updateInStorage(notif); }, GLOBAL_NOTIFICATION_TIMEOUT.d_ms); } storage.set(NOTIFICATION_KEY, newState); } export function notifyError( title: TranslatedString, description: TranslatedString | undefined, debug?: any, ) { notify({ type: "error" as const, title, description, debug, when: AbsoluteTime.now(), }); } export function notifyException(title: TranslatedString, ex: Error) { notify({ type: "error" as const, title, description: ex.message as TranslatedString, debug: ex.stack, when: AbsoluteTime.now(), }); } export function notifyInfo(title: TranslatedString) { notify({ type: "info" as const, title, when: AbsoluteTime.now(), }); } export type Notification = { message: NotificationMessage; acknowledge: () => void; }; export function useNotifications(): Notification[] { const [, setLastUpdate] = useState(); const value = storage.get(NOTIFICATION_KEY) ?? new Map(); useEffect(() => { return storage.onUpdate(NOTIFICATION_KEY, () => { setLastUpdate(Date.now()) // const mem = storage.get(NOTIFICATION_KEY) ?? new Map(); // setter(structuredClone(mem)); }); }); return Array.from(value.values()).map((message, idx) => { return { message, acknowledge: () => { message.ack = true; updateInStorage(message); }, }; }); } function hashCode(str: string): string { if (str.length === 0) return "0"; let hash = 0; let chr; for (let i = 0; i < str.length; i++) { chr = str.charCodeAt(i); hash = (hash << 5) - hash + chr; hash |= 0; // Convert to 32bit integer } return hash.toString(16); } function hash(msg: NotificationMessage): string { let str = (msg.type + ":" + msg.title) as string; if (msg.type === "error") { if (msg.description) { str += ":" + msg.description; } if (msg.debug) { str += ":" + msg.debug; } } return hashCode(str); } function errorMap>( resp: T, map: (d: T["case"]) => TranslatedString, ): void { notify({ type: "error", title: map(resp.case), description: resp.detail.hint as TranslatedString, debug: resp.detail, when: AbsoluteTime.now(), }); } export type ErrorNotificationHandler = ( cb: (notify: typeof errorMap) => Promise, ) => Promise; /** * @deprecated use useLocalNotificationHandler * * @returns */ export function useLocalNotification(): [ Notification | undefined, (n: NotificationMessage) => void, ErrorNotificationHandler, ] { const { i18n } = useTranslationContext(); const [value, setter] = useState(); const notif = !value ? undefined : { message: value, acknowledge: () => { setter(undefined); }, }; async function errorHandling(cb: (notify: typeof errorMap) => Promise) { try { return await cb(errorMap); } catch (error: unknown) { if (error instanceof TalerError) { notify(buildUnifiedRequestErrorMessage(i18n, error)); } else { notifyError( i18n.str`Operation failed, please report`, (error instanceof Error ? error.message : JSON.stringify(error)) as TranslatedString, ); } } } return [notif, setter, errorHandling]; } type HandlerMaker = , A, B>( onClick: () => Promise, onOperationSuccess: | ((result: T extends OperationOk ? T : never) => void) | (( result: T extends OperationOk ? T : never, ) => TranslatedString | undefined), onOperationFail: ( d: T extends OperationFail ? T : never, ) => TranslatedString, onOperationComplete?: () => void, ) => ButtonHandler; export function useLocalNotificationHandler(): [ Notification | undefined, HandlerMaker, (n: NotificationMessage) => void, ] { const [value, setter] = useState(); const notif = !value ? undefined : { message: value, acknowledge: () => { setter(undefined); }, }; function makeHandler, A, B>( onClick: () => Promise, onOperationSuccess: | ((result: T extends OperationOk ? T : never) => void) | (( result: T extends OperationOk ? T : never, ) => TranslatedString | undefined), onOperationFail: ( d: T extends OperationFail ? T : never, ) => TranslatedString, onOperationComplete?: () => void, ): ButtonHandler { return { onClick, onNotification: setter, onOperationFail, onOperationSuccess, onOperationComplete, }; } return [notif, makeHandler, setter]; } export function buildUnifiedRequestErrorMessage( i18n: InternationalizationAPI, cause: TalerError, ): ErrorNotification { let result: ErrorNotification; switch (cause.errorDetail.code) { case TalerErrorCode.GENERIC_TIMEOUT: { result = { type: "error", title: i18n.str`Request timeout`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: { result = { type: "error", title: i18n.str`Request cancelled`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { result = { type: "error", title: i18n.str`Request timeout`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { result = { type: "error", title: i18n.str`Request throttled`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { result = { type: "error", title: i18n.str`Malformed response`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.WALLET_NETWORK_ERROR: { result = { type: "error", title: i18n.str`Network error`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { result = { type: "error", title: i18n.str`Unexpected request error`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } default: { result = { type: "error", title: i18n.str`Unexpected error`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), when: AbsoluteTime.now(), }; break; } } return result; }