summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-02-29 15:45:22 -0300
committerSebastian <sebasjm@gmail.com>2024-02-29 15:45:22 -0300
commit206780bb0ee763bcf50a3f4f9f78579a8adcdb3a (patch)
tree7d332c27a7c64f676d6ac7101af125a806cf6318
parent922d5b4d7aeed3f7c2f9eab1482f60d2fa50f234 (diff)
downloadwallet-core-206780bb0ee763bcf50a3f4f9f78579a8adcdb3a.tar.gz
wallet-core-206780bb0ee763bcf50a3f4f9f78579a8adcdb3a.tar.bz2
wallet-core-206780bb0ee763bcf50a3f4f9f78579a8adcdb3a.zip
observe UI, WIP
-rw-r--r--packages/taler-wallet-webextension/src/components/WalletActivity.tsx845
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx1
-rw-r--r--packages/taler-wallet-webextension/src/platform/api.ts12
-rw-r--r--packages/taler-wallet-webextension/src/platform/chrome.ts13
-rw-r--r--packages/taler-wallet-webextension/src/platform/dev.ts1
-rw-r--r--packages/taler-wallet-webextension/src/test-utils.ts3
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts10
8 files changed, 641 insertions, 246 deletions
diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
index 8c55d1fc9..41932f143 100644
--- a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
+++ b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx
@@ -14,11 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
+ AbsoluteTime,
NotificationType,
ObservabilityEventType,
+ RequestProgressNotification,
TalerErrorCode,
TalerErrorDetail,
- TransactionMajorState,
+ TaskProgressNotification,
WalletNotification,
assertUnreachable
} from "@gnu-taler/taler-util";
@@ -29,10 +31,10 @@ import { useEffect, useState } from "preact/hooks";
import { Pages } from "../NavigationBar.js";
import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
import { Modal } from "./Modal.js";
import { Time } from "./Time.js";
-import { useSettings } from "../hooks/useSettings.js";
interface Props extends JSX.HTMLAttributes {
}
@@ -41,25 +43,29 @@ interface Props extends JSX.HTMLAttributes {
export function WalletActivity({ }: Props): VNode {
const { i18n } = useTranslationContext()
const [settings, updateSettings] = useSettings()
+ const api = useBackendContext();
useEffect(() => {
document.body.style.marginBottom = "250px"
return () => {
document.body.style.marginBottom = "0px"
}
})
- const [table, setTable] = useState<"tasks" | "events" | "calls">("tasks")
+ const [table, setTable] = useState<"tasks" | "events">("tasks")
return (
<div style={{ position: "fixed", bottom: 0, background: "white", zIndex: 1, height: 250, overflowY: "scroll", width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between", float: "right" }}>
<div />
- <div onClick={() => {
- updateSettings("showWalletActivity", false)
- }}>
- close
+ <div>
+ <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => {
+ updateSettings("showWalletActivity", false)
+ }}>
+ close
+ </div>
</div>
</div>
- <div>
+ <div style={{ display: "flex", justifyContent: "space-around" }}>
<Button variant={table === "tasks" ? "contained" : "outlined"}
+ style={{ margin: 4 }}
onClick={async () => {
setTable("tasks")
}}
@@ -67,6 +73,7 @@ export function WalletActivity({ }: Props): VNode {
<i18n.Translate>Tasks</i18n.Translate>
</Button>
<Button variant={table === "events" ? "contained" : "outlined"}
+ style={{ margin: 4 }}
onClick={async () => {
setTable("events")
}}
@@ -74,22 +81,12 @@ export function WalletActivity({ }: Props): VNode {
<i18n.Translate>Events</i18n.Translate>
</Button>
- <Button variant={table === "calls" ? "contained" : "outlined"}
- onClick={async () => {
- setTable("calls")
- }}
- >
- <i18n.Translate>Calls</i18n.Translate>
- </Button>
</div>
{(function (): VNode {
switch (table) {
case "events": {
return <ObservavilityEventsTable />
}
- case "calls": {
- return <WalletCallsTable />
- }
case "tasks": {
return <ActiveTasksTable />
}
@@ -102,238 +99,599 @@ export function WalletActivity({ }: Props): VNode {
);
}
-export function WalletCallsTable({ }: {}): VNode {
- return <div />
+interface MoreInfoPRops { events: (WalletNotification & { when: AbsoluteTime })[], onClick: (content: VNode) => void }
+type Notif = {
+ id: string;
+ events: (WalletNotification & { when: AbsoluteTime })[];
+ description: string;
+ start: AbsoluteTime;
+ end: AbsoluteTime;
+ reference: {
+ eventType: NotificationType,
+ referenceType: "task" | "transaction" | "operation" | "exchange",
+ id: string;
+ } | undefined,
+ MoreInfo: (p: MoreInfoPRops) => VNode;
+}
+
+function ShowBalanceChange({ events }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.BalanceChange) return <Fragment />;
+ return <Fragment>
+ <dt>Transaction</dt>
+ <dd>
+ <a title={not.hintTransactionId} href={Pages.balanceTransaction({ tid: not.hintTransactionId })}>{not.hintTransactionId.substring(0, 10)}</a>
+ </dd>
+ </Fragment>
+}
+
+function ShowBackupOperationError({ events, onClick }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.BackupOperationError) return <Fragment />;
+ return <Fragment>
+ <dt>Error</dt>
+ <dd>
+ <a href="#" onClick={(e) => {
+ e.preventDefault();
+ const error = not.error
+ onClick(<Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>{TalerErrorCode[error.code]} ({error.code})</dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd><Time
+ timestamp={error.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ /></dd>
+ </dl>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+ </Fragment>)
+ }}>{TalerErrorCode[not.error.code]}</a>
+ </dd>
+ </Fragment>
+}
+
+function ShowTransactionStateTransition({ events, onClick }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.TransactionStateTransition) return <Fragment />;
+ return <Fragment>
+ <dt>Old state</dt>
+ <dd>
+ {not.oldTxState.major} - {not.oldTxState.minor ?? ""}
+ </dd>
+ <dt>New state</dt>
+ <dd>
+ {not.newTxState.major} - {not.newTxState.minor ?? ""}
+ </dd>
+ <dt>Transaction</dt>
+ <dd>
+ <a title={not.transactionId} href={Pages.balanceTransaction({ tid: not.transactionId })}>{not.transactionId.substring(0, 10)}</a>
+ </dd>
+ {not.errorInfo ? <Fragment>
+ <dt>Error</dt>
+ <dd>
+ <a href="#" onClick={(e) => {
+ if (!not.errorInfo) return;
+ e.preventDefault();
+ const error = not.errorInfo;
+ onClick(<Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>{TalerErrorCode[error.code]} ({error.code})</dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Message</dt>
+ <dd>{error.message ?? "--"}</dd>
+ </dl>
+ </Fragment>)
+
+ }}>{TalerErrorCode[not.errorInfo.code]}</a>
+ </dd>
+ </Fragment> : undefined}
+ <dt>Experimental</dt>
+ <dd>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(not.experimentalUserData, undefined, 2)}
+ </pre>
+ </dd>
+
+
+ </Fragment>
+}
+function ShowExchangeStateTransition({ events, onClick }: MoreInfoPRops): VNode {
+ if (!events.length) return <Fragment />;
+ const not = events[0];
+ if (not.type !== NotificationType.ExchangeStateTransition) return <Fragment />;
+ return <Fragment>
+ <dt>Exchange</dt>
+ <dd>
+ {not.exchangeBaseUrl}
+ </dd>
+ {not.oldExchangeState && not.newExchangeState.exchangeEntryStatus !== not.oldExchangeState?.exchangeEntryStatus && <Fragment>
+ <dt>Entry status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeEntryStatus} to {not.newExchangeState.exchangeEntryStatus}
+ </dd>
+ </Fragment>}
+ {not.oldExchangeState && not.newExchangeState.exchangeUpdateStatus !== not.oldExchangeState?.exchangeUpdateStatus && <Fragment>
+ <dt>Update status</dt>
+ <dd>
+ from {not.oldExchangeState.exchangeUpdateStatus} to {not.newExchangeState.exchangeUpdateStatus}
+ </dd>
+ </Fragment>}
+ {not.oldExchangeState && not.newExchangeState.tosStatus !== not.oldExchangeState?.tosStatus && <Fragment>
+ <dt>Tos status</dt>
+ <dd>
+ from {not.oldExchangeState.tosStatus} to {not.newExchangeState.tosStatus}
+ </dd>
+ </Fragment>}
+ </Fragment>
+}
+
+type ObservaNotifWithTime = ((TaskProgressNotification | RequestProgressNotification) & {
+ when: AbsoluteTime;
+})
+function ShowObservabilityEvent({ events, onClick }: MoreInfoPRops): VNode {
+ // let prev: ObservaNotifWithTime;
+ const asd = events.map(not => {
+ if (not.type !== NotificationType.RequestObservabilityEvent && not.type !== NotificationType.TaskObservabilityEvent) return <Fragment />;
+
+ const title = (function () {
+ switch (not.event.type) {
+ case ObservabilityEventType.HttpFetchFinishError:
+ case ObservabilityEventType.HttpFetchFinishSuccess:
+ case ObservabilityEventType.HttpFetchStart: return "HTTP Request"
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError:
+ case ObservabilityEventType.DbQueryStart: return "Database"
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError:
+ case ObservabilityEventType.RequestStart: return "Wallet"
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError:
+ case ObservabilityEventType.CryptoStart: return "Crypto"
+ case ObservabilityEventType.TaskStart: return "Task start"
+ case ObservabilityEventType.TaskStop: return "Task stop"
+ case ObservabilityEventType.TaskReset: return "Task reset"
+ case ObservabilityEventType.ShepherdTaskResult: return "Schedule"
+ case ObservabilityEventType.DeclareTaskDependency: return "Task dependecy"
+ }
+ })();
+
+ return <ShowObervavilityDetails title={title} notif={not} onClick={onClick} />
+
+ })
+ return <table>
+ <thead>
+ <td>Event</td>
+ <td>Info</td>
+ <td>Start</td>
+ <td>End</td>
+ </thead>
+ <tbody>
+ {asd}
+ </tbody>
+ </table>
+}
+
+function ShowObervavilityDetails({ title, notif, onClick, prev }: { title: string, notif: ObservaNotifWithTime, prev?: ObservaNotifWithTime, onClick: (content: VNode) => void }): VNode {
+ switch (notif.event.type) {
+ case ObservabilityEventType.HttpFetchStart:
+ case ObservabilityEventType.HttpFetchFinishError:
+ case ObservabilityEventType.HttpFetchFinishSuccess: {
+ return <tr>
+ <td><a href="#" onClick={(e) => {
+ e.preventDefault();
+ onClick(<Fragment>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>);
+ }}>{title}</a></td>
+ <td>
+ {notif.event.url} {
+ prev?.event.type === ObservabilityEventType.HttpFetchFinishSuccess ? `(${prev.event.status})`
+ : prev?.event.type === ObservabilityEventType.HttpFetchFinishError ? <a href="#" onClick={(e) => {
+ e.preventDefault();
+ if (prev.event.type !== ObservabilityEventType.HttpFetchFinishError) return;
+ const error = prev.event.error
+ onClick(<Fragment>
+ <dl>
+ <dt>Code</dt>
+ <dd>{TalerErrorCode[error.code]} ({error.code})</dd>
+ <dt>Hint</dt>
+ <dd>{error.hint ?? "--"}</dd>
+ <dt>Time</dt>
+ <dd><Time
+ timestamp={error.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ /></dd>
+ </dl>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+ {JSON.stringify(error, undefined, 2)}
+ </pre>
+
+ </Fragment>)
+ }}>fail</a> : undefined
+ }
+ </td>
+ <td> <Time
+ timestamp={notif.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ /></td>
+ <td> <Time
+ timestamp={prev?.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ /></td>
+ </tr>
+
+ }
+ case ObservabilityEventType.DbQueryStart:
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError: {
+ return <tr>
+ <td><a href="#" onClick={(e) => {
+ e.preventDefault();
+ onClick(<Fragment>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>);
+ }}>{title}</a></td>
+ <td>
+ {notif.event.location} {notif.event.name}
+ </td>
+ <td>
+ <Time
+ timestamp={notif.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ <td>
+ <Time
+ timestamp={prev?.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ </tr>
+ }
+
+ case ObservabilityEventType.TaskStart:
+ case ObservabilityEventType.TaskStop:
+ case ObservabilityEventType.DeclareTaskDependency:
+ case ObservabilityEventType.TaskReset: {
+ return <tr>
+ <td><a href="#" onClick={(e) => {
+ e.preventDefault();
+ onClick(<Fragment>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>);
+ }}>{title}</a></td>
+ <td>
+ {notif.event.taskId}
+ </td>
+ <td>
+ <Time
+ timestamp={notif.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ <td>
+ <Time
+ timestamp={prev?.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ </tr>
+ }
+ case ObservabilityEventType.ShepherdTaskResult: {
+ return <tr>
+ <td><a href="#" onClick={(e) => {
+ e.preventDefault();
+ onClick(<Fragment>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>);
+ }}>{title}</a></td>
+ <td>
+ {notif.event.resultType}
+ </td>
+ <td>
+ <Time
+ timestamp={notif.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ <td>
+ <Time
+ timestamp={prev?.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ </tr>
+
+ }
+ case ObservabilityEventType.CryptoStart:
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError: {
+ return <tr>
+ <td><a href="#" onClick={(e) => {
+ e.preventDefault();
+ onClick(<Fragment>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>);
+ }}>{title}</a></td>
+ <td>
+ {notif.event.operation}
+ </td>
+ <td>
+ <Time
+ timestamp={notif.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ <td>
+ <Time
+ timestamp={prev?.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ </tr>
+ }
+ case ObservabilityEventType.RequestStart:
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError: {
+ return <tr >
+ <td><a href="#" onClick={(e) => {
+ e.preventDefault();
+ onClick(<Fragment>
+ <pre
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+ >
+ {JSON.stringify({ event: notif, prev }, undefined, 2)}
+ </pre>
+ </Fragment>);
+ }}>{title}</a></td>
+ <td>
+ {notif.event.type}
+ </td>
+ <td>
+ <Time
+ timestamp={notif.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ <td>
+ <Time
+ timestamp={prev?.when}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </td>
+ </tr>
+ }
+ }
+}
+
+function getNotificationFor(id: string, event: WalletNotification, start: AbsoluteTime, list: Notif[]): Notif | undefined {
+ const eventWithTime = { ...event, when: start }
+ switch (event.type) {
+ case NotificationType.BalanceChange: {
+ return ({
+ id,
+ events: [eventWithTime],
+ reference: {
+ eventType: event.type,
+ referenceType: "transaction",
+ id: event.hintTransactionId,
+ },
+ description: "Balance change",
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowBalanceChange
+ })
+ }
+ case NotificationType.BackupOperationError: {
+ return ({
+ id,
+ events: [eventWithTime],
+ reference: undefined,
+ description: "Backup error",
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowBackupOperationError
+ })
+ }
+ case NotificationType.TransactionStateTransition: {
+ const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.transactionId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(eventWithTime)
+ return undefined
+ }
+ return ({
+ id,
+ events: [eventWithTime],
+ reference: {
+ eventType: event.type,
+ referenceType: "transaction",
+ id: event.transactionId,
+ },
+ description: event.type,
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowTransactionStateTransition
+ })
+ }
+ case NotificationType.ExchangeStateTransition: {
+ const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.exchangeBaseUrl)
+ if (found) {
+ found.end = start;
+ found.events.unshift(eventWithTime)
+ return undefined
+ }
+ return ({
+ id,
+ events: [eventWithTime],
+ description: "Exchange update",
+ reference: {
+ eventType: event.type,
+ referenceType: "exchange",
+ id: event.exchangeBaseUrl,
+ },
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowExchangeStateTransition
+ })
+ }
+ case NotificationType.TaskObservabilityEvent: {
+ const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.taskId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(eventWithTime)
+ return undefined
+ }
+ return ({
+ id,
+ events: [eventWithTime],
+ reference: {
+ eventType: event.type,
+ referenceType: "task",
+ id: event.taskId,
+ },
+ description: `Task update ${event.taskId}`,
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowObservabilityEvent
+ })
+ }
+ case NotificationType.RequestObservabilityEvent: {
+ const found = list.find(a => a.reference?.eventType === event.type && a.reference.id === event.requestId)
+ if (found) {
+ found.end = start;
+ found.events.unshift(eventWithTime)
+ return undefined
+ }
+ return ({
+ id,
+ events: [eventWithTime],
+ reference: {
+ eventType: event.type,
+ referenceType: "operation",
+ id: event.requestId,
+ },
+ description: `wallet.${event.operation}(${event.requestId})`,
+ start,
+ end: AbsoluteTime.never(),
+ MoreInfo: ShowObservabilityEvent
+ })
+ }
+ default: {
+ assertUnreachable(event)
+ }
+ }
}
-const notifications: WalletNotification[] = []
+
export function ObservavilityEventsTable({ }: {}): VNode {
const { i18n } = useTranslationContext()
const listenAllEvents = Array.from<NotificationType>({ length: 1 });
listenAllEvents.includes = () => true
const api = useBackendContext();
const [lastEvent, setLastEvent] = useState<Date>(new Date())
+ const [lastShow, setLastShow] = useState<Date>(new Date())
+
+ const [notifications, setNotifications] = useState<{ notif: WalletNotification, when: AbsoluteTime }[]>([])
useEffect(() => {
- return api.listener.onUpdateNotification(listenAllEvents, (notif) => {
- notifications.unshift(notif)
- setLastEvent(new Date())
+ return api.listener.onUpdateNotification(listenAllEvents, (not) => {
+ console.log(not)
+ const time = new Date();
+ setLastEvent(time)
+ notifications.unshift({
+ notif: not,
+ when: AbsoluteTime.now()
+ })
+ setNotifications(Array.from(notifications))
});
});
- const [showError, setShowError] = useState<TalerErrorDetail>()
- return <div>
- {showError && <ErroDetailModal error={showError} onClose={(async () => { setShowError(undefined) })} />}
- {notifications.map((not) => {
- return (
- <details>
- <summary>{not.type}</summary>
- {(function () {
- switch (not.type) {
- case NotificationType.BalanceChange: {
- return <Fragment>
- <dt>Transaction</dt>
- <dd>
- <a title={not.hintTransactionId} href={Pages.balanceTransaction({ tid: not.hintTransactionId })}>{not.hintTransactionId.substring(0, 10)}</a>
- </dd>
- </Fragment>
- }
- case NotificationType.BackupOperationError: {
- return <Fragment>
- <dt>Error</dt>
- <dd>
- <a href="#" onClick={(e) => { e.preventDefault(); setShowError(not.error) }}>{TalerErrorCode[not.error.code]}</a>
- </dd>
- </Fragment>
- }
- case NotificationType.TransactionStateTransition: {
- return <Fragment>
- <dt>Old state</dt>
- <dd>
- {not.oldTxState.major} - {not.oldTxState.minor ?? ""}
- </dd>
- <dt>New state</dt>
- <dd>
- {not.newTxState.major} - {not.newTxState.minor ?? ""}
- </dd>
- <dt>Transaction</dt>
- <dd>
- <a title={not.transactionId} href={Pages.balanceTransaction({ tid: not.transactionId })}>{not.transactionId.substring(0, 10)}</a>
- </dd>
- {not.errorInfo ? <Fragment>
- <dt>Error</dt>
- <dd>
- <a href="#" onClick={(e) => {
- e.preventDefault(); setShowError({
- code: not.errorInfo!.code,
- hint: not.errorInfo!.hint,
- message: not.errorInfo!.message,
- })
- }}>{TalerErrorCode[not.errorInfo!.code]}</a>
- </dd>
- </Fragment> : undefined}
- <dt>Experimental</dt>
- <dd>
- <pre>
- {JSON.stringify(not.experimentalUserData, undefined, 2)}
- </pre>
- </dd>
- </Fragment>
- }
- case NotificationType.ExchangeStateTransition: {
- return <Fragment>
- <dt>Exchange</dt>
- <dd>
- {not.exchangeBaseUrl}
- </dd>
- <dt>Entry status</dt>
- <dd>
- {not.newExchangeState.exchangeEntryStatus}
- </dd>
- <dt>Update status</dt>
- <dd>
- {not.newExchangeState.exchangeUpdateStatus}
- </dd>
- <dt>Tos status</dt>
- <dd>
- {not.newExchangeState.tosStatus}
- </dd>
- </Fragment>
- }
- case NotificationType.TaskObservabilityEvent: {
- return <Fragment>
- <dt>Task</dt>
- <dd>
- {not.taskId}
- </dd>
- <dt>Event</dt>
- <dd>
- {not.event.type}
- </dd>
- {(function () {
- switch (not.event.type) {
- case ObservabilityEventType.HttpFetchStart:
- case ObservabilityEventType.HttpFetchFinishError:
- case ObservabilityEventType.HttpFetchFinishSuccess: {
- return <Fragment>
- <dt>Request</dt>
- <dd>{not.event.url}</dd>
- </Fragment>
- }
- case ObservabilityEventType.DbQueryStart:
- case ObservabilityEventType.DbQueryFinishSuccess:
- case ObservabilityEventType.DbQueryFinishError: {
- return <Fragment>
- <dt>Location</dt>
- <dd>{not.event.location}</dd>
- <dt>Name</dt>
- <dd>{not.event.name}</dd>
- </Fragment>
- }
-
- case ObservabilityEventType.TaskStart:
- case ObservabilityEventType.TaskStop:
- case ObservabilityEventType.DeclareTaskDependency:
- case ObservabilityEventType.TaskReset: {
- return <Fragment>
- <dt>Task</dt>
- <dd>{not.event.taskId}</dd>
- </Fragment>
- }
- case ObservabilityEventType.ShepherdTaskResult: {
- return <Fragment>
- <dt>result</dt>
- <dd>{not.event.resultType}</dd>
- </Fragment>
-
- }
- case ObservabilityEventType.CryptoStart:
- case ObservabilityEventType.CryptoFinishSuccess:
- case ObservabilityEventType.CryptoFinishError: {
- return <Fragment>
- <dt>operation</dt>
- <dd>{not.event.operation}</dd>
- </Fragment>
- }
- case ObservabilityEventType.RequestStart:
- case ObservabilityEventType.RequestFinishSuccess:
- case ObservabilityEventType.RequestFinishError: {
- return <Fragment />
- }
- }
- })()}
- </Fragment>
- }
- case NotificationType.RequestObservabilityEvent: {
- return <Fragment>
- <dt>Operation</dt>
- <dd>
- {not.operation}
- </dd>
- <dt>Request</dt>
- <dd>
- {not.requestId}
- </dd>
- <dt>Event type</dt>
- <dd>
- {not.event.type}
- </dd>
- {(function () {
- switch (not.event.type) {
- case ObservabilityEventType.HttpFetchStart:
- case ObservabilityEventType.HttpFetchFinishError:
- case ObservabilityEventType.HttpFetchFinishSuccess: {
- return <Fragment>
- <dt>Request</dt>
- <dd>{not.event.url}</dd>
- </Fragment>
- }
- case ObservabilityEventType.DbQueryStart:
- case ObservabilityEventType.DbQueryFinishSuccess:
- case ObservabilityEventType.DbQueryFinishError: {
- return <Fragment>
- <dt>Location</dt>
- <dd>{not.event.location}</dd>
- <dt>Name</dt>
- <dd>{not.event.name}</dd>
- </Fragment>
- }
-
- case ObservabilityEventType.TaskStart:
- case ObservabilityEventType.TaskStop:
- case ObservabilityEventType.DeclareTaskDependency:
- case ObservabilityEventType.TaskReset: {
- return <Fragment>
- <dt>Task</dt>
- <dd>{not.event.taskId}</dd>
- </Fragment>
- }
- case ObservabilityEventType.ShepherdTaskResult: {
- return <Fragment>
- <dt>result</dt>
- <dd>{not.event.resultType}</dd>
- </Fragment>
-
- }
- case ObservabilityEventType.CryptoStart:
- case ObservabilityEventType.CryptoFinishSuccess:
- case ObservabilityEventType.CryptoFinishError: {
- return <Fragment>
- <dt>operation</dt>
- <dd>{not.event.operation}</dd>
- </Fragment>
- }
- case ObservabilityEventType.RequestStart:
- case ObservabilityEventType.RequestFinishSuccess:
- case ObservabilityEventType.RequestFinishError: {
- return <Fragment />
- }
- }
- })()}
-
- </Fragment>
+ const [showDetails, setShowDetails] = useState<VNode>()
+ const [notif, setnotif] = useState<Notif[]>([])
+ return <div>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ {lastShow === lastEvent ?
+ <div>last event {lastEvent.toString()}</div>
+ : <div>there are more events, update to see them</div>}
+ <div>
+ {lastShow !== lastEvent ?
+ <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => {
+ const list = [...notif]
+ for (const pepe of notifications) {
+ const event = getNotificationFor(String(list.length), pepe.notif, pepe.when, list)
+ if (event) {
+ list.push(event)
}
}
- })()}
+ setnotif(list)
+ setLastShow(lastEvent)
+ }}>
+ update
+ </div>
+ : <div />}
+ <div style={{ padding: 4, margin: 2, border: "solid 1px black" }} onClick={() => {
+ setNotifications([])
+ }}>
+ clear
+ </div>
+ </div>
+ </div>
+ {showDetails && <Modal title="event details" onClose={{ onClick: (async () => { setShowDetails(undefined) }) as any }} >
+ {showDetails}
+ </Modal>}
+ {notif.map((not) => {
+ return (
+ <details key={not.id}>
+ <summary>
+ <div style={{ width: "90%", display: "inline-flex", justifyContent: "space-between", padding: 4 }}>
+ <div style={{ padding: 4 }}>
+ {not.description}
+ </div>
+ <div style={{ padding: 4 }}>
+ <Time
+ timestamp={not.start}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ </div>
+ <div style={{ padding: 4 }}><Time
+ timestamp={not.end}
+ format="yyyy/MM/dd HH:mm:ss"
+ /></div>
+ </div>
+ </summary>
+ <not.MoreInfo events={not.events} onClick={(details) => {
+ setShowDetails(details)
+ }} />
</details>
);
})}
@@ -355,7 +713,7 @@ function ErroDetailModal({ error, onClose }: { error: TalerErrorDetail, onClose:
format="yyyy/MM/dd HH:mm:ss"
/></dd>
</dl>
- <pre>
+ <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
{JSON.stringify(error, undefined, 2)}
</pre>
</Modal>
@@ -371,14 +729,15 @@ export function ActiveTasksTable({ }: {}): VNode {
);
const [showError, setShowError] = useState<TalerErrorDetail>()
const tasks = state && !state.hasError ? state.response.tasks : [];
- useEffect(() => {
- return api.listener.onUpdateNotification(listenAllEvents, (notif) => {
- state?.retry()
- });
- });
+ // useEffect(() => {
+ // return api.listener.onUpdateNotification(listenAllEvents, (notif) => {
+ // state?.retry()
+ // });
+ // });
return <Fragment>
{showError && <ErroDetailModal error={showError} onClose={(async () => { setShowError(undefined) })} />}
- <table>
+
+ <table style={{ width: "100%" }}>
<thead>
<tr>
<th>
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 2501c61c8..89678c74a 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -35,7 +35,6 @@ export const WalletAction = styled.div`
align-items: center;
margin: auto;
- height: 100%;
& h1:first-child {
margin-top: 0;
diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts
index e3afe35bd..faf0f2820 100644
--- a/packages/taler-wallet-webextension/src/platform/api.ts
+++ b/packages/taler-wallet-webextension/src/platform/api.ts
@@ -55,6 +55,7 @@ export interface CrossBrowserPermissionsApi {
export enum ExtensionNotificationType {
SettingsChange = "settings-change",
+ ClearNotifications = "clear-notifications",
}
export interface SettingsChangeNotification {
@@ -62,8 +63,11 @@ export interface SettingsChangeNotification {
currentValue: Settings;
}
+export interface ClearNotificaitonNotification {
+ type: ExtensionNotificationType.ClearNotifications;
+}
-export type ExtensionNotification = SettingsChangeNotification
+export type ExtensionNotification = SettingsChangeNotification | ClearNotificaitonNotification
export type MessageFromBackend = {
type: "wallet",
@@ -300,6 +304,12 @@ export interface ForegroundPlatformAPI {
): Promise<MessageResponse>;
/**
+ * Used by the wallet frontend to send notification about new information
+ * @param message
+ */
+ triggerWalletEvent(message: MessageFromBackend): void;
+
+ /**
* Used from the frontend to receive notifications about new information
* @param listener
* @return function to unsubscribe the listener
diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts
index d88dae460..fc2d1db09 100644
--- a/packages/taler-wallet-webextension/src/platform/chrome.ts
+++ b/packages/taler-wallet-webextension/src/platform/chrome.ts
@@ -45,6 +45,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
findTalerUriInClipboard,
getPermissionsApi,
getWalletWebExVersion,
+ triggerWalletEvent,
listenToWalletBackground,
notifyWhenAppIsReady,
openWalletPage,
@@ -324,6 +325,18 @@ function listenToWalletBackground(listener: (m: any) => void): () => void {
const allPorts: chrome.runtime.Port[] = [];
+
+function triggerWalletEvent(message: MessageFromBackend): void {
+ for (const notif of allPorts) {
+ // const message: MessageFromBackend = { type: msg.type };
+ try {
+ notif.postMessage(message);
+ } catch (e) {
+ logger.error("error posting a message", e);
+ }
+ }
+}
+
function sendMessageToAllChannels(message: MessageFromBackend): void {
for (const notif of allPorts) {
// const message: MessageFromBackend = { type: msg.type };
diff --git a/packages/taler-wallet-webextension/src/platform/dev.ts b/packages/taler-wallet-webextension/src/platform/dev.ts
index 2993c88bc..7cbbe5b25 100644
--- a/packages/taler-wallet-webextension/src/platform/dev.ts
+++ b/packages/taler-wallet-webextension/src/platform/dev.ts
@@ -36,6 +36,7 @@ const api: BackgroundPlatformAPI & ForegroundPlatformAPI = {
findTalerUriInClipboard: async () => undefined,
listenNetworkConnectionState,
openNewURLFromPopup: () => undefined,
+ triggerWalletEvent: () => undefined,
getPermissionsApi: () => ({
containsClipboardPermissions: async () => true,
removeClipboardPermissions: async () => false,
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts
index d25326942..90037819f 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -115,6 +115,9 @@ export function createWalletApiMock(): {
},
}),
listener: {
+ trigger: () => {
+
+ },
onUpdateNotification(
mTypes: NotificationType[],
callback: ((d: WalletNotification) => void) | undefined,
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
index cdd3994d7..c98538755 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -97,7 +97,7 @@ export function DeveloperPage({ }: Props): VNode {
const { safely } = useAlertContext();
const listenAllEvents = Array.from<NotificationType>({ length: 1 });
- listenAllEvents.includes = () => true
+ // listenAllEvents.includes = () => true
const hook = useAsyncAsHook(async () => {
const list = await api.wallet.call(WalletApiOperation.ListExchanges, {});
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
index df99d3f17..a1c09ef8d 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -41,6 +41,7 @@ import {
WalletCoreResponseType,
} from "@gnu-taler/taler-wallet-core";
import {
+ ExtensionNotification,
MessageFromBackend,
MessageFromFrontendBackground,
MessageFromFrontendWallet,
@@ -192,14 +193,23 @@ export type WxApiType = {
wallet: WalletCoreApiClient;
background: BackgroundApiClient;
listener: {
+ trigger: (d: ExtensionNotification) => void;
onUpdateNotification: typeof onUpdateNotification;
};
};
+function trigger(w:ExtensionNotification) {
+ platform.triggerWalletEvent({
+ type: "web-extension",
+ notification: w,
+ })
+}
+
export const wxApi = {
wallet: new WalletApiClientImpl(),
background: new BackgroundApiClientImpl(),
listener: {
+ trigger,
onUpdateNotification,
},
};