import { TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util"; import { useEffect, useState } from "preact/hooks"; import { memoryMap, useTranslationContext } from "../index.browser.js"; export type NotificationMessage = ErrorNotification | InfoNotification; export interface ErrorNotification { type: "error"; title: TranslatedString; description?: TranslatedString; debug?: any; } export interface InfoNotification { type: "info"; title: TranslatedString; } const storage = memoryMap>(); const NOTIFICATION_KEY = "notification"; export function notify(notif: NotificationMessage): void { const currentState: Map = storage.get(NOTIFICATION_KEY) ?? new Map(); const newState = currentState.set(hash(notif), notif); storage.set(NOTIFICATION_KEY, newState); } export function notifyError( title: TranslatedString, description: TranslatedString | undefined, debug?: any, ) { notify({ type: "error" as const, title, description, debug, }); } export function notifyException( title: TranslatedString, ex: Error, ) { notify({ type: "error" as const, title, description: ex.message as TranslatedString, debug: ex.stack, }); } export function notifyInfo(title: TranslatedString) { notify({ type: "info" as const, title, }); } export type Notification = { message: NotificationMessage; remove: () => void; }; export function useNotifications(): Notification[] { const [value, setter] = useState>(new Map()); useEffect(() => { return storage.onUpdate(NOTIFICATION_KEY, () => { const mem = storage.get(NOTIFICATION_KEY) ?? new Map(); setter(structuredClone(mem)); }); }); return Array.from(value.values()).map((message, idx) => { 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); }, }; }); } 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); } export function useLocalNotification(): [Notification | undefined, (n: NotificationMessage) => void, (cb: () => Promise) => Promise] { const {i18n} = useTranslationContext(); const [value, setter] = useState(); const notif = !value ? undefined : { message: value, remove: () => { setter(undefined); }, } async function errorHandling(cb: () => Promise) { try { return await cb() } catch (error: unknown) { if (error instanceof TalerError) { notify(buildRequestErrorMessage(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 Translator = ReturnType["i18n"] function buildRequestErrorMessage( i18n: Translator, cause: TalerError): ErrorNotification { let result: ErrorNotification; switch (cause.errorDetail.code) { 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), }; 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), }; 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), }; 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), }; 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), }; break; } default: { result = { type: "error", title: i18n.str`Unexpected error`, description: cause.message as TranslatedString, debug: JSON.stringify(cause.errorDetail, undefined, 2), }; break; } } return result; }