taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit b99844a85d37dd724036130d43e3822eaeee4dc4
parent 2bc77f355f74bd98c6ba748c5db1822a243aaa36
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 14 Feb 2025 18:05:05 -0300

fix #9543 part of #9536

Diffstat:
Mpackages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx | 21++++++++-------------
Mpackages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx | 439+++++++++++++++++++++++++++++++++++++++++--------------------------------------
2 files changed, 238 insertions(+), 222 deletions(-)

diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx @@ -28,24 +28,19 @@ import { TranslatedString, WithdrawalExchangeAccountDetails, } from "@gnu-taler/taler-util"; -import { - encodeCrockForURI, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; +import { useBackendContext } from "../context/backend.js"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { Button } from "../mui/Button.js"; +import { SafeHandler } from "../mui/handlers.js"; import { CopiedIcon, CopyIcon } from "../svg/index.js"; import { Amount } from "./Amount.js"; -import { ButtonBox, TooltipLeft, WarningBox } from "./styled/index.js"; -import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { useBackendContext } from "../context/backend.js"; -import { QR } from "./QR.js"; -import { Pages } from "../NavigationBar.js"; -import { ShowQRsForPaytoPopup } from "./ShowQRsForPaytoPopup.js"; -import { SafeHandler } from "../mui/handlers.js"; import { ShowBanksForPaytoPopup } from "./ShowBanksForPaytoPopup.js"; +import { ShowQRsForPaytoPopup } from "./ShowQRsForPaytoPopup.js"; +import { ButtonBox, TooltipLeft, WarningBox } from "./styled/index.js"; export interface BankDetailsProps { subject: string; @@ -219,7 +214,7 @@ function IBANAccountInfoTable({ </i18n.Translate> </td> </tr> - <Row name={i18n.str`Subject`} value={subject} literal /> + <Row name={i18n.str`Subject`} value={`Taler ${subject}`} literal /> <tr> <td colSpan={3}> diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx @@ -18,6 +18,7 @@ import { AbsoluteTime, Amounts, CoinStatus, + ExchangeListItem, ExchangeTosStatus, LogLevel, NotificationType, @@ -69,11 +70,238 @@ type SplitedCoinInfo = { usable: CalculatedCoinfInfo[]; }; +const listenAllEvents = Array.from<NotificationType>({ length: 1 }); + +function ExchangeActions({}: {}): VNode { + const [time, reload] = useState<number>(); + const { i18n } = useTranslationContext(); + const api = useBackendContext(); + const hook = useAsyncAsHook(async () => { + const list = await api.wallet.call(WalletApiOperation.ListExchanges, {}); + return { exchanges: list.exchanges }; + }, [time]); + const exchangeList = hook && !hook.hasError ? hook.response.exchanges : []; + + if (!exchangeList || !exchangeList.length) { + return ( + <div> + <i18n.Translate>No exchange yet</i18n.Translate> + </div> + ); + } + return ( + <table> + <thead> + <tr> + <th> + <i18n.Translate>Currency</i18n.Translate> + </th> + <th> + <i18n.Translate>URL</i18n.Translate> + </th> + <th> + <i18n.Translate>Status</i18n.Translate> + </th> + <th> + <i18n.Translate>Terms of Service</i18n.Translate> + </th> + <th> + <i18n.Translate>Last Update</i18n.Translate> + </th> + <th> + <i18n.Translate>Actions</i18n.Translate> + </th> + </tr> + </thead> + <tbody> + {exchangeList.map((e, idx) => { + function TosStatus(): VNode { + switch (e.tosStatus) { + case ExchangeTosStatus.Accepted: + return ( + <SuccessText> + <i18n.Translate>ok</i18n.Translate> + </SuccessText> + ); + case ExchangeTosStatus.Pending: + return ( + <WarningText> + <i18n.Translate>pending</i18n.Translate> + </WarningText> + ); + case ExchangeTosStatus.Proposed: + return <i18n.Translate>proposed</i18n.Translate>; + default: + return ( + <DestructiveText> + <i18n.Translate> + unknown (exchange status should be updated) + </i18n.Translate> + </DestructiveText> + ); + } + } + const uri = !e.masterPub + ? undefined + : stringifyWithdrawExchange({ + exchangeBaseUrl: e.exchangeBaseUrl, + }); + return ( + <tr key={idx}> + <td> + <a + href={ + !uri + ? undefined + : `#${Pages.defaultCta({ + uri: encodeCrockForURI(uri), + })}` + } + > + {e.scopeInfo + ? `${e.scopeInfo.currency} (${ + e.scopeInfo.type === ScopeType.Global + ? "global" + : "regional" + })` + : e.currency} + </a> + </td> + <td> + <a + href={new URL(`/keys`, e.exchangeBaseUrl).href} + target="_blank" + rel="noreferrer" + > + {e.exchangeBaseUrl} + </a> + </td> + <td> + {e.exchangeEntryStatus} / {e.exchangeUpdateStatus} + </td> + <td> + <TosStatus /> + </td> + <td> + {e.lastUpdateTimestamp + ? AbsoluteTime.toIsoString( + AbsoluteTime.fromPreciseTimestamp(e.lastUpdateTimestamp), + ) + : "never"} + </td> + <td> + <button + onClick={async () => { + await api.wallet.call( + WalletApiOperation.UpdateExchangeEntry, + { + exchangeBaseUrl: e.exchangeBaseUrl, + force: true, + }, + ); + reload(new Date().getTime()); + }} + > + <i18n.Translate>Reload</i18n.Translate> + </button> + <button + onClick={async () => { + await api.wallet.call(WalletApiOperation.DeleteExchange, { + exchangeBaseUrl: e.exchangeBaseUrl, + }); + reload(new Date().getTime()); + }} + > + <i18n.Translate>Delete</i18n.Translate> + </button> + <button + onClick={async () => { + await api.wallet.call(WalletApiOperation.DeleteExchange, { + exchangeBaseUrl: e.exchangeBaseUrl, + purge: true, + }); + reload(new Date().getTime()); + }} + > + <i18n.Translate>Purge</i18n.Translate> + </button> + {e.scopeInfo && e.masterPub && e.currency ? ( + e.scopeInfo.type === ScopeType.Global ? ( + <button + onClick={async () => { + await api.wallet.call( + WalletApiOperation.RemoveGlobalCurrencyExchange, + { + exchangeBaseUrl: e.exchangeBaseUrl, + currency: e.currency!, + exchangeMasterPub: e.masterPub!, + }, + ); + reload(new Date().getTime()); + }} + > + <i18n.Translate>Make regional</i18n.Translate> + </button> + ) : e.scopeInfo.type === ScopeType.Auditor ? undefined : e + .scopeInfo.type === ScopeType.Exchange ? ( + <button + onClick={async () => { + await api.wallet.call( + WalletApiOperation.AddGlobalCurrencyExchange, + { + exchangeBaseUrl: e.exchangeBaseUrl, + currency: e.currency!, + exchangeMasterPub: e.masterPub!, + }, + ); + reload(new Date().getTime()); + }} + > + <i18n.Translate>Make global</i18n.Translate> + </button> + ) : undefined + ) : undefined} + <button + disabled={e.tosStatus !== ExchangeTosStatus.Accepted} + onClick={async () => { + await api.wallet.call( + WalletApiOperation.SetExchangeTosForgotten, + { + exchangeBaseUrl: e.exchangeBaseUrl, + }, + ); + reload(new Date().getTime()); + }} + > + <i18n.Translate>Forget ToS</i18n.Translate> + </button> + </td> + </tr> + ); + })} + </tbody> + </table> + ); +} + export function DeveloperPage(): VNode { const { i18n } = useTranslationContext(); const [downloadedDatabase, setDownloadedDatabase] = useState< { time: Date; content: string } | undefined >(undefined); + const [tagName, setTagName] = useState(""); + const [logLevel, setLogLevel] = useState("info"); + const api = useBackendContext(); + const fileRef = useRef<HTMLInputElement>(null); + const [settings, updateSettings] = useSettings(); + const { safely } = useAlertContext(); + useEffect(() => { + return api.listener.onUpdateNotification(listenAllEvents, (ev) => { + console.log("event", ev); + return hook?.retry(); + }); + }); + async function onExportDatabase(): Promise<void> { const db = await api.wallet.call(WalletApiOperation.ExportDb, {}); const content = JSON.stringify(db); @@ -82,19 +310,12 @@ export function DeveloperPage(): VNode { content, }); } - const api = useBackendContext(); - const fileRef = useRef<HTMLInputElement>(null); async function onImportDatabase(str: string): Promise<void> { await api.wallet.call(WalletApiOperation.ImportDb, { dump: JSON.parse(str), }); } - const [settings, updateSettings] = useSettings(); - const { safely } = useAlertContext(); - - const listenAllEvents = Array.from<NotificationType>({ length: 1 }); - // listenAllEvents.includes = () => true const hook = useAsyncAsHook(async () => { const list = await api.wallet.call(WalletApiOperation.ListExchanges, {}); @@ -105,13 +326,6 @@ export function DeveloperPage(): VNode { const exchangeList = hook && !hook.hasError ? hook.response.exchanges : []; const coins = hook && !hook.hasError ? hook.response.coins.coins : []; - useEffect(() => { - return api.listener.onUpdateNotification(listenAllEvents, (ev) => { - console.log("event", ev); - return hook?.retry(); - }); - }); - const currencies: { [ex: string]: string } = {}; const money_by_exchange = coins.reduce( (prev, cur) => { @@ -138,8 +352,6 @@ export function DeveloperPage(): VNode { }, ); - const [tagName, setTagName] = useState(""); - const [logLevel, setLogLevel] = useState("info"); return ( <div> <p> @@ -275,199 +487,8 @@ export function DeveloperPage(): VNode { <SubTitle> <i18n.Translate>Exchange Entries</i18n.Translate> </SubTitle> - {!exchangeList || !exchangeList.length ? ( - <div> - <i18n.Translate>No exchange yet</i18n.Translate> - </div> - ) : ( - <Fragment> - <table> - <thead> - <tr> - <th> - <i18n.Translate>Currency</i18n.Translate> - </th> - <th> - <i18n.Translate>URL</i18n.Translate> - </th> - <th> - <i18n.Translate>Status</i18n.Translate> - </th> - <th> - <i18n.Translate>Terms of Service</i18n.Translate> - </th> - <th> - <i18n.Translate>Last Update</i18n.Translate> - </th> - <th> - <i18n.Translate>Actions</i18n.Translate> - </th> - </tr> - </thead> - <tbody> - {exchangeList.map((e, idx) => { - function TosStatus(): VNode { - switch (e.tosStatus) { - case ExchangeTosStatus.Accepted: - return ( - <SuccessText> - <i18n.Translate>ok</i18n.Translate> - </SuccessText> - ); - case ExchangeTosStatus.Pending: - return ( - <WarningText> - <i18n.Translate>pending</i18n.Translate> - </WarningText> - ); - case ExchangeTosStatus.Proposed: - return <i18n.Translate>proposed</i18n.Translate>; - default: - return ( - <DestructiveText> - <i18n.Translate> - unknown (exchange status should be updated) - </i18n.Translate> - </DestructiveText> - ); - } - } - const uri = !e.masterPub - ? undefined - : stringifyWithdrawExchange({ - exchangeBaseUrl: e.exchangeBaseUrl, - }); - return ( - <tr key={idx}> - <td> - <a - href={ - !uri - ? undefined - : `#${Pages.defaultCta({ uri: encodeCrockForURI(uri) })}` - } - > - {e.scopeInfo - ? `${e.scopeInfo.currency} (${ - e.scopeInfo.type === ScopeType.Global - ? "global" - : "regional" - })` - : e.currency} - </a> - </td> - <td> - <a - href={new URL(`/keys`, e.exchangeBaseUrl).href} - target="_blank" - rel="noreferrer" - > - {e.exchangeBaseUrl} - </a> - </td> - <td> - {e.exchangeEntryStatus} / {e.exchangeUpdateStatus} - </td> - <td> - <TosStatus /> - </td> - <td> - {e.lastUpdateTimestamp - ? AbsoluteTime.toIsoString( - AbsoluteTime.fromPreciseTimestamp( - e.lastUpdateTimestamp, - ), - ) - : "never"} - </td> - <td> - <button - onClick={() => { - api.wallet.call( - WalletApiOperation.UpdateExchangeEntry, - { - exchangeBaseUrl: e.exchangeBaseUrl, - force: true, - }, - ); - }} - > - Reload - </button> - <button - onClick={() => { - api.wallet.call(WalletApiOperation.DeleteExchange, { - exchangeBaseUrl: e.exchangeBaseUrl, - }); - }} - > - Delete - </button> - <button - onClick={() => { - api.wallet.call(WalletApiOperation.DeleteExchange, { - exchangeBaseUrl: e.exchangeBaseUrl, - purge: true, - }); - }} - > - Purge - </button> - {e.scopeInfo && e.masterPub && e.currency ? ( - e.scopeInfo.type === ScopeType.Global ? ( - <button - onClick={() => { - api.wallet.call( - WalletApiOperation.RemoveGlobalCurrencyExchange, - { - exchangeBaseUrl: e.exchangeBaseUrl, - currency: e.currency!, - exchangeMasterPub: e.masterPub!, - }, - ); - }} - > - Make regional - </button> - ) : e.scopeInfo.type === - ScopeType.Auditor ? undefined : e.scopeInfo.type === - ScopeType.Exchange ? ( - <button - onClick={() => { - api.wallet.call( - WalletApiOperation.AddGlobalCurrencyExchange, - { - exchangeBaseUrl: e.exchangeBaseUrl, - currency: e.currency!, - exchangeMasterPub: e.masterPub!, - }, - ); - }} - > - Make global - </button> - ) : undefined - ) : undefined} - <button - onClick={() => { - api.wallet.call( - WalletApiOperation.SetExchangeTosForgotten, - { - exchangeBaseUrl: e.exchangeBaseUrl, - }, - ); - }} - > - Forget ToS - </button> - </td> - </tr> - ); - })} - </tbody> - </table> - </Fragment> - )} + <ExchangeActions /> + <div style={{ display: "flex", justifyContent: "space-between" }}> <div /> <LinkPrimary href={`#${Pages.settingsExchangeAdd({})}`}>