aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-03-28 14:03:59 -0300
committerSebastian <sebasjm@gmail.com>2022-03-28 14:04:06 -0300
commit1d85662946c393ea6985c178842be40d38fc687d (patch)
treec01293631a6fc13810458ea4638bb3f8daa4c389 /packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
parent2ba6692c6ad97285e852fb0b74a650e067817003 (diff)
downloadwallet-core-1d85662946c393ea6985c178842be40d38fc687d.tar.xz
storybook like page for dev, plus some style fixes
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx384
1 files changed, 384 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
new file mode 100644
index 000000000..f4d717df6
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -0,0 +1,384 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ Amounts,
+ CoinDumpJson,
+ ExchangeListItem,
+ NotificationType,
+} from "@gnu-taler/taler-util";
+import { PendingTaskInfo } from "@gnu-taler/taler-wallet-core";
+import { format } from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { useRef, useState } from "preact/hooks";
+import { Diagnostics } from "../components/Diagnostics";
+import { NotifyUpdateFadeOut } from "../components/styled";
+import { Time } from "../components/Time";
+import { useTranslationContext } from "../context/translation";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
+import { useDiagnostics } from "../hooks/useDiagnostics";
+import * as wxApi from "../wxApi";
+
+export function DeveloperPage(): VNode {
+ const [status, timedOut] = useDiagnostics();
+
+ const listenAllEvents = Array.from<NotificationType>({ length: 1 });
+ listenAllEvents.includes = () => true; // includes every event
+
+ const response = useAsyncAsHook(async () => {
+ const op = await wxApi.getPendingOperations();
+ const c = await wxApi.dumpCoins();
+ const ex = await wxApi.listExchanges();
+ return {
+ operations: op.pendingOperations,
+ coins: c.coins,
+ exchanges: ex.exchanges,
+ };
+ }, listenAllEvents);
+
+ const nonResponse = { operations: [], coins: [], exchanges: [] };
+ const { operations, coins, exchanges } =
+ response === undefined
+ ? nonResponse
+ : response.hasError
+ ? nonResponse
+ : response.response;
+
+ const balanceResponse = useAsyncAsHook(wxApi.getBalance);
+
+ return (
+ <View
+ status={status}
+ timedOut={timedOut}
+ operations={operations}
+ coins={coins}
+ exchanges={exchanges}
+ onDownloadDatabase={async () => {
+ const db = await wxApi.exportDB();
+ return JSON.stringify(db);
+ }}
+ />
+ );
+}
+
+type CoinsInfo = CoinDumpJson["coins"];
+type CalculatedCoinfInfo = {
+ denom_value: number;
+ remain_value: number;
+ status: string;
+ from_refresh: boolean;
+ id: string;
+};
+
+type SplitedCoinInfo = {
+ spent: CalculatedCoinfInfo[];
+ usable: CalculatedCoinfInfo[];
+};
+
+export interface Props {
+ status: any;
+ timedOut: boolean;
+ operations: PendingTaskInfo[];
+ coins: CoinsInfo;
+ exchanges: ExchangeListItem[];
+ onDownloadDatabase: () => Promise<string>;
+}
+
+function hashObjectId(o: any): string {
+ return JSON.stringify(o);
+}
+
+export function View({
+ status,
+ timedOut,
+ operations,
+ coins,
+ onDownloadDatabase,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+ const [downloadedDatabase, setDownloadedDatabase] = useState<
+ { time: Date; content: string } | undefined
+ >(undefined);
+ async function onExportDatabase(): Promise<void> {
+ const content = await onDownloadDatabase();
+ setDownloadedDatabase({
+ time: new Date(),
+ content,
+ });
+ }
+ const fileRef = useRef<HTMLInputElement>(null);
+ async function onImportDatabase(str: string): Promise<void> {
+ return wxApi.importDB(JSON.parse(str));
+ }
+ const currencies: { [ex: string]: string } = {};
+ const money_by_exchange = coins.reduce(
+ (prev, cur) => {
+ const denom = Amounts.parseOrThrow(cur.denom_value);
+ if (!prev[cur.exchange_base_url]) {
+ prev[cur.exchange_base_url] = [];
+ currencies[cur.exchange_base_url] = denom.currency;
+ }
+ prev[cur.exchange_base_url].push({
+ denom_value: parseFloat(Amounts.stringifyValue(denom)),
+ remain_value: parseFloat(
+ Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)),
+ ),
+ status: cur.coin_suspended ? "suspended" : "ok",
+ from_refresh: cur.refresh_parent_coin_pub !== undefined,
+ id: cur.coin_pub,
+ });
+ return prev;
+ },
+ {} as {
+ [exchange_name: string]: CalculatedCoinfInfo[];
+ },
+ );
+
+ return (
+ <div>
+ <p>
+ <i18n.Translate>Debug tools</i18n.Translate>:
+ </p>
+ <button
+ onClick={() =>
+ confirmReset(
+ i18n.str`Do you want to IRREVOCABLY DESTROY everything inside your wallet and LOSE ALL YOUR COINS?`,
+ )
+ }
+ >
+ <i18n.Translate>reset</i18n.Translate>
+ </button>
+ <br />
+ <button onClick={() => fileRef?.current?.click()}>
+ <i18n.Translate>import database</i18n.Translate>
+ </button>
+ <input
+ ref={fileRef}
+ style={{ display: "none" }}
+ type="file"
+ onChange={async (e) => {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return Promise.reject();
+ }
+ const buf = await f[0].arrayBuffer();
+ const str = new Uint8Array(buf).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ );
+ return onImportDatabase(str);
+ }}
+ />
+ <br />
+ <button onClick={onExportDatabase}>
+ <i18n.Translate>export database</i18n.Translate>
+ </button>
+ {downloadedDatabase && (
+ <div>
+ <i18n.Translate>
+ Database exported at
+ <Time
+ timestamp={{ t_ms: downloadedDatabase.time.getTime() }}
+ format="yyyy/MM/dd HH:mm:ss"
+ />
+ <a
+ href={`data:text/plain;charset=utf-8;base64,${toBase64(
+ downloadedDatabase.content,
+ )}`}
+ download={`taler-wallet-database-${format(
+ downloadedDatabase.time,
+ "yyyy/MM/dd_HH:mm",
+ )}.json`}
+ >
+ <i18n.Translate>click here</i18n.Translate>
+ </a>
+ to download
+ </i18n.Translate>
+ </div>
+ )}
+ <br />
+ <p>
+ <i18n.Translate>Coins</i18n.Translate>:
+ </p>
+ {Object.keys(money_by_exchange).map((ex) => {
+ const allcoins = money_by_exchange[ex];
+ allcoins.sort((a, b) => {
+ return b.denom_value - a.denom_value;
+ });
+
+ const coins = allcoins.reduce(
+ (prev, cur) => {
+ if (cur.remain_value > 0) prev.usable.push(cur);
+ if (cur.remain_value === 0) prev.spent.push(cur);
+ return prev;
+ },
+ {
+ spent: [],
+ usable: [],
+ } as SplitedCoinInfo,
+ );
+
+ return <ShowAllCoins coins={coins} ex={ex} currencies={currencies} />;
+ })}
+ <br />
+ <Diagnostics diagnostics={status} timedOut={timedOut} />
+ {operations && operations.length > 0 && (
+ <Fragment>
+ <p>
+ <i18n.Translate>Pending operations</i18n.Translate>
+ </p>
+ <dl>
+ {operations.reverse().map((o) => {
+ return (
+ <NotifyUpdateFadeOut key={hashObjectId(o)}>
+ <dt>
+ {o.type}{" "}
+ <Time
+ timestamp={o.timestampDue}
+ format="yy/MM/dd hh:mm:ss"
+ />
+ </dt>
+ <dd>
+ <pre>{JSON.stringify(o, undefined, 2)}</pre>
+ </dd>
+ </NotifyUpdateFadeOut>
+ );
+ })}
+ </dl>
+ </Fragment>
+ )}
+ </div>
+ );
+}
+
+function ShowAllCoins({
+ ex,
+ coins,
+ currencies,
+}: {
+ ex: string;
+ coins: SplitedCoinInfo;
+ currencies: { [ex: string]: string };
+}) {
+ const { i18n } = useTranslationContext();
+ const [collapsedSpent, setCollapsedSpent] = useState(true);
+ const [collapsedUnspent, setCollapsedUnspent] = useState(false);
+ const total = coins.usable.reduce((prev, cur) => prev + cur.denom_value, 0);
+ return (
+ <Fragment>
+ <p>
+ <b>{ex}</b>: {total} {currencies[ex]}
+ </p>
+ <p>
+ <b>
+ <i18n.Translate>usable coins</i18n.Translate>
+ </b>
+ </p>
+ {collapsedUnspent ? (
+ <div onClick={() => setCollapsedUnspent(false)}>click to show</div>
+ ) : (
+ <table onClick={() => setCollapsedUnspent(true)}>
+ <tr>
+ <td>
+ <i18n.Translate>id</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>denom</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>value</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>status</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>from refresh?</i18n.Translate>
+ </td>
+ </tr>
+ {coins.usable.map((c) => {
+ return (
+ <tr>
+ <td>{c.id.substring(0, 5)}</td>
+ <td>{c.denom_value}</td>
+ <td>{c.remain_value}</td>
+ <td>{c.status}</td>
+ <td>{c.from_refresh ? "true" : "false"}</td>
+ </tr>
+ );
+ })}
+ </table>
+ )}
+ <p>
+ <i18n.Translate>spent coins</i18n.Translate>
+ </p>
+ {collapsedSpent ? (
+ <div onClick={() => setCollapsedSpent(false)}>
+ <i18n.Translate>click to show</i18n.Translate>
+ </div>
+ ) : (
+ <table onClick={() => setCollapsedSpent(true)}>
+ <tr>
+ <td>
+ <i18n.Translate>id</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>denom</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>value</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>status</i18n.Translate>
+ </td>
+ <td>
+ <i18n.Translate>from refresh?</i18n.Translate>
+ </td>
+ </tr>
+ {coins.spent.map((c) => {
+ return (
+ <tr>
+ <td>{c.id.substring(0, 5)}</td>
+ <td>{c.denom_value}</td>
+ <td>{c.remain_value}</td>
+ <td>{c.status}</td>
+ <td>{c.from_refresh ? "true" : "false"}</td>
+ </tr>
+ );
+ })}
+ </table>
+ )}
+ </Fragment>
+ );
+}
+
+function toBase64(str: string): string {
+ return btoa(
+ encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
+ return String.fromCharCode(parseInt(p1, 16));
+ }),
+ );
+}
+
+function runIntegrationTest() {}
+
+export async function confirmReset(
+ confirmTheResetMessage: string,
+): Promise<void> {
+ if (confirm(confirmTheResetMessage)) {
+ await wxApi.resetDb();
+ window.close();
+ }
+}