welcome page with error diagnostics / react refactoring

This commit is contained in:
Florian Dold 2019-09-05 16:10:53 +02:00
parent fab4e33896
commit 8144b0f553
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
27 changed files with 535 additions and 907 deletions

View File

@ -86,3 +86,7 @@ install: tsc
npm install -g --prefix $(prefix) .
endif
.PHONY: watch
watch: tsconfig.json
./node_modules/.bin/webpack --watch

View File

@ -17,7 +17,7 @@ export function openTalerDb(
const req = idbFactory.open(DB_NAME, WALLET_DB_VERSION);
req.onerror = e => {
console.log("taler database error", e);
reject(e);
reject(new Error("database error"));
};
req.onsuccess = e => {
req.result.onversionchange = (evt: IDBVersionChangeEvent) => {

View File

@ -43,7 +43,7 @@ import { Index, Store } from "./query";
* In the future we might consider adding migration functions for
* each version increment.
*/
export const WALLET_DB_VERSION = 26;
export const WALLET_DB_VERSION = 27;
/**
* A reserve record as stored in the wallet's database.

View File

@ -250,42 +250,42 @@ msgstr ""
msgid "Cancel withdraw operation"
msgstr ""
#: src/webex/renderHtml.tsx:225
#: src/webex/renderHtml.tsx:226
#, fuzzy, c-format
msgid "Withdrawal fees:"
msgstr "Abheben bei"
#: src/webex/renderHtml.tsx:226
#: src/webex/renderHtml.tsx:227
#, c-format
msgid "Rounding loss:"
msgstr ""
#: src/webex/renderHtml.tsx:227
#: src/webex/renderHtml.tsx:228
#, c-format
msgid "Earliest expiration (for deposit): %1$s"
msgstr ""
#: src/webex/renderHtml.tsx:233
#: src/webex/renderHtml.tsx:234
#, c-format
msgid "# Coins"
msgstr ""
#: src/webex/renderHtml.tsx:234
#: src/webex/renderHtml.tsx:235
#, c-format
msgid "Value"
msgstr ""
#: src/webex/renderHtml.tsx:235
#: src/webex/renderHtml.tsx:236
#, fuzzy, c-format
msgid "Withdraw Fee"
msgstr "Abheben bei %1$s"
#: src/webex/renderHtml.tsx:236
#: src/webex/renderHtml.tsx:237
#, c-format
msgid "Refresh Fee"
msgstr ""
#: src/webex/renderHtml.tsx:237
#: src/webex/renderHtml.tsx:238
#, c-format
msgid "Deposit Fee"
msgstr ""

View File

@ -241,42 +241,42 @@ msgstr ""
msgid "Cancel withdraw operation"
msgstr ""
#: src/webex/renderHtml.tsx:225
#: src/webex/renderHtml.tsx:226
#, c-format
msgid "Withdrawal fees:"
msgstr ""
#: src/webex/renderHtml.tsx:226
#: src/webex/renderHtml.tsx:227
#, c-format
msgid "Rounding loss:"
msgstr ""
#: src/webex/renderHtml.tsx:227
#: src/webex/renderHtml.tsx:228
#, c-format
msgid "Earliest expiration (for deposit): %1$s"
msgstr ""
#: src/webex/renderHtml.tsx:233
#: src/webex/renderHtml.tsx:234
#, c-format
msgid "# Coins"
msgstr ""
#: src/webex/renderHtml.tsx:234
#: src/webex/renderHtml.tsx:235
#, c-format
msgid "Value"
msgstr ""
#: src/webex/renderHtml.tsx:235
#: src/webex/renderHtml.tsx:236
#, c-format
msgid "Withdraw Fee"
msgstr ""
#: src/webex/renderHtml.tsx:236
#: src/webex/renderHtml.tsx:237
#, c-format
msgid "Refresh Fee"
msgstr ""
#: src/webex/renderHtml.tsx:237
#: src/webex/renderHtml.tsx:238
#, c-format
msgid "Deposit Fee"
msgstr ""

View File

@ -241,42 +241,42 @@ msgstr ""
msgid "Cancel withdraw operation"
msgstr ""
#: src/webex/renderHtml.tsx:225
#: src/webex/renderHtml.tsx:226
#, c-format
msgid "Withdrawal fees:"
msgstr ""
#: src/webex/renderHtml.tsx:226
#: src/webex/renderHtml.tsx:227
#, c-format
msgid "Rounding loss:"
msgstr ""
#: src/webex/renderHtml.tsx:227
#: src/webex/renderHtml.tsx:228
#, c-format
msgid "Earliest expiration (for deposit): %1$s"
msgstr ""
#: src/webex/renderHtml.tsx:233
#: src/webex/renderHtml.tsx:234
#, c-format
msgid "# Coins"
msgstr ""
#: src/webex/renderHtml.tsx:234
#: src/webex/renderHtml.tsx:235
#, c-format
msgid "Value"
msgstr ""
#: src/webex/renderHtml.tsx:235
#: src/webex/renderHtml.tsx:236
#, c-format
msgid "Withdraw Fee"
msgstr ""
#: src/webex/renderHtml.tsx:236
#: src/webex/renderHtml.tsx:237
#, c-format
msgid "Refresh Fee"
msgstr ""
#: src/webex/renderHtml.tsx:237
#: src/webex/renderHtml.tsx:238
#, c-format
msgid "Deposit Fee"
msgstr ""

View File

@ -241,42 +241,42 @@ msgstr ""
msgid "Cancel withdraw operation"
msgstr ""
#: src/webex/renderHtml.tsx:225
#: src/webex/renderHtml.tsx:226
#, c-format
msgid "Withdrawal fees:"
msgstr ""
#: src/webex/renderHtml.tsx:226
#: src/webex/renderHtml.tsx:227
#, c-format
msgid "Rounding loss:"
msgstr ""
#: src/webex/renderHtml.tsx:227
#: src/webex/renderHtml.tsx:228
#, c-format
msgid "Earliest expiration (for deposit): %1$s"
msgstr ""
#: src/webex/renderHtml.tsx:233
#: src/webex/renderHtml.tsx:234
#, c-format
msgid "# Coins"
msgstr ""
#: src/webex/renderHtml.tsx:234
#: src/webex/renderHtml.tsx:235
#, c-format
msgid "Value"
msgstr ""
#: src/webex/renderHtml.tsx:235
#: src/webex/renderHtml.tsx:236
#, c-format
msgid "Withdraw Fee"
msgstr ""
#: src/webex/renderHtml.tsx:236
#: src/webex/renderHtml.tsx:237
#, c-format
msgid "Refresh Fee"
msgstr ""
#: src/webex/renderHtml.tsx:237
#: src/webex/renderHtml.tsx:238
#, c-format
msgid "Deposit Fee"
msgstr ""

View File

@ -245,42 +245,42 @@ msgstr "Acceptera avgifter och utbetala"
msgid "Cancel withdraw operation"
msgstr ""
#: src/webex/renderHtml.tsx:225
#: src/webex/renderHtml.tsx:226
#, c-format
msgid "Withdrawal fees:"
msgstr "Utbetalnings avgifter:"
#: src/webex/renderHtml.tsx:226
#: src/webex/renderHtml.tsx:227
#, c-format
msgid "Rounding loss:"
msgstr ""
#: src/webex/renderHtml.tsx:227
#: src/webex/renderHtml.tsx:228
#, c-format
msgid "Earliest expiration (for deposit): %1$s"
msgstr ""
#: src/webex/renderHtml.tsx:233
#: src/webex/renderHtml.tsx:234
#, c-format
msgid "# Coins"
msgstr "# Mynt"
#: src/webex/renderHtml.tsx:234
#: src/webex/renderHtml.tsx:235
#, c-format
msgid "Value"
msgstr "Värde"
#: src/webex/renderHtml.tsx:235
#: src/webex/renderHtml.tsx:236
#, c-format
msgid "Withdraw Fee"
msgstr "Utbetalnings avgift"
#: src/webex/renderHtml.tsx:236
#: src/webex/renderHtml.tsx:237
#, c-format
msgid "Refresh Fee"
msgstr "Återhämtnings avgift"
#: src/webex/renderHtml.tsx:237
#: src/webex/renderHtml.tsx:238
#, c-format
msgid "Deposit Fee"
msgstr "Depostitions avgift"

View File

@ -241,42 +241,42 @@ msgstr ""
msgid "Cancel withdraw operation"
msgstr ""
#: src/webex/renderHtml.tsx:225
#: src/webex/renderHtml.tsx:226
#, c-format
msgid "Withdrawal fees:"
msgstr ""
#: src/webex/renderHtml.tsx:226
#: src/webex/renderHtml.tsx:227
#, c-format
msgid "Rounding loss:"
msgstr ""
#: src/webex/renderHtml.tsx:227
#: src/webex/renderHtml.tsx:228
#, c-format
msgid "Earliest expiration (for deposit): %1$s"
msgstr ""
#: src/webex/renderHtml.tsx:233
#: src/webex/renderHtml.tsx:234
#, c-format
msgid "# Coins"
msgstr ""
#: src/webex/renderHtml.tsx:234
#: src/webex/renderHtml.tsx:235
#, c-format
msgid "Value"
msgstr ""
#: src/webex/renderHtml.tsx:235
#: src/webex/renderHtml.tsx:236
#, c-format
msgid "Withdraw Fee"
msgstr ""
#: src/webex/renderHtml.tsx:236
#: src/webex/renderHtml.tsx:237
#, c-format
msgid "Refresh Fee"
msgstr ""
#: src/webex/renderHtml.tsx:237
#: src/webex/renderHtml.tsx:238
#, c-format
msgid "Deposit Fee"
msgstr ""

View File

@ -507,8 +507,16 @@ export interface AcceptWithdrawalResponse {
* Details about a purchase, including refund status.
*/
export interface PurchaseDetails {
contractTerms: ContractTerms,
hasRefund: boolean,
totalRefundAmount: AmountJson,
totalRefundAndRefreshFees: AmountJson,
}
contractTerms: ContractTerms;
hasRefund: boolean;
totalRefundAmount: AmountJson;
totalRefundAndRefreshFees: AmountJson;
}
export interface WalletDiagnostics {
walletManifestVersion: string;
walletManifestDisplayVersion: string;
errors: string[];
firefoxIdbProblem: boolean;
dbOutdated: boolean;
}

View File

@ -1,63 +0,0 @@
/*
This file is part of TALER
(C) 2016 Inria
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/>
*/
/**
* General helper React components.
*/
/**
* Imports.
*/
import * as React from "react";
/**
* Wrapper around state that will cause updates to the
* containing component.
*/
export interface StateHolder<T> {
(): T;
(newState: T): void;
}
/**
* Component that doesn't hold its state in one object,
* but has multiple state holders.
*/
export abstract class ImplicitStateComponent<PropType> extends React.Component<PropType, any> {
private _implicit = {needsUpdate: false, didMount: false};
componentDidMount() {
this._implicit.didMount = true;
if (this._implicit.needsUpdate) {
this.setState({} as any);
}
}
makeState<StateType>(initial: StateType): StateHolder<StateType> {
let state: StateType = initial;
return (s?: StateType): StateType => {
if (s !== undefined) {
state = s;
if (this._implicit.didMount) {
this.setState({} as any);
} else {
this._implicit.needsUpdate = true;
}
}
return state;
};
}
}

View File

@ -205,6 +205,11 @@ export interface MessageMap {
request: { talerPayUri: string };
response: walletTypes.PreparePayResult;
};
"get-diagnostics": {
request: { };
response: walletTypes.WalletDiagnostics;
};
}

View File

@ -20,20 +20,11 @@
* @author Florian Dold
*/
import {
CurrencyRecord,
} from "../../dbTypes";
import { ImplicitStateComponent, StateHolder } from "../components";
import {
getCurrencies,
updateCurrency,
} from "../wxApi";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { CurrencyRecord } from "../../dbTypes";
import { getCurrencies, updateCurrency } from "../wxApi";
import React, { useState } from "react";
import URI = require("urijs");
import { registerMountPage } from "../renderHtml";
interface ConfirmAuditorProps {
url: string;
@ -42,36 +33,39 @@ interface ConfirmAuditorProps {
expirationStamp: number;
}
class ConfirmAuditor extends ImplicitStateComponent<ConfirmAuditorProps> {
private addDone: StateHolder<boolean> = this.makeState(false);
constructor(props: ConfirmAuditorProps) {
super(props);
}
function ConfirmAuditor(props: ConfirmAuditorProps) {
const [addDone, setAddDone] = useState(false);
async add() {
const add = async() => {
const currencies = await getCurrencies();
let currency: CurrencyRecord|undefined;
let currency: CurrencyRecord | undefined;
for (const c of currencies) {
if (c.name === this.props.currency) {
if (c.name === props.currency) {
currency = c;
}
}
if (!currency) {
currency = { name: this.props.currency, auditors: [], fractionalDigits: 2, exchanges: [] };
currency = {
name: props.currency,
auditors: [],
fractionalDigits: 2,
exchanges: [],
};
}
const newAuditor = {
auditorPub: this.props.auditorPub,
baseUrl: this.props.url,
expirationStamp: this.props.expirationStamp,
auditorPub: props.auditorPub,
baseUrl: props.url,
expirationStamp: props.expirationStamp,
};
let auditorFound = false;
for (const idx in currency.auditors) {
const a = currency.auditors[idx];
if (a.baseUrl === this.props.url) {
if (a.baseUrl === props.url) {
auditorFound = true;
// Update auditor if already found by URL.
currency.auditors[idx] = newAuditor;
@ -84,47 +78,54 @@ class ConfirmAuditor extends ImplicitStateComponent<ConfirmAuditorProps> {
await updateCurrency(currency);
this.addDone(true);
setAddDone(true);
}
back() {
const back = () => {
window.history.back();
}
};
render(): JSX.Element {
return (
<div id="main">
<p>Do you want to let <strong>{this.props.auditorPub}</strong> audit the currency "{this.props.currency}"?</p>
{this.addDone() ?
(
<div>
Auditor was added! You can also{" "}
<a href={chrome.extension.getURL("/src/webex/pages/auditors.html")}>view and edit</a>{" "}
auditors.
</div>
)
:
(
<div>
<button onClick={() => this.add()} className="pure-button pure-button-primary">Yes</button>
<button onClick={() => this.back()} className="pure-button">No</button>
</div>
)
}
</div>
);
}
return (
<div id="main">
<p>
Do you want to let <strong>{props.auditorPub}</strong> audit the
currency "{props.currency}"?
</p>
{addDone ? (
<div>
Auditor was added! You can also{" "}
<a href={chrome.extension.getURL("/src/webex/pages/auditors.html")}>
view and edit
</a>{" "}
auditors.
</div>
) : (
<div>
<button
onClick={() => add()}
className="pure-button pure-button-primary"
>
Yes
</button>
<button onClick={() => back()} className="pure-button">
No
</button>
</div>
)}
</div>
);
}
function main() {
registerMountPage(() => {
const walletPageUrl = new URI(document.location.href);
const query: any = JSON.parse((URI.parseQuery(walletPageUrl.query()) as any).req);
const query: any = JSON.parse(
(URI.parseQuery(walletPageUrl.query()) as any).req,
);
const url = query.url;
const currency: string = query.currency;
const auditorPub: string = query.auditorPub;
const expirationStamp = Number.parseInt(query.expirationStamp);
const args = { url, currency, auditorPub, expirationStamp };
ReactDOM.render(<ConfirmAuditor {...args} />, document.getElementById("container")!);
}
document.addEventListener("DOMContentLoaded", main);
return <ConfirmAuditor {...args}/>;
});

View File

@ -1,30 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GNU Taler Help - Empty Wallet</title>
<link rel="icon" href="/img/icon.png">
<meta name="description" content="">
<link rel="stylesheet" type="text/css" href="/src/style/wallet.css">
</head>
<body>
<div class="container" id="main">
<div class="row">
<div class="col-lg-12">
<h2 lang="en">Your wallet is empty!</h2>
<p lang="en">You have succeeded with installing the Taler wallet. However, before
you can buy articles using the Taler wallet, you must withdraw electronic coins.
This is typically done by visiting your bank's online banking Web site. There,
you instruct your bank to transfer the funds to a Taler exchange operator. In
return, your wallet will be allowed to withdraw electronic coins.</p>
<p lang="en">At this stage, we are not aware of any regular exchange operators issuing
coins in well-known currencies. However, to see how Taler would work, you
can visit our "fake" bank at
<a href="https://bank.demo.taler.net/">bank.demo.taler.net</a> to
withdraw coins in the "KUDOS" currency that we created just for
demonstrating the system.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -20,73 +20,54 @@
* @author Florian Dold
*/
/**
* Imports.
*/
import {
ReserveRecord,
} from "../../dbTypes";
import { ImplicitStateComponent, StateHolder } from "../components";
import { renderAmount } from "../renderHtml";
import {
getPaybackReserves,
withdrawPaybackReserve,
} from "../wxApi";
import { ReserveRecord } from "../../dbTypes";
import { renderAmount, registerMountPage } from "../renderHtml";
import { getPaybackReserves, withdrawPaybackReserve } from "../wxApi";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { useState } from "react";
function Payback() {
const [reserves, setReserves] = useState<ReserveRecord[] | null>(null);
useState(() => {
const update = async () => {
const r = await getPaybackReserves();
setReserves(r);
};
class Payback extends ImplicitStateComponent<{}> {
private reserves: StateHolder<ReserveRecord[]|null> = this.makeState(null);
constructor(props: {}) {
super(props);
const port = chrome.runtime.connect();
port.onMessage.addListener((msg: any) => {
if (msg.notify) {
console.log("got notified");
this.update();
update();
}
});
this.update();
}
});
async update() {
const reserves = await getPaybackReserves();
this.reserves(reserves);
if (!reserves) {
return <span>loading ...</span>;
}
withdrawPayback(pub: string) {
withdrawPaybackReserve(pub);
}
render(): JSX.Element {
const reserves = this.reserves();
if (!reserves) {
return <span>loading ...</span>;
}
if (reserves.length === 0) {
return <span>No reserves with payback available.</span>;
}
return (
<div>
{reserves.map((r) => (
<div>
<h2>Reserve for ${renderAmount(r.current_amount!)}</h2>
<ul>
<li>Exchange: ${r.exchange_base_url}</li>
</ul>
<button onClick={() => this.withdrawPayback(r.reserve_pub)}>Withdraw again</button>
</div>
))}
</div>
);
if (reserves.length === 0) {
return <span>No reserves with payback available.</span>;
}
return (
<div>
{reserves.map(r => (
<div>
<h2>Reserve for ${renderAmount(r.current_amount!)}</h2>
<ul>
<li>Exchange: ${r.exchange_base_url}</li>
</ul>
<button onClick={() => withdrawPaybackReserve(r.reserve_pub)}>
Withdraw again
</button>
</div>
))}
</div>
);
}
function main() {
ReactDOM.render(<Payback />, document.getElementById("container")!);
}
document.addEventListener("DOMContentLoaded", main);
registerMountPage(() => <Payback />);

View File

@ -14,7 +14,6 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Popup shown to the user when they click
* the Taler browser action button.
@ -38,7 +37,7 @@ import {
WalletBalanceEntry,
} from "../../walletTypes";
import { abbrev, renderAmount } from "../renderHtml";
import { abbrev, renderAmount, PageLink } from "../renderHtml";
import * as wxApi from "../wxApi";
import * as React from "react";
@ -47,7 +46,7 @@ import * as ReactDOM from "react-dom";
import URI = require("urijs");
function onUpdateNotification(f: () => void): () => void {
const port = chrome.runtime.connect({name: "notifications"});
const port = chrome.runtime.connect({ name: "notifications" });
const listener = () => {
f();
};
@ -57,7 +56,6 @@ function onUpdateNotification(f: () => void): () => void {
};
}
class Router extends React.Component<any, any> {
static setRoute(s: string): void {
window.location.hash = s;
@ -92,13 +90,12 @@ class Router extends React.Component<any, any> {
console.log("router unmounted");
}
render(): JSX.Element {
const route = window.location.hash.substring(1);
console.log("rendering route", route);
let defaultChild: React.ReactChild|null = null;
let foundChild: React.ReactChild|null = null;
React.Children.forEach(this.props.children, (child) => {
let defaultChild: React.ReactChild | null = null;
let foundChild: React.ReactChild | null = null;
React.Children.forEach(this.props.children, child => {
const childProps: any = (child as any).props;
if (!childProps) {
return;
@ -119,7 +116,6 @@ class Router extends React.Component<any, any> {
}
}
interface TabProps {
target: string;
children?: React.ReactNode;
@ -141,7 +137,6 @@ function Tab(props: TabProps) {
);
}
class WalletNavBar extends React.Component<any, any> {
private cancelSubscription: any;
@ -161,20 +156,14 @@ class WalletNavBar extends React.Component<any, any> {
console.log("rendering nav bar");
return (
<div className="nav" id="header">
<Tab target="/balance">
{i18n.str`Balance`}
</Tab>
<Tab target="/history">
{i18n.str`History`}
</Tab>
<Tab target="/debug">
{i18n.str`Debug`}
</Tab>
</div>);
<Tab target="/balance">{i18n.str`Balance`}</Tab>
<Tab target="/history">{i18n.str`History`}</Tab>
<Tab target="/debug">{i18n.str`Debug`}</Tab>
</div>
);
}
}
function ExtensionLink(props: any) {
const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
chrome.tabs.create({
@ -189,7 +178,6 @@ function ExtensionLink(props: any) {
);
}
/**
* Render an amount as a large number with a small currency symbol.
*/
@ -197,10 +185,21 @@ function bigAmount(amount: AmountJson): JSX.Element {
const v = amount.value + amount.fraction / Amounts.fractionalBase;
return (
<span>
<span style={{fontSize: "300%"}}>{v}</span>
{" "}
<span style={{ fontSize: "300%" }}>{v}</span>{" "}
<span>{amount.currency}</span>
</span>
</span>
);
}
function EmptyBalanceView() {
return (
<div>
<i18n.Translate wrap="p">
You have no balance to show. Need some{" "}
<PageLink pageName="welcome.html">help</PageLink> getting
started?
</i18n.Translate>
</div>
);
}
@ -245,57 +244,44 @@ class WalletBalanceView extends React.Component<any, any> {
this.setState({});
}
renderEmpty(): JSX.Element {
const helpLink = (
<ExtensionLink target="/src/webex/pages/help/empty-wallet.html">
{i18n.str`help`}
</ExtensionLink>
);
return (
<div>
<i18n.Translate wrap="p">
You have no balance to show. Need some
{" "}<span>{helpLink}</span>{" "}
getting started?
</i18n.Translate>
</div>
);
}
formatPending(entry: WalletBalanceEntry): JSX.Element {
let incoming: JSX.Element | undefined;
let payment: JSX.Element | undefined;
console.log("available: ", entry.pendingIncoming ? renderAmount(entry.available) : null);
console.log("incoming: ", entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null);
console.log(
"available: ",
entry.pendingIncoming ? renderAmount(entry.available) : null,
);
console.log(
"incoming: ",
entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null,
);
if (Amounts.isNonZero(entry.pendingIncoming)) {
incoming = (
<i18n.Translate wrap="span">
<span style={{color: "darkgreen"}}>
<span style={{ color: "darkgreen" }}>
{"+"}
{renderAmount(entry.pendingIncoming)}
</span>
{" "}
</span>{" "}
incoming
</i18n.Translate>
</i18n.Translate>
);
}
if (Amounts.isNonZero(entry.pendingPayment)) {
payment = (
<i18n.Translate wrap="span">
<span style={{color: "red"}}>
<span style={{ color: "red" }}>
{"-"}
{renderAmount(entry.pendingPayment)}
</span>
{" "}
</span>{" "}
being spent
</i18n.Translate>
);
}
const l = [incoming, payment].filter((x) => x !== undefined);
const l = [incoming, payment].filter(x => x !== undefined);
if (l.length === 0) {
return <span />;
}
@ -303,49 +289,41 @@ class WalletBalanceView extends React.Component<any, any> {
if (l.length === 1) {
return <span>({l})</span>;
}
return <span>({l[0]}, {l[1]})</span>;
return (
<span>
({l[0]}, {l[1]})
</span>
);
}
render(): JSX.Element {
const wallet = this.balance;
if (this.gotError) {
return i18n.str`Error: could not retrieve balance information.`;
return (
<div>
<p>{i18n.str`Error: could not retrieve balance information.`}</p>
<p>
Click <PageLink pageName="welcome.html">here</PageLink> for help and diagnostics.
</p>
</div>
);
}
if (!wallet) {
return <span></span>;
}
console.log(wallet);
let paybackAvailable = false;
const listing = Object.keys(wallet.byCurrency).map((key) => {
const listing = Object.keys(wallet.byCurrency).map(key => {
const entry: WalletBalanceEntry = wallet.byCurrency[key];
if (entry.paybackAmount.value !== 0 || entry.paybackAmount.fraction !== 0) {
paybackAvailable = true;
}
return (
<p>
{bigAmount(entry.available)}
{" "}
{this.formatPending(entry)}
{bigAmount(entry.available)} {this.formatPending(entry)}
</p>
);
});
const makeLink = (page: string, name: string) => {
const url = chrome.extension.getURL(`/src/webex/pages/${page}`);
return <div><a className="actionLink" href={url} target="_blank">{name}</a></div>;
};
return (
<div>
{listing.length > 0 ? listing : this.renderEmpty()}
{paybackAvailable && makeLink("payback", i18n.str`Payback`)}
{makeLink("return-coins.html#dissolve", i18n.str`Return Electronic Cash to Bank Account`)}
{makeLink("auditors.html", i18n.str`Manage Trusted Auditors and Exchanges`)}
</div>
);
return <div>{listing.length > 0 ? listing : <EmptyBalanceView />}</div>;
}
}
function formatHistoryItem(historyItem: HistoryRecord) {
const d = historyItem.detail;
console.log("hist item", historyItem);
@ -353,13 +331,12 @@ function formatHistoryItem(historyItem: HistoryRecord) {
case "create-reserve":
return (
<i18n.Translate wrap="p">
Bank requested reserve (<span>{abbrev(d.reservePub)}</span>) for
{" "}
Bank requested reserve (<span>{abbrev(d.reservePub)}</span>) for{" "}
<span>{renderAmount(d.requestedAmount)}</span>.
</i18n.Translate>
);
case "confirm-reserve": {
const exchange = (new URI(d.exchangeBaseUrl)).host();
const exchange = new URI(d.exchangeBaseUrl).host();
const pub = abbrev(d.reservePub);
return (
<i18n.Translate wrap="p">
@ -372,30 +349,37 @@ function formatHistoryItem(historyItem: HistoryRecord) {
case "offer-contract": {
return (
<i18n.Translate wrap="p">
Merchant <em>{abbrev(d.merchantName, 15)}</em> offered
contract <span>{abbrev(d.contractTermsHash)}</span>.
Merchant <em>{abbrev(d.merchantName, 15)}</em> offered contract{" "}
<span>{abbrev(d.contractTermsHash)}</span>.
</i18n.Translate>
);
}
case "depleted-reserve": {
const exchange = d.exchangeBaseUrl ? (new URI(d.exchangeBaseUrl)).host() : "??";
const exchange = d.exchangeBaseUrl
? new URI(d.exchangeBaseUrl).host()
: "??";
const amount = renderAmount(d.requestedAmount);
const pub = abbrev(d.reservePub);
return (
<i18n.Translate wrap="p">
Withdrew <span>{amount}</span> from <span>{exchange}</span> (<span>{pub}</span>).
Withdrew <span>{amount}</span> from <span>{exchange}</span> (
<span>{pub}</span>).
</i18n.Translate>
);
}
case "pay": {
const url = d.fulfillmentUrl;
const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>;
const fulfillmentLinkElem = <a href={url} onClick={openTab(url)}>view product</a>;
const fulfillmentLinkElem = (
<a href={url} onClick={openTab(url)}>
view product
</a>
);
return (
<i18n.Translate wrap="p">
Paid <span>{renderAmount(d.amount)}</span> to merchant <span>{merchantElem}</span>.
<span> </span>
(<span>{fulfillmentLinkElem}</span>)
Paid <span>{renderAmount(d.amount)}</span> to merchant{" "}
<span>{merchantElem}</span>.<span> </span>(
<span>{fulfillmentLinkElem}</span>)
</i18n.Translate>
);
}
@ -403,12 +387,15 @@ function formatHistoryItem(historyItem: HistoryRecord) {
const merchantElem = <em>{abbrev(d.merchantName, 15)}</em>;
return (
<i18n.Translate wrap="p">
Merchant <span>{merchantElem}</span> gave a refund over <span>{renderAmount(d.refundAmount)}</span>.
Merchant <span>{merchantElem}</span> gave a refund over{" "}
<span>{renderAmount(d.refundAmount)}</span>.
</i18n.Translate>
);
}
case "tip": {
const tipPageUrl = new URI(chrome.extension.getURL("/src/webex/pages/tip.html"));
const tipPageUrl = new URI(
chrome.extension.getURL("/src/webex/pages/tip.html"),
);
const params = { tip_id: d.tipId, merchant_domain: d.merchantDomain };
const url = tipPageUrl.query(params).href();
const tipLink = <a href={url} onClick={openTab(url)}>{i18n.str`tip`}</a>;
@ -416,19 +403,23 @@ function formatHistoryItem(historyItem: HistoryRecord) {
return (
<>
<i18n.Translate wrap="p">
Merchant <span>{d.merchantDomain}</span> gave
a <span>{tipLink}</span> of <span>{renderAmount(d.amount)}</span>.
Merchant <span>{d.merchantDomain}</span> gave a{" "}
<span>{tipLink}</span> of <span>{renderAmount(d.amount)}</span>.
</i18n.Translate>
<span> { d.accepted ? null : <i18n.Translate>You did not accept the tip yet.</i18n.Translate> }</span>
<span>
{" "}
{d.accepted ? null : (
<i18n.Translate>You did not accept the tip yet.</i18n.Translate>
)}
</span>
</>
);
}
default:
return (<p>{i18n.str`Unknown event (${historyItem.type})`}</p>);
return <p>{i18n.str`Unknown event (${historyItem.type})`}</p>;
}
}
class WalletHistory extends React.Component<any, any> {
private myHistory: any[];
private gotError = false;
@ -445,7 +436,7 @@ class WalletHistory extends React.Component<any, any> {
}
update() {
chrome.runtime.sendMessage({type: "get-history"}, (resp) => {
chrome.runtime.sendMessage({ type: "get-history" }, resp => {
if (this.unmounted) {
return;
}
@ -480,7 +471,7 @@ class WalletHistory extends React.Component<any, any> {
const item = (
<div className="historyItem">
<div className="historyDate">
{(new Date(record.timestamp)).toString()}
{new Date(record.timestamp).toString()}
</div>
{formatHistoryItem(record)}
</div>
@ -494,10 +485,8 @@ class WalletHistory extends React.Component<any, any> {
}
return <p>{i18n.str`Your wallet has no events recorded.`}</p>;
}
}
function reload() {
try {
chrome.runtime.reload();
@ -508,43 +497,43 @@ function reload() {
}
function confirmReset() {
if (confirm("Do you want to IRREVOCABLY DESTROY everything inside your" +
" wallet and LOSE ALL YOUR COINS?")) {
if (
confirm(
"Do you want to IRREVOCABLY DESTROY everything inside your" +
" wallet and LOSE ALL YOUR COINS?",
)
) {
wxApi.resetDb();
window.close();
}
}
function WalletDebug(props: any) {
return (<div>
<p>Debug tools:</p>
<button onClick={openExtensionPage("/src/webex/pages/popup.html")}>
wallet tab
</button>
<button onClick={openExtensionPage("/src/webex/pages/benchmark.html")}>
benchmark
</button>
<button onClick={openExtensionPage("/src/webex/pages/show-db.html")}>
show db
</button>
<button onClick={openExtensionPage("/src/webex/pages/tree.html")}>
show tree
</button>
<button onClick={openExtensionPage("/src/webex/pages/logs.html")}>
show logs
</button>
<br />
<button onClick={confirmReset}>
reset
</button>
<button onClick={reload}>
reload chrome extension
</button>
</div>);
return (
<div>
<p>Debug tools:</p>
<button onClick={openExtensionPage("/src/webex/pages/popup.html")}>
wallet tab
</button>
<button onClick={openExtensionPage("/src/webex/pages/benchmark.html")}>
benchmark
</button>
<button onClick={openExtensionPage("/src/webex/pages/show-db.html")}>
show db
</button>
<button onClick={openExtensionPage("/src/webex/pages/tree.html")}>
show tree
</button>
<button onClick={openExtensionPage("/src/webex/pages/logs.html")}>
show logs
</button>
<br />
<button onClick={confirmReset}>reset</button>
<button onClick={reload}>reload chrome extension</button>
</div>
);
}
function openExtensionPage(page: string) {
return () => {
chrome.tabs.create({
@ -553,7 +542,6 @@ function openExtensionPage(page: string) {
};
}
function openTab(page: string) {
return (evt: React.SyntheticEvent<any>) => {
evt.preventDefault();
@ -563,15 +551,14 @@ function openTab(page: string) {
};
}
const el = (
<div>
<WalletNavBar />
<div style={{margin: "1em"}}>
<div style={{ margin: "1em" }}>
<Router>
<WalletBalanceView route="/balance" default/>
<WalletHistory route="/history"/>
<WalletDebug route="/debug"/>
<WalletBalanceView route="/balance" default />
<WalletHistory route="/history" />
<WalletDebug route="/debug" />
</Router>
</div>
</div>
@ -581,5 +568,5 @@ runOnceWhenReady(() => {
ReactDOM.render(el, document.getElementById("content")!);
// Will be used by the backend to detect when the popup gets closed,
// so we can clear notifications
chrome.runtime.connect({name: "popup"});
chrome.runtime.connect({ name: "popup" });
});

View File

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Taler Wallet: Tree View</title>
<link rel="stylesheet" type="text/css" href="../style/wallet.css">
<link rel="icon" href="/img/icon.png">
<script src="/dist/page-common-bundle.js"></script>
<script src="/dist/tree-bundle.js"></script>
<style>
.tree-item {
margin: 2em;
border-radius: 5px;
border: 1px solid gray;
padding: 1em;
}
</style>
<body>
<div id="container"></div>
</body>
</html>

View File

@ -1,402 +0,0 @@
/*
This file is part of TALER
(C) 2016 Inria
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/>
*/
/**
* Show contents of the wallet as a tree.
*
* @author Florian Dold
*/
import { getTalerStampDate } from "../../helpers";
import {
CoinRecord,
CoinStatus,
DenominationRecord,
ExchangeRecord,
PreCoinRecord,
ReserveRecord,
} from "../../dbTypes";
import { ImplicitStateComponent, StateHolder } from "../components";
import {
getCoins,
getDenoms,
getExchanges,
getPreCoins,
getReserves,
payback,
refresh,
} from "../wxApi";
import { ExpanderText, renderAmount } from "../renderHtml";
import * as React from "react";
import * as ReactDOM from "react-dom";
interface ReserveViewProps {
reserve: ReserveRecord;
}
class ReserveView extends React.Component<ReserveViewProps, {}> {
render(): JSX.Element {
const r: ReserveRecord = this.props.reserve;
return (
<div className="tree-item">
<ul>
<li>Key: {r.reserve_pub}</li>
<li>Created: {(new Date(r.created * 1000).toString())}</li>
<li>Current: {r.current_amount ? renderAmount(r.current_amount!) : "null"}</li>
<li>Requested: {renderAmount(r.requested_amount)}</li>
<li>Confirmed: {r.timestamp_confirmed}</li>
</ul>
</div>
);
}
}
interface ReserveListProps {
exchangeBaseUrl: string;
}
interface ToggleProps {
expanded: StateHolder<boolean>;
}
class Toggle extends ImplicitStateComponent<ToggleProps> {
renderButton() {
const show = () => {
this.props.expanded(true);
this.setState({});
};
const hide = () => {
this.props.expanded(false);
this.setState({});
};
if (this.props.expanded()) {
return <button onClick={hide}>hide</button>;
}
return <button onClick={show}>show</button>;
}
render() {
return (
<div style={{display: "inline"}}>
{this.renderButton()}
{this.props.expanded() ? this.props.children : []}
</div>);
}
}
interface CoinViewProps {
coin: CoinRecord;
}
interface RefreshDialogProps {
coin: CoinRecord;
}
class RefreshDialog extends ImplicitStateComponent<RefreshDialogProps> {
private refreshRequested = this.makeState<boolean>(false);
render(): JSX.Element {
if (!this.refreshRequested()) {
return (
<div style={{display: "inline"}}>
<button onClick={() => this.refreshRequested(true)}>refresh</button>
</div>
);
}
return (
<div>
Refresh amount: <input type="text" size={10} />
<button onClick={() => refresh(this.props.coin.coinPub)}>ok</button>
<button onClick={() => this.refreshRequested(false)}>cancel</button>
</div>
);
}
}
class CoinView extends React.Component<CoinViewProps, {}> {
render() {
const c = this.props.coin;
return (
<div className="tree-item">
<ul>
<li>Key: {c.coinPub}</li>
<li>Current amount: {renderAmount(c.currentAmount)}</li>
<li>Denomination: <ExpanderText text={c.denomPub} /></li>
<li>Suspended: {(c.suspended || false).toString()}</li>
<li>Status: {CoinStatus[c.status]}</li>
<li><RefreshDialog coin={c} /></li>
<li><button onClick={() => payback(c.coinPub)}>Payback</button></li>
</ul>
</div>
);
}
}
interface PreCoinViewProps {
precoin: PreCoinRecord;
}
class PreCoinView extends React.Component<PreCoinViewProps, {}> {
render() {
const c = this.props.precoin;
return (
<div className="tree-item">
<ul>
<li>Key: {c.coinPub}</li>
</ul>
</div>
);
}
}
interface CoinListProps {
exchangeBaseUrl: string;
}
class CoinList extends ImplicitStateComponent<CoinListProps> {
private coins = this.makeState<CoinRecord[] | null>(null);
private expanded = this.makeState<boolean>(false);
constructor(props: CoinListProps) {
super(props);
this.update(props);
}
async update(props: CoinListProps) {
const coins = await getCoins(props.exchangeBaseUrl);
this.coins(coins);
}
componentWillReceiveProps(newProps: CoinListProps) {
this.update(newProps);
}
render(): JSX.Element {
if (!this.coins()) {
return <div>...</div>;
}
return (
<div className="tree-item">
Coins ({this.coins() !.length.toString()})
{" "}
<Toggle expanded={this.expanded}>
{this.coins() !.map((c) => <CoinView coin={c} />)}
</Toggle>
</div>
);
}
}
interface PreCoinListProps {
exchangeBaseUrl: string;
}
class PreCoinList extends ImplicitStateComponent<PreCoinListProps> {
private precoins = this.makeState<PreCoinRecord[] | null>(null);
private expanded = this.makeState<boolean>(false);
constructor(props: PreCoinListProps) {
super(props);
this.update();
}
async update() {
const precoins = await getPreCoins(this.props.exchangeBaseUrl);
this.precoins(precoins);
}
render(): JSX.Element {
if (!this.precoins()) {
return <div>...</div>;
}
return (
<div className="tree-item">
Planchets ({this.precoins() !.length.toString()})
{" "}
<Toggle expanded={this.expanded}>
{this.precoins() !.map((c) => <PreCoinView precoin={c} />)}
</Toggle>
</div>
);
}
}
interface DenominationListProps {
exchange: ExchangeRecord;
}
class DenominationList extends ImplicitStateComponent<DenominationListProps> {
private expanded = this.makeState<boolean>(false);
private denoms = this.makeState<undefined|DenominationRecord[]>(undefined);
constructor(props: DenominationListProps) {
super(props);
this.update();
}
async update() {
const d = await getDenoms(this.props.exchange.baseUrl);
this.denoms(d);
}
renderDenom(d: DenominationRecord) {
return (
<div className="tree-item">
<ul>
<li>Offered: {d.isOffered ? "yes" : "no"}</li>
<li>Value: {renderAmount(d.value)}</li>
<li>Withdraw fee: {renderAmount(d.feeWithdraw)}</li>
<li>Refresh fee: {renderAmount(d.feeRefresh)}</li>
<li>Deposit fee: {renderAmount(d.feeDeposit)}</li>
<li>Refund fee: {renderAmount(d.feeRefund)}</li>
<li>Start: {getTalerStampDate(d.stampStart)!.toString()}</li>
<li>Withdraw expiration: {getTalerStampDate(d.stampExpireWithdraw)!.toString()}</li>
<li>Legal expiration: {getTalerStampDate(d.stampExpireLegal)!.toString()}</li>
<li>Deposit expiration: {getTalerStampDate(d.stampExpireDeposit)!.toString()}</li>
<li>Denom pub: <ExpanderText text={d.denomPub} /></li>
</ul>
</div>
);
}
render(): JSX.Element {
const denoms = this.denoms();
if (!denoms) {
return (
<div className="tree-item">
Denominations (...)
{" "}
<Toggle expanded={this.expanded}>
...
</Toggle>
</div>
);
}
return (
<div className="tree-item">
Denominations ({denoms.length.toString()})
{" "}
<Toggle expanded={this.expanded}>
{denoms.map((d) => this.renderDenom(d))}
</Toggle>
</div>
);
}
}
class ReserveList extends ImplicitStateComponent<ReserveListProps> {
private reserves = this.makeState<ReserveRecord[] | null>(null);
private expanded = this.makeState<boolean>(false);
constructor(props: ReserveListProps) {
super(props);
this.update();
}
async update() {
const reserves = await getReserves(this.props.exchangeBaseUrl);
this.reserves(reserves);
}
render(): JSX.Element {
if (!this.reserves()) {
return <div>...</div>;
}
return (
<div className="tree-item">
Reserves ({this.reserves() !.length.toString()})
{" "}
<Toggle expanded={this.expanded}>
{this.reserves() !.map((r) => <ReserveView reserve={r} />)}
</Toggle>
</div>
);
}
}
interface ExchangeProps {
exchange: ExchangeRecord;
}
class ExchangeView extends React.Component<ExchangeProps, {}> {
render(): JSX.Element {
const e = this.props.exchange;
return (
<div className="tree-item">
<ul>
<li>Exchange Base Url: {this.props.exchange.baseUrl}</li>
<li>Master public key: <ExpanderText text={this.props.exchange.masterPublicKey} /></li>
</ul>
<DenominationList exchange={e} />
<ReserveList exchangeBaseUrl={this.props.exchange.baseUrl} />
<CoinList exchangeBaseUrl={this.props.exchange.baseUrl} />
<PreCoinList exchangeBaseUrl={this.props.exchange.baseUrl} />
</div>
);
}
}
interface ExchangesListState {
exchanges?: ExchangeRecord[];
}
class ExchangesList extends React.Component<{}, ExchangesListState> {
constructor(props: {}) {
super(props);
const port = chrome.runtime.connect();
port.onMessage.addListener((msg: any) => {
if (msg.notify) {
console.log("got notified");
this.update();
}
});
this.update();
this.state = {} as any;
}
async update() {
const exchanges = await getExchanges();
console.log("exchanges: ", exchanges);
this.setState({ exchanges });
}
render(): JSX.Element {
const exchanges = this.state.exchanges;
if (!exchanges) {
return <span>...</span>;
}
return (
<div className="tree-item">
Exchanges ({exchanges.length.toString()}):
{exchanges.map((e) => <ExchangeView exchange={e} />)}
</div>
);
}
}
function main() {
ReactDOM.render(<ExchangesList />, document.getElementById("container")!);
}
document.addEventListener("DOMContentLoaded", main);

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Taler Wallet: Withdraw</title>
<link rel="icon" href="/img/icon.png">
<link rel="stylesheet" type="text/css" href="../style/pure.css">
<link rel="stylesheet" type="text/css" href="../style/wallet.css">
<script src="/dist/page-common-bundle.js"></script>
<script src="/dist/welcome-bundle.js"></script>
</head>
<body>
<section id="main">
<h1>GNU Taler Wallet Installed!</h1>
<div id="container">Loading...</div>
</section>
</body>
</html>

113
src/webex/pages/welcome.tsx Normal file
View File

@ -0,0 +1,113 @@
/*
This file is part of GNU Taler
(C) 2019 Taler Systems SA
GNU 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.
GNU 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
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Welcome page, shown on first installs.
*
* @author Florian Dold
*/
import React, { useState, useEffect } from "react";
import { getDiagnostics } from "../wxApi";
import { registerMountPage, PageLink } from "../renderHtml";
import { WalletDiagnostics } from "../../walletTypes";
function Diagnostics() {
const [timedOut, setTimedOut] = useState(false);
const [diagnostics, setDiagnostics] = useState<WalletDiagnostics | undefined>(
undefined,
);
useEffect(() => {
let gotDiagnostics = false;
setTimeout(() => {
if (!gotDiagnostics) {
console.error("timed out");
setTimedOut(true);
}
}, 1000);
const doFetch = async () => {
const d = await getDiagnostics();
console.log("got diagnostics", d);
gotDiagnostics = true;
setDiagnostics(d);
};
console.log("fetching diagnostics");
doFetch();
}, []);
if (timedOut) {
return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>;
}
if (diagnostics) {
if (diagnostics.errors.length === 0) {
return <p>Running diagnostics ... everything looks fine.</p>;
} else {
return (
<div
style={{
borderLeft: "0.5em solid red",
paddingLeft: "1em",
paddingTop: "0.2em",
paddingBottom: "0.2em",
}}
>
<p>Problems detected:</p>
<ol>
{diagnostics.errors.map(errMsg => (
<li>{errMsg}</li>
))}
</ol>
{diagnostics.firefoxIdbProblem ? (
<p>
Please check in your <code>about:config</code> settings that you
have IndexedDB enabled (check the preference name{" "}
<code>dom.indexedDB.enabled</code>).
</p>
) : null}
{diagnostics.dbOutdated ? (
<p>
Your wallet database is outdated. Currently automatic migration is
not supported. Please go{" "}
<PageLink pageName="reset-required.html">here</PageLink> to reset
the wallet database.
</p>
) : null}
</div>
);
}
}
return <p>Running diagnostics ...</p>;
}
function Welcome() {
return (
<>
<p>Thank you for installing the wallet.</p>
<h2>First Steps</h2>
<p>
Check out <a href="https://demo.taler.net/">demo.taler.net</a> for a
demo.
</p>
<h2>Troubleshooting</h2>
<Diagnostics />
</>
);
}
registerMountPage(() => <Welcome />);

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Taler Wallet: Select Taler Provider</title>
<title>Taler Wallet: Withdraw</title>
<link rel="icon" href="/img/icon.png">
<link rel="stylesheet" type="text/css" href="../style/pure.css">

View File

@ -21,21 +21,13 @@
* @author Florian Dold
*/
import { canonicalizeBaseUrl } from "../../helpers";
import * as i18n from "../../i18n";
import { AmountJson } from "../../amounts";
import * as Amounts from "../../amounts";
import { CurrencyRecord } from "../../dbTypes";
import {
CreateReserveResponse,
ReserveCreationInfo,
WithdrawDetails,
} from "../../walletTypes";
import { ImplicitStateComponent, StateHolder } from "../components";
import { WithdrawDetailView, renderAmount } from "../renderHtml";
import React, { useState, useEffect } from "react";

View File

@ -26,22 +26,16 @@
*/
import { AmountJson } from "../amounts";
import * as Amounts from "../amounts";
import {
DenominationRecord,
} from "../dbTypes";
import {
ReserveCreationInfo,
} from "../walletTypes";
import { ImplicitStateComponent } from "./components";
import * as moment from "moment";
import * as i18n from "../i18n";
import * as React from "react";
import React from "react";
import ReactDOM from "react-dom";
/**
@ -274,49 +268,16 @@ interface ExpanderTextProps {
text: string;
}
/**
* Show a heading with a toggle to show/hide the expandable content.
*/
export class ExpanderText extends ImplicitStateComponent<ExpanderTextProps> {
private expanded = this.makeState<boolean>(false);
private textArea: any = undefined;
componentDidUpdate() {
if (this.expanded() && this.textArea) {
this.textArea.focus();
this.textArea.scrollTop = 0;
}
}
render(): JSX.Element {
if (!this.expanded()) {
return (
<span onClick={() => { this.expanded(true); }}>
{(this.props.text.length <= 10)
? this.props.text
: (
<span>
{this.props.text.substring(0, 10)}
<span style={{textDecoration: "underline"}}>...</span>
</span>
)
}
</span>
);
}
return (
<textarea
readOnly
style={{display: "block"}}
onBlur={() => this.expanded(false)}
ref={(e) => this.textArea = e}>
{this.props.text}
</textarea>
);
}
export function ExpanderText({ text }: ExpanderTextProps) {
return <span>{text}</span>;
}
export interface LoadingButtonProps {
loading: boolean;
}
@ -340,4 +301,35 @@ export function ProgressButton(
{props.children}
</button>
);
}
export function registerMountPage(mainFn: () => React.ReactElement) {
async function main() {
try {
const mainElement = mainFn();
const container = document.getElementById("container");
if (!container) {
throw Error("container not found, can't mount page contents");
}
ReactDOM.render(
mainElement,
container,
);
} catch (e) {
document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
console.error("got error", e);
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
return;
} else {
main();
}
}
export function PageLink(props: React.PropsWithChildren<{pageName: string}>) {
const url = chrome.extension.getURL(`/src/webex/pages/${props.pageName}`);
return <a className="actionLink" href={url} target="_blank">{props.children}</a>;
}

View File

@ -42,6 +42,7 @@ import {
TipStatus,
WalletBalance,
PurchaseDetails,
WalletDiagnostics,
} from "../walletTypes";
import {
@ -396,3 +397,10 @@ export function preparePay(talerPayUri: string) {
export function acceptWithdrawal(talerWithdrawUri: string, selectedExchange: string) {
return callBackend("accept-withdrawal", { talerWithdrawUri, selectedExchange });
}
/**
* Get diagnostics information
*/
export function getDiagnostics(): Promise<WalletDiagnostics> {
return callBackend("get-diagnostics", {});
}

View File

@ -25,40 +25,34 @@
*/
import { BrowserHttpLib } from "../http";
import * as logging from "../logging";
import { AmountJson } from "../amounts";
import {
ConfirmReserveRequest,
CreateReserveRequest,
Notifier,
ReturnCoinsRequest,
WalletDiagnostics,
} from "../walletTypes";
import { Wallet } from "../wallet";
import { isFirefox } from "./compat";
import { PurchaseRecord, WALLET_DB_VERSION } from "../dbTypes";
import { WALLET_DB_VERSION } from "../dbTypes";
import { openTalerDb, exportDb, importDb, deleteDb } from "../db";
import { ChromeBadge } from "./chromeBadge";
import { MessageType } from "./messages";
import * as wxApi from "./wxApi";
import URI = require("urijs");
import Port = chrome.runtime.Port;
import MessageSender = chrome.runtime.MessageSender;
import { BrowserCryptoWorkerFactory } from "../crypto/cryptoApi";
import { OpenedPromise, openPromise } from "../promiseUtils";
const NeedsWallet = Symbol("NeedsWallet");
function handleMessage(
async function handleMessage(
sender: MessageSender,
type: MessageType,
detail: any,
): any {
): Promise<any> {
function assertNotFound(t: never): never {
console.error(`Request type ${t as string} unknown`);
console.error(`Request detail was ${detail}`);
@ -251,7 +245,7 @@ function handleMessage(
const resp: wxApi.UpgradeResponse = {
currentDbVersion: WALLET_DB_VERSION.toString(),
dbResetRequired,
oldDbVersion: (oldDbVersion || "unknown").toString(),
oldDbVersion: (outdatedDbVersion || "unknown").toString(),
};
return resp;
}
@ -314,6 +308,39 @@ function handleMessage(
detail.selectedExchange,
);
}
case "get-diagnostics": {
const manifestData = chrome.runtime.getManifest();
const errors: string[] = [];
let firefoxIdbProblem = false;
let dbOutdated = false;
try {
await walletInit.promise;
} catch (e) {
errors.push("Error during wallet initialization: " + e);
if (currentDatabase === undefined && outdatedDbVersion === undefined && isFirefox()) {
firefoxIdbProblem = true;
}
}
if (!currentWallet) {
errors.push("Could not create wallet backend.");
}
if (!currentDatabase) {
errors.push("Could not open database");
}
if (outdatedDbVersion !== undefined) {
errors.push(`Outdated DB version: ${outdatedDbVersion}`);
dbOutdated = true;
}
const diagnostics: WalletDiagnostics = {
walletManifestDisplayVersion:
manifestData.version_name || "(undefined)",
walletManifestVersion: manifestData.version,
errors,
firefoxIdbProblem,
dbOutdated,
};
return diagnostics;
}
case "prepare-pay":
return needsWallet().preparePay(detail.talerPayUri);
default:
@ -351,7 +378,7 @@ async function dispatch(
error: {
message: e.message,
stack,
}
},
});
} catch (e) {
console.log(e);
@ -441,26 +468,24 @@ function makeSyncWalletRedirect(
return { redirectUrl: outerUrl.href() };
}
// Rate limit cache for executePayment operations, to break redirect loops
let rateLimitCache: { [n: number]: number } = {};
function clearRateLimitCache() {
rateLimitCache = {};
}
/**
* Currently active wallet instance. Might be unloaded and
* re-instantiated when the database is reset.
*/
let currentWallet: Wallet | undefined;
let currentDatabase: IDBDatabase | undefined;
/**
* Last version if an outdated DB, if applicable.
*/
let oldDbVersion: number | undefined;
let outdatedDbVersion: number | undefined;
let walletInit: OpenedPromise<void> = openPromise<void>();
function handleUpgradeUnsupported(oldDbVersion: number, newDbVersion: number) {
console.log("DB migration not supported");
outdatedDbVersion = oldDbVersion;
chrome.tabs.create({
url: chrome.extension.getURL("/src/webex/pages/reset-required.html"),
});
@ -473,20 +498,25 @@ async function reinitWallet() {
currentWallet.stop();
currentWallet = undefined;
}
currentDatabase = undefined;
setBadgeText({ text: "" });
const badge = new ChromeBadge();
let db: IDBDatabase;
try {
db = await openTalerDb(indexedDB, reinitWallet, handleUpgradeUnsupported);
currentDatabase = await openTalerDb(
indexedDB,
reinitWallet,
handleUpgradeUnsupported,
);
} catch (e) {
console.error("could not open database", e);
walletInit.reject(e);
return;
}
const http = new BrowserHttpLib();
const notifier = new ChromeNotifier();
console.log("setting wallet");
const wallet = new Wallet(
db,
currentDatabase,
http,
badge,
notifier,
@ -495,6 +525,7 @@ async function reinitWallet() {
// Useful for debugging in the background page.
(window as any).talerWallet = wallet;
currentWallet = wallet;
walletInit.resolve();
}
/**
@ -528,6 +559,13 @@ function injectScript(
* Sets up all event handlers and other machinery.
*/
export async function wxMain() {
chrome.runtime.onInstalled.addListener(details => {
if (details.reason === "install") {
const url = chrome.extension.getURL("/src/webex/pages/welcome.html");
chrome.tabs.create({ active: true, url: url });
}
});
// Explicitly unload the extension page as soon as an update is available,
// so the update gets installed as soon as possible.
chrome.runtime.onUpdateAvailable.addListener(details => {
@ -630,8 +668,6 @@ export async function wxMain() {
tabTimers[tabId] = timers;
});
chrome.extension.getBackgroundPage()!.setInterval(clearRateLimitCache, 5000);
reinitWallet();
// Handlers for messages coming directly from the content

View File

@ -66,7 +66,6 @@
"src/webex/background.ts",
"src/webex/chromeBadge.ts",
"src/webex/compat.ts",
"src/webex/components.ts",
"src/webex/messages.ts",
"src/webex/notify.ts",
"src/webex/pages/add-auditor.tsx",
@ -84,7 +83,7 @@
"src/webex/pages/return-coins.tsx",
"src/webex/pages/show-db.ts",
"src/webex/pages/tip.tsx",
"src/webex/pages/tree.tsx",
"src/webex/pages/welcome.tsx",
"src/webex/pages/withdraw.tsx",
"src/webex/renderHtml.tsx",
"src/webex/wxApi.ts",

View File

@ -79,6 +79,7 @@ module.exports = function (env) {
"benchmark": "./src/webex/pages/benchmark.tsx",
"pay": "./src/webex/pages/pay.tsx",
"withdraw": "./src/webex/pages/withdraw.tsx",
"welcome": "./src/webex/pages/welcome.tsx",
"error": "./src/webex/pages/error.tsx",
"logs": "./src/webex/pages/logs.tsx",
"payback": "./src/webex/pages/payback.tsx",
@ -88,7 +89,6 @@ module.exports = function (env) {
"refund": "./src/webex/pages/refund.tsx",
"show-db": "./src/webex/pages/show-db.ts",
"tip": "./src/webex/pages/tip.tsx",
"tree": "./src/webex/pages/tree.tsx",
},
name: "pages",
optimization: {