components renaming to follow react pattern
This commit is contained in:
parent
d58945c830
commit
0b4976601f
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
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 { JSX } from "preact";
|
||||||
|
|
||||||
|
export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean; onToggle: () => void; }): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
checked={enabled}
|
||||||
|
onClick={onToggle}
|
||||||
|
type="checkbox"
|
||||||
|
id="checkbox-perm"
|
||||||
|
style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }} />
|
||||||
|
<label
|
||||||
|
htmlFor="checkbox-perm"
|
||||||
|
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
|
||||||
|
>
|
||||||
|
Automatically open wallet based on page content
|
||||||
|
</label>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: "#383838",
|
||||||
|
fontSize: "smaller",
|
||||||
|
display: "block",
|
||||||
|
marginLeft: "2em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
(Enabling this option below will make using the wallet faster, but
|
||||||
|
requires more permissions from your browser.)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
import { useState, useEffect } from "preact/hooks";
|
||||||
|
import * as wxApi from "../wxApi";
|
||||||
|
import { getPermissionsApi } from "../compat";
|
||||||
|
import { extendedPermissions } from "../permissions";
|
||||||
|
|
||||||
|
|
||||||
|
export function useExtendedPermissions(): [boolean, () => void] {
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
setEnabled(v => !v);
|
||||||
|
handleExtendedPerm(enabled).then(result => {
|
||||||
|
setEnabled(result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getExtendedPermValue(): Promise<void> {
|
||||||
|
const res = await wxApi.getExtendedPermissions();
|
||||||
|
setEnabled(res.newValue);
|
||||||
|
}
|
||||||
|
getExtendedPermValue();
|
||||||
|
}, []);
|
||||||
|
return [enabled, toggle];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExtendedPerm(isEnabled: boolean): Promise<boolean> {
|
||||||
|
let nextVal: boolean | undefined;
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
const granted = await new Promise<boolean>((resolve, reject) => {
|
||||||
|
// We set permissions here, since apparently FF wants this to be done
|
||||||
|
// as the result of an input event ...
|
||||||
|
getPermissionsApi().request(extendedPermissions, (granted: boolean) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.error("error requesting permissions");
|
||||||
|
console.error(chrome.runtime.lastError);
|
||||||
|
reject(chrome.runtime.lastError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("permissions granted:", granted);
|
||||||
|
resolve(granted);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const res = await wxApi.setExtendedPermissions(granted);
|
||||||
|
nextVal = res.newValue;
|
||||||
|
} else {
|
||||||
|
const res = await wxApi.setExtendedPermissions(false);
|
||||||
|
nextVal = res.newValue;
|
||||||
|
}
|
||||||
|
console.log("new permissions applied:", nextVal ?? false);
|
||||||
|
return nextVal ?? false
|
||||||
|
}
|
@ -1,24 +0,0 @@
|
|||||||
import { useState, useEffect } from "preact/hooks";
|
|
||||||
import * as wxApi from "../wxApi";
|
|
||||||
import { handleExtendedPerm } from "../wallet/welcome";
|
|
||||||
|
|
||||||
|
|
||||||
export function useExtendedPermissions(): [boolean, () => void] {
|
|
||||||
const [enabled, setEnabled] = useState(false);
|
|
||||||
|
|
||||||
const toggle = () => {
|
|
||||||
setEnabled(v => !v);
|
|
||||||
handleExtendedPerm(enabled).then(result => {
|
|
||||||
setEnabled(result);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getExtendedPermValue(): Promise<void> {
|
|
||||||
const res = await wxApi.getExtendedPermissions();
|
|
||||||
setEnabled(res.newValue);
|
|
||||||
}
|
|
||||||
getExtendedPermValue();
|
|
||||||
}, []);
|
|
||||||
return [enabled, toggle];
|
|
||||||
}
|
|
@ -0,0 +1,93 @@
|
|||||||
|
import { classifyTalerUri, TalerUriType } from "@gnu-taler/taler-util";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
export function useTalerActionURL(): [string | undefined, (s: boolean) => void] {
|
||||||
|
const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const [dismissed, setDismissed] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
async function check(): Promise<void> {
|
||||||
|
const talerUri = await findTalerUriInActiveTab();
|
||||||
|
if (talerUri) {
|
||||||
|
const actionUrl = actionForTalerUri(talerUri);
|
||||||
|
setTalerActionUrl(actionUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check();
|
||||||
|
}, []);
|
||||||
|
const url = dismissed ? undefined : talerActionUrl;
|
||||||
|
return [url, setDismissed];
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionForTalerUri(talerUri: string): string | undefined {
|
||||||
|
const uriType = classifyTalerUri(talerUri);
|
||||||
|
switch (uriType) {
|
||||||
|
case TalerUriType.TalerWithdraw:
|
||||||
|
return makeExtensionUrlWithParams("static/wallet.html#/withdraw", {
|
||||||
|
talerWithdrawUri: talerUri,
|
||||||
|
});
|
||||||
|
case TalerUriType.TalerPay:
|
||||||
|
return makeExtensionUrlWithParams("static/wallet.html#/pay", {
|
||||||
|
talerPayUri: talerUri,
|
||||||
|
});
|
||||||
|
case TalerUriType.TalerTip:
|
||||||
|
return makeExtensionUrlWithParams("static/wallet.html#/tip", {
|
||||||
|
talerTipUri: talerUri,
|
||||||
|
});
|
||||||
|
case TalerUriType.TalerRefund:
|
||||||
|
return makeExtensionUrlWithParams("static/wallet.html#/refund", {
|
||||||
|
talerRefundUri: talerUri,
|
||||||
|
});
|
||||||
|
case TalerUriType.TalerNotifyReserve:
|
||||||
|
// FIXME: implement
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(
|
||||||
|
"Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeExtensionUrlWithParams(
|
||||||
|
url: string,
|
||||||
|
params?: { [name: string]: string | undefined },
|
||||||
|
): string {
|
||||||
|
const innerUrl = new URL(chrome.extension.getURL("/" + url));
|
||||||
|
if (params) {
|
||||||
|
for (const key in params) {
|
||||||
|
const p = params[key];
|
||||||
|
if (p) {
|
||||||
|
innerUrl.searchParams.set(key, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return innerUrl.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findTalerUriInActiveTab(): Promise<string | undefined> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.tabs.executeScript(
|
||||||
|
{
|
||||||
|
code: `
|
||||||
|
(() => {
|
||||||
|
let x = document.querySelector("a[href^='taler://'") || document.querySelector("a[href^='taler+http://'");
|
||||||
|
return x ? x.href.toString() : null;
|
||||||
|
})();
|
||||||
|
`,
|
||||||
|
allFrames: false,
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.error(chrome.runtime.lastError);
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("got result", result);
|
||||||
|
resolve(result[0]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
173
packages/taler-wallet-webextension/src/popup/Balance.tsx
Normal file
173
packages/taler-wallet-webextension/src/popup/Balance.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
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,
|
||||||
|
BalancesResponse,
|
||||||
|
Balance, i18n, AmountJson, amountFractionalBase
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { Component, JSX } from "preact";
|
||||||
|
import { PageLink, renderAmount } from "../renderHtml";
|
||||||
|
import * as wxApi from "../wxApi";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an amount as a large number with a small currency symbol.
|
||||||
|
*/
|
||||||
|
function bigAmount(amount: AmountJson): JSX.Element {
|
||||||
|
const v = amount.value + amount.fraction / amountFractionalBase;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
|
||||||
|
<span>{amount.currency}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyBalanceView(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<p><i18n.Translate>
|
||||||
|
You have no balance to show. Need some{" "}
|
||||||
|
<PageLink pageName="/welcome">help</PageLink> getting started?
|
||||||
|
</i18n.Translate></p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class BalancePage extends Component<any, any> {
|
||||||
|
private balance?: BalancesResponse;
|
||||||
|
private gotError = false;
|
||||||
|
private canceler: (() => void) | undefined = undefined;
|
||||||
|
private unmount = false;
|
||||||
|
private updateBalanceRunning = false;
|
||||||
|
|
||||||
|
componentWillMount(): void {
|
||||||
|
this.canceler = wxApi.onUpdateNotification(() => this.updateBalance());
|
||||||
|
this.updateBalance();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
console.log("component WalletBalanceView will unmount");
|
||||||
|
if (this.canceler) {
|
||||||
|
this.canceler();
|
||||||
|
}
|
||||||
|
this.unmount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBalance(): Promise<void> {
|
||||||
|
if (this.updateBalanceRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateBalanceRunning = true;
|
||||||
|
let balance: BalancesResponse;
|
||||||
|
try {
|
||||||
|
balance = await wxApi.getBalance();
|
||||||
|
} catch (e) {
|
||||||
|
if (this.unmount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.gotError = true;
|
||||||
|
console.error("could not retrieve balances", e);
|
||||||
|
this.setState({});
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
this.updateBalanceRunning = false;
|
||||||
|
}
|
||||||
|
if (this.unmount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.gotError = false;
|
||||||
|
console.log("got balance", balance);
|
||||||
|
this.balance = balance;
|
||||||
|
this.setState({});
|
||||||
|
}
|
||||||
|
|
||||||
|
formatPending(entry: Balance): JSX.Element {
|
||||||
|
let incoming: JSX.Element | undefined;
|
||||||
|
let payment: JSX.Element | undefined;
|
||||||
|
|
||||||
|
const available = Amounts.parseOrThrow(entry.available);
|
||||||
|
const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
|
||||||
|
const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"available: ",
|
||||||
|
entry.pendingIncoming ? renderAmount(entry.available) : null
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"incoming: ",
|
||||||
|
entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!Amounts.isZero(pendingIncoming)) {
|
||||||
|
incoming = (
|
||||||
|
<span><i18n.Translate>
|
||||||
|
<span style={{ color: "darkgreen" }}>
|
||||||
|
{"+"}
|
||||||
|
{renderAmount(entry.pendingIncoming)}
|
||||||
|
</span>{" "}
|
||||||
|
incoming
|
||||||
|
</i18n.Translate></span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const l = [incoming, payment].filter((x) => x !== undefined);
|
||||||
|
if (l.length === 0) {
|
||||||
|
return <span />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l.length === 1) {
|
||||||
|
return <span>({l})</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
({l[0]}, {l[1]})
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
const wallet = this.balance;
|
||||||
|
if (this.gotError) {
|
||||||
|
return (
|
||||||
|
<div className="balance">
|
||||||
|
<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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listing = wallet.balances.map((entry) => {
|
||||||
|
const av = Amounts.parseOrThrow(entry.available);
|
||||||
|
return (
|
||||||
|
<p key={av.currency}>
|
||||||
|
{bigAmount(av)} {this.formatPending(entry)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return listing.length > 0 ? (
|
||||||
|
<div className="balance">{listing}</div>
|
||||||
|
) : (
|
||||||
|
<EmptyBalanceView />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
packages/taler-wallet-webextension/src/popup/Debug.tsx
Normal file
63
packages/taler-wallet-webextension/src/popup/Debug.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
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 { JSX } from "preact";
|
||||||
|
import { Diagnostics } from "../components/Diagnostics";
|
||||||
|
import * as wxApi from "../wxApi";
|
||||||
|
|
||||||
|
|
||||||
|
export function DebugPage(props: any): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Debug tools:</p>
|
||||||
|
<button onClick={openExtensionPage("/static/popup.html")}>wallet tab</button>
|
||||||
|
<br />
|
||||||
|
<button onClick={confirmReset}>reset</button>
|
||||||
|
<button onClick={reload}>reload chrome extension</button>
|
||||||
|
<Diagnostics />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reload(): void {
|
||||||
|
try {
|
||||||
|
chrome.runtime.reload();
|
||||||
|
window.close();
|
||||||
|
} catch (e) {
|
||||||
|
// Functionality missing in firefox, ignore!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmReset(): Promise<void> {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"Do you want to IRREVOCABLY DESTROY everything inside your" +
|
||||||
|
" wallet and LOSE ALL YOUR COINS?",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await wxApi.resetDb();
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openExtensionPage(page: string) {
|
||||||
|
return () => {
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: chrome.extension.getURL(page),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
227
packages/taler-wallet-webextension/src/popup/History.tsx
Normal file
227
packages/taler-wallet-webextension/src/popup/History.tsx
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
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 { AmountString, Timestamp, Transaction, TransactionsResponse, TransactionType } from "@gnu-taler/taler-util";
|
||||||
|
import { JSX } from "preact";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import * as wxApi from "../wxApi";
|
||||||
|
import { Pages } from "./popup";
|
||||||
|
|
||||||
|
|
||||||
|
export function HistoryPage(props: any): JSX.Element {
|
||||||
|
const [transactions, setTransactions] = useState<
|
||||||
|
TransactionsResponse | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async (): Promise<void> => {
|
||||||
|
const res = await wxApi.getTransactions();
|
||||||
|
setTransactions(res);
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!transactions) {
|
||||||
|
return <div>Loading ...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const txs = [...transactions.transactions].reverse();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{txs.map((tx, i) => (
|
||||||
|
<TransactionItem key={i} tx={tx} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransactionItem(props: { tx: Transaction }): JSX.Element {
|
||||||
|
const tx = props.tx;
|
||||||
|
switch (tx.type) {
|
||||||
|
case TransactionType.Withdrawal:
|
||||||
|
return (
|
||||||
|
<TransactionLayout
|
||||||
|
id={tx.transactionId}
|
||||||
|
amount={tx.amountEffective}
|
||||||
|
debitCreditIndicator={"credit"}
|
||||||
|
title="Withdrawal"
|
||||||
|
subtitle={`via ${tx.exchangeBaseUrl}`}
|
||||||
|
timestamp={tx.timestamp}
|
||||||
|
iconPath="/static/img/ri-bank-line.svg"
|
||||||
|
pending={tx.pending}
|
||||||
|
></TransactionLayout>
|
||||||
|
);
|
||||||
|
case TransactionType.Payment:
|
||||||
|
return (
|
||||||
|
<TransactionLayout
|
||||||
|
id={tx.transactionId}
|
||||||
|
amount={tx.amountEffective}
|
||||||
|
debitCreditIndicator={"debit"}
|
||||||
|
title="Payment"
|
||||||
|
subtitle={tx.info.summary}
|
||||||
|
timestamp={tx.timestamp}
|
||||||
|
iconPath="/static/img/ri-shopping-cart-line.svg"
|
||||||
|
pending={tx.pending}
|
||||||
|
></TransactionLayout>
|
||||||
|
);
|
||||||
|
case TransactionType.Refund:
|
||||||
|
return (
|
||||||
|
<TransactionLayout
|
||||||
|
id={tx.transactionId}
|
||||||
|
amount={tx.amountEffective}
|
||||||
|
debitCreditIndicator={"credit"}
|
||||||
|
title="Refund"
|
||||||
|
subtitle={tx.info.summary}
|
||||||
|
timestamp={tx.timestamp}
|
||||||
|
iconPath="/static/img/ri-refund-2-line.svg"
|
||||||
|
pending={tx.pending}
|
||||||
|
></TransactionLayout>
|
||||||
|
);
|
||||||
|
case TransactionType.Tip:
|
||||||
|
return (
|
||||||
|
<TransactionLayout
|
||||||
|
id={tx.transactionId}
|
||||||
|
amount={tx.amountEffective}
|
||||||
|
debitCreditIndicator={"credit"}
|
||||||
|
title="Tip"
|
||||||
|
subtitle={`from ${new URL(tx.merchantBaseUrl).hostname}`}
|
||||||
|
timestamp={tx.timestamp}
|
||||||
|
iconPath="/static/img/ri-hand-heart-line.svg"
|
||||||
|
pending={tx.pending}
|
||||||
|
></TransactionLayout>
|
||||||
|
);
|
||||||
|
case TransactionType.Refresh:
|
||||||
|
return (
|
||||||
|
<TransactionLayout
|
||||||
|
id={tx.transactionId}
|
||||||
|
amount={tx.amountEffective}
|
||||||
|
debitCreditIndicator={"credit"}
|
||||||
|
title="Refresh"
|
||||||
|
subtitle={`via exchange ${tx.exchangeBaseUrl}`}
|
||||||
|
timestamp={tx.timestamp}
|
||||||
|
iconPath="/static/img/ri-refresh-line.svg"
|
||||||
|
pending={tx.pending}
|
||||||
|
></TransactionLayout>
|
||||||
|
);
|
||||||
|
case TransactionType.Deposit:
|
||||||
|
return (
|
||||||
|
<TransactionLayout
|
||||||
|
id={tx.transactionId}
|
||||||
|
amount={tx.amountEffective}
|
||||||
|
debitCreditIndicator={"debit"}
|
||||||
|
title="Refresh"
|
||||||
|
subtitle={`to ${tx.targetPaytoUri}`}
|
||||||
|
timestamp={tx.timestamp}
|
||||||
|
iconPath="/static/img/ri-refresh-line.svg"
|
||||||
|
pending={tx.pending}
|
||||||
|
></TransactionLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
|
||||||
|
const date = new Date(props.timestamp.t_ms);
|
||||||
|
const dateStr = date.toLocaleString([], {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
} as any);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
border: "1px solid gray",
|
||||||
|
borderRadius: "0.5em",
|
||||||
|
margin: "0.5em 0",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
padding: "0.5em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={props.iconPath} />
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", flexDirection: "column", marginLeft: "1em" }}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: "small", color: "gray" }}>{dateStr}</div>
|
||||||
|
<div style={{ fontVariant: "small-caps", fontSize: "x-large" }}>
|
||||||
|
<a href={Pages.transaction.replace(':tid', props.id)}><span>{props.title}</span></a>
|
||||||
|
{props.pending ? (
|
||||||
|
<span style={{ color: "darkblue" }}> (Pending)</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>{props.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
<TransactionAmount
|
||||||
|
pending={props.pending}
|
||||||
|
amount={props.amount}
|
||||||
|
debitCreditIndicator={props.debitCreditIndicator}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionLayoutProps {
|
||||||
|
debitCreditIndicator: "debit" | "credit" | "unknown";
|
||||||
|
amount: AmountString | "unknown";
|
||||||
|
timestamp: Timestamp;
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
subtitle: string;
|
||||||
|
iconPath: string;
|
||||||
|
pending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionAmountProps {
|
||||||
|
debitCreditIndicator: "debit" | "credit" | "unknown";
|
||||||
|
amount: AmountString | "unknown";
|
||||||
|
pending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TransactionAmount(props: TransactionAmountProps): JSX.Element {
|
||||||
|
const [currency, amount] = props.amount.split(":");
|
||||||
|
let sign: string;
|
||||||
|
switch (props.debitCreditIndicator) {
|
||||||
|
case "credit":
|
||||||
|
sign = "+";
|
||||||
|
break;
|
||||||
|
case "debit":
|
||||||
|
sign = "-";
|
||||||
|
break;
|
||||||
|
case "unknown":
|
||||||
|
sign = "";
|
||||||
|
}
|
||||||
|
const style: JSX.AllCSSProperties = {
|
||||||
|
marginLeft: "auto",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
alignSelf: "center"
|
||||||
|
};
|
||||||
|
if (props.pending) {
|
||||||
|
style.color = "gray";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ ...style }}>
|
||||||
|
<div style={{ fontSize: "x-large" }}>
|
||||||
|
{sign}
|
||||||
|
{amount}
|
||||||
|
</div>
|
||||||
|
<div>{currency}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
34
packages/taler-wallet-webextension/src/popup/Settings.tsx
Normal file
34
packages/taler-wallet-webextension/src/popup/Settings.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
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 { PermissionsCheckbox } from "../components/PermissionsCheckbox";
|
||||||
|
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
|
||||||
|
|
||||||
|
|
||||||
|
export function SettingsPage() {
|
||||||
|
const [permissionsEnabled, togglePermissions] = useExtendedPermissions();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Permissions</h2>
|
||||||
|
<PermissionsCheckbox enabled={permissionsEnabled} onToggle={togglePermissions} />
|
||||||
|
{/*
|
||||||
|
<h2>Developer mode</h2>
|
||||||
|
<DebugCheckbox enabled={permissionsEnabled} onToggle={togglePermissions} />
|
||||||
|
*/}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -26,11 +26,12 @@ import {
|
|||||||
TransactionWithdrawal,
|
TransactionWithdrawal,
|
||||||
WithdrawalType
|
WithdrawalType
|
||||||
} from '@gnu-taler/taler-util';
|
} from '@gnu-taler/taler-util';
|
||||||
import { WalletTransactionView as Component } from './popup';
|
import { FunctionalComponent } from 'preact';
|
||||||
|
import { TransactionView as TestedComponent } from './Transaction';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'popup/transaction details',
|
title: 'popup/transaction/details',
|
||||||
component: Component,
|
component: TestedComponent,
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story: any) => <div>
|
(Story: any) => <div>
|
||||||
<link key="1" rel="stylesheet" type="text/css" href="/style/pure.css" />
|
<link key="1" rel="stylesheet" type="text/css" href="/style/pure.css" />
|
||||||
@ -114,32 +115,32 @@ const exampleData = {
|
|||||||
} as TransactionRefund,
|
} as TransactionRefund,
|
||||||
}
|
}
|
||||||
|
|
||||||
function dynamic<T>(props: any) {
|
function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
|
||||||
const r = (args: any) => <Component {...args} />
|
const r = (args: any) => <Component {...args} />
|
||||||
r.args = props
|
r.args = props
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotYetLoaded = dynamic({});
|
export const NotYetLoaded = createExample(TestedComponent,{});
|
||||||
|
|
||||||
export const Withdraw = dynamic({
|
export const Withdraw = createExample(TestedComponent,{
|
||||||
transaction: exampleData.withdraw
|
transaction: exampleData.withdraw
|
||||||
});
|
});
|
||||||
|
|
||||||
export const WithdrawPending = dynamic({
|
export const WithdrawPending = createExample(TestedComponent,{
|
||||||
transaction: { ...exampleData.withdraw, pending: true },
|
transaction: { ...exampleData.withdraw, pending: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const Payment = dynamic({
|
export const Payment = createExample(TestedComponent,{
|
||||||
transaction: exampleData.payment
|
transaction: exampleData.payment
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PaymentPending = dynamic({
|
export const PaymentPending = createExample(TestedComponent,{
|
||||||
transaction: { ...exampleData.payment, pending: true },
|
transaction: { ...exampleData.payment, pending: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PaymentWithProducts = dynamic({
|
export const PaymentWithProducts = createExample(TestedComponent,{
|
||||||
transaction: {
|
transaction: {
|
||||||
...exampleData.payment,
|
...exampleData.payment,
|
||||||
info: {
|
info: {
|
||||||
@ -154,35 +155,35 @@ export const PaymentWithProducts = dynamic({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const Deposit = dynamic({
|
export const Deposit = createExample(TestedComponent,{
|
||||||
transaction: exampleData.deposit
|
transaction: exampleData.deposit
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DepositPending = dynamic({
|
export const DepositPending = createExample(TestedComponent,{
|
||||||
transaction: { ...exampleData.deposit, pending: true }
|
transaction: { ...exampleData.deposit, pending: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Refresh = dynamic({
|
export const Refresh = createExample(TestedComponent,{
|
||||||
transaction: exampleData.refresh
|
transaction: exampleData.refresh
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Tip = dynamic({
|
export const Tip = createExample(TestedComponent,{
|
||||||
transaction: exampleData.tip
|
transaction: exampleData.tip
|
||||||
});
|
});
|
||||||
|
|
||||||
export const TipPending = dynamic({
|
export const TipPending = createExample(TestedComponent,{
|
||||||
transaction: { ...exampleData.tip, pending: true }
|
transaction: { ...exampleData.tip, pending: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Refund = dynamic({
|
export const Refund = createExample(TestedComponent,{
|
||||||
transaction: exampleData.refund
|
transaction: exampleData.refund
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RefundPending = dynamic({
|
export const RefundPending = createExample(TestedComponent,{
|
||||||
transaction: { ...exampleData.refund, pending: true }
|
transaction: { ...exampleData.refund, pending: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RefundWithProducts = dynamic({
|
export const RefundWithProducts = createExample(TestedComponent,{
|
||||||
transaction: {
|
transaction: {
|
||||||
...exampleData.refund,
|
...exampleData.refund,
|
||||||
info: {
|
info: {
|
327
packages/taler-wallet-webextension/src/popup/Transaction.tsx
Normal file
327
packages/taler-wallet-webextension/src/popup/Transaction.tsx
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
/*
|
||||||
|
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, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { JSX } from "preact";
|
||||||
|
import { route } from 'preact-router';
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import * as wxApi from "../wxApi";
|
||||||
|
import { Pages } from "./popup";
|
||||||
|
|
||||||
|
|
||||||
|
export function TransactionPage({ tid }: { tid: string; }): JSX.Element {
|
||||||
|
const [transaction, setTransaction] = useState<
|
||||||
|
Transaction | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async (): Promise<void> => {
|
||||||
|
const res = await wxApi.getTransactions();
|
||||||
|
const ts = res.transactions.filter(t => t.transactionId === tid);
|
||||||
|
if (ts.length === 1) {
|
||||||
|
setTransaction(ts[0]);
|
||||||
|
} else {
|
||||||
|
route(Pages.history);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <TransactionView
|
||||||
|
transaction={transaction}
|
||||||
|
onDelete={() => wxApi.deleteTransaction(tid).then(_ => history.go(-1))}
|
||||||
|
onBack={() => { history.go(-1); }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletTransactionProps {
|
||||||
|
transaction?: Transaction,
|
||||||
|
onDelete: () => void,
|
||||||
|
onBack: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TransactionView({ transaction, onDelete, onBack }: WalletTransactionProps) {
|
||||||
|
if (!transaction) {
|
||||||
|
return <div><i18n.Translate>Loading ...</i18n.Translate></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Footer() {
|
||||||
|
return <footer style={{ marginTop: 'auto', display: 'flex' }}>
|
||||||
|
<button onClick={onBack}><i18n.Translate>back</i18n.Translate></button>
|
||||||
|
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}>
|
||||||
|
<button onClick={onDelete}><i18n.Translate>remove</i18n.Translate></button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
|
|
||||||
|
function Pending() {
|
||||||
|
if (!transaction?.pending) return null
|
||||||
|
return <span style={{ fontWeight: 'normal', fontSize: 16, color: 'gray' }}>(pending...)</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction.type === TransactionType.Withdrawal) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
||||||
|
<section>
|
||||||
|
<h1>Withdrawal <Pending /></h1>
|
||||||
|
<p>
|
||||||
|
From <b>{transaction.exchangeBaseUrl}</b>
|
||||||
|
</p>
|
||||||
|
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
||||||
|
<tr>
|
||||||
|
<td>Amount subtracted</td>
|
||||||
|
<td>{transaction.amountRaw}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Amount received</td>
|
||||||
|
<td>{transaction.amountEffective}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Exchange fee</td>
|
||||||
|
<td>{Amounts.stringify(
|
||||||
|
Amounts.sub(
|
||||||
|
Amounts.parseOrThrow(transaction.amountRaw),
|
||||||
|
Amounts.parseOrThrow(transaction.amountEffective),
|
||||||
|
).amount
|
||||||
|
)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>When</td>
|
||||||
|
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction.type === TransactionType.Payment) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
||||||
|
<section>
|
||||||
|
<h1>Payment ({transaction.proposalId.substring(0, 10)}...) <Pending /></h1>
|
||||||
|
<p>
|
||||||
|
To <b>{transaction.info.merchant.name}</b>
|
||||||
|
</p>
|
||||||
|
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
||||||
|
<tr>
|
||||||
|
<td>Order id</td>
|
||||||
|
<td>{transaction.info.orderId}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Summary</td>
|
||||||
|
<td>{transaction.info.summary}</td>
|
||||||
|
</tr>
|
||||||
|
{transaction.info.products && transaction.info.products.length > 0 &&
|
||||||
|
<tr>
|
||||||
|
<td>Products</td>
|
||||||
|
<td><ol style={{ margin: 0, textAlign: 'left' }}>
|
||||||
|
{transaction.info.products.map(p =>
|
||||||
|
<li>{p.description}</li>
|
||||||
|
)}</ol></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
<tr>
|
||||||
|
<td>Order amount</td>
|
||||||
|
<td>{transaction.amountRaw}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Order amount and fees</td>
|
||||||
|
<td>{transaction.amountEffective}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Exchange fee</td>
|
||||||
|
<td>{Amounts.stringify(
|
||||||
|
Amounts.sub(
|
||||||
|
Amounts.parseOrThrow(transaction.amountEffective),
|
||||||
|
Amounts.parseOrThrow(transaction.amountRaw),
|
||||||
|
).amount
|
||||||
|
)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>When</td>
|
||||||
|
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction.type === TransactionType.Deposit) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
||||||
|
<section>
|
||||||
|
<h1>Deposit ({transaction.depositGroupId}) <Pending /></h1>
|
||||||
|
<p>
|
||||||
|
To <b>{transaction.targetPaytoUri}</b>
|
||||||
|
</p>
|
||||||
|
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
||||||
|
<tr>
|
||||||
|
<td>Amount deposit</td>
|
||||||
|
<td>{transaction.amountRaw}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Amount deposit and fees</td>
|
||||||
|
<td>{transaction.amountEffective}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Exchange fee</td>
|
||||||
|
<td>{Amounts.stringify(
|
||||||
|
Amounts.sub(
|
||||||
|
Amounts.parseOrThrow(transaction.amountEffective),
|
||||||
|
Amounts.parseOrThrow(transaction.amountRaw),
|
||||||
|
).amount
|
||||||
|
)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>When</td>
|
||||||
|
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction.type === TransactionType.Refresh) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
||||||
|
<section>
|
||||||
|
<h1>Refresh <Pending /></h1>
|
||||||
|
<p>
|
||||||
|
From <b>{transaction.exchangeBaseUrl}</b>
|
||||||
|
</p>
|
||||||
|
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
||||||
|
<tr>
|
||||||
|
<td>Amount refreshed</td>
|
||||||
|
<td>{transaction.amountRaw}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Fees</td>
|
||||||
|
<td>{transaction.amountEffective}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>When</td>
|
||||||
|
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transaction.type === TransactionType.Tip) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
||||||
|
<section>
|
||||||
|
<h1>Tip <Pending /></h1>
|
||||||
|
<p>
|
||||||
|
From <b>{transaction.merchantBaseUrl}</b>
|
||||||
|
</p>
|
||||||
|
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
||||||
|
<tr>
|
||||||
|
<td>Amount deduce</td>
|
||||||
|
<td>{transaction.amountRaw}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Amount received</td>
|
||||||
|
<td>{transaction.amountEffective}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Exchange fee</td>
|
||||||
|
<td>{Amounts.stringify(
|
||||||
|
Amounts.sub(
|
||||||
|
Amounts.parseOrThrow(transaction.amountRaw),
|
||||||
|
Amounts.parseOrThrow(transaction.amountEffective),
|
||||||
|
).amount
|
||||||
|
)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>When</td>
|
||||||
|
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRANSACTION_FROM_REFUND = /[a-z]*:([\w]{10}).*/
|
||||||
|
if (transaction.type === TransactionType.Refund) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
||||||
|
<section>
|
||||||
|
<h1>Refund ({TRANSACTION_FROM_REFUND.exec(transaction.refundedTransactionId)![1]}...) <Pending /></h1>
|
||||||
|
<p>
|
||||||
|
From <b>{transaction.info.merchant.name}</b>
|
||||||
|
</p>
|
||||||
|
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
||||||
|
<tr>
|
||||||
|
<td>Order id</td>
|
||||||
|
<td>{transaction.info.orderId}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Summary</td>
|
||||||
|
<td>{transaction.info.summary}</td>
|
||||||
|
</tr>
|
||||||
|
{transaction.info.products && transaction.info.products.length > 0 &&
|
||||||
|
<tr>
|
||||||
|
<td>Products</td>
|
||||||
|
<td><ol>
|
||||||
|
{transaction.info.products.map(p =>
|
||||||
|
<li>{p.description}</li>
|
||||||
|
)}</ol></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
<tr>
|
||||||
|
<td>Amount deduce</td>
|
||||||
|
<td>{transaction.amountRaw}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Amount received</td>
|
||||||
|
<td>{transaction.amountEffective}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Exchange fee</td>
|
||||||
|
<td>{Amounts.stringify(
|
||||||
|
Amounts.sub(
|
||||||
|
Amounts.parseOrThrow(transaction.amountRaw),
|
||||||
|
Amounts.parseOrThrow(transaction.amountEffective),
|
||||||
|
).amount
|
||||||
|
)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>When</td>
|
||||||
|
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <div></div>
|
||||||
|
}
|
@ -25,29 +25,9 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
AmountJson,
|
classifyTalerUri, i18n, TalerUriType
|
||||||
Amounts,
|
|
||||||
BalancesResponse,
|
|
||||||
Balance,
|
|
||||||
classifyTalerUri,
|
|
||||||
TalerUriType,
|
|
||||||
TransactionsResponse,
|
|
||||||
Transaction,
|
|
||||||
TransactionType,
|
|
||||||
AmountString,
|
|
||||||
Timestamp,
|
|
||||||
amountFractionalBase,
|
|
||||||
i18n,
|
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { format } from "date-fns";
|
import { ComponentChildren, JSX } from "preact";
|
||||||
import { Component, ComponentChildren, Fragment, JSX } from "preact";
|
|
||||||
import { route } from 'preact-router';
|
|
||||||
import { useEffect, useState } from "preact/hooks";
|
|
||||||
import { Diagnostics } from "../components/Diagnostics";
|
|
||||||
import { PermissionsCheckbox } from "../components/PermissionsCheckbox";
|
|
||||||
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
|
|
||||||
import { PageLink, renderAmount } from "../renderHtml";
|
|
||||||
import * as wxApi from "../wxApi";
|
|
||||||
|
|
||||||
export enum Pages {
|
export enum Pages {
|
||||||
balance = '/balance',
|
balance = '/balance',
|
||||||
@ -86,878 +66,3 @@ export function WalletNavBar({ current }: { current?: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Render an amount as a large number with a small currency symbol.
|
|
||||||
*/
|
|
||||||
function bigAmount(amount: AmountJson): JSX.Element {
|
|
||||||
const v = amount.value + amount.fraction / amountFractionalBase;
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
|
|
||||||
<span>{amount.currency}</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyBalanceView(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<p><i18n.Translate>
|
|
||||||
You have no balance to show. Need some{" "}
|
|
||||||
<PageLink pageName="/welcome">help</PageLink> getting started?
|
|
||||||
</i18n.Translate></p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WalletBalanceView extends Component<any, any> {
|
|
||||||
private balance?: BalancesResponse;
|
|
||||||
private gotError = false;
|
|
||||||
private canceler: (() => void) | undefined = undefined;
|
|
||||||
private unmount = false;
|
|
||||||
private updateBalanceRunning = false;
|
|
||||||
|
|
||||||
componentWillMount(): void {
|
|
||||||
this.canceler = wxApi.onUpdateNotification(() => this.updateBalance());
|
|
||||||
this.updateBalance();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
|
||||||
console.log("component WalletBalanceView will unmount");
|
|
||||||
if (this.canceler) {
|
|
||||||
this.canceler();
|
|
||||||
}
|
|
||||||
this.unmount = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateBalance(): Promise<void> {
|
|
||||||
if (this.updateBalanceRunning) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.updateBalanceRunning = true;
|
|
||||||
let balance: BalancesResponse;
|
|
||||||
try {
|
|
||||||
balance = await wxApi.getBalance();
|
|
||||||
} catch (e) {
|
|
||||||
if (this.unmount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.gotError = true;
|
|
||||||
console.error("could not retrieve balances", e);
|
|
||||||
this.setState({});
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
this.updateBalanceRunning = false;
|
|
||||||
}
|
|
||||||
if (this.unmount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.gotError = false;
|
|
||||||
console.log("got balance", balance);
|
|
||||||
this.balance = balance;
|
|
||||||
this.setState({});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatPending(entry: Balance): JSX.Element {
|
|
||||||
let incoming: JSX.Element | undefined;
|
|
||||||
let payment: JSX.Element | undefined;
|
|
||||||
|
|
||||||
const available = Amounts.parseOrThrow(entry.available);
|
|
||||||
const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
|
|
||||||
const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"available: ",
|
|
||||||
entry.pendingIncoming ? renderAmount(entry.available) : null,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"incoming: ",
|
|
||||||
entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!Amounts.isZero(pendingIncoming)) {
|
|
||||||
incoming = (
|
|
||||||
<span><i18n.Translate>
|
|
||||||
<span style={{ color: "darkgreen" }}>
|
|
||||||
{"+"}
|
|
||||||
{renderAmount(entry.pendingIncoming)}
|
|
||||||
</span>{" "}
|
|
||||||
incoming
|
|
||||||
</i18n.Translate></span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const l = [incoming, payment].filter((x) => x !== undefined);
|
|
||||||
if (l.length === 0) {
|
|
||||||
return <span />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (l.length === 1) {
|
|
||||||
return <span>({l})</span>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
({l[0]}, {l[1]})
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
const wallet = this.balance;
|
|
||||||
if (this.gotError) {
|
|
||||||
return (
|
|
||||||
<div className="balance">
|
|
||||||
<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);
|
|
||||||
const listing = wallet.balances.map((entry) => {
|
|
||||||
const av = Amounts.parseOrThrow(entry.available);
|
|
||||||
return (
|
|
||||||
<p key={av.currency}>
|
|
||||||
{bigAmount(av)} {this.formatPending(entry)}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return listing.length > 0 ? (
|
|
||||||
<div className="balance">{listing}</div>
|
|
||||||
) : (
|
|
||||||
<EmptyBalanceView />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransactionAmountProps {
|
|
||||||
debitCreditIndicator: "debit" | "credit" | "unknown";
|
|
||||||
amount: AmountString | "unknown";
|
|
||||||
pending: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TransactionAmount(props: TransactionAmountProps): JSX.Element {
|
|
||||||
const [currency, amount] = props.amount.split(":");
|
|
||||||
let sign: string;
|
|
||||||
switch (props.debitCreditIndicator) {
|
|
||||||
case "credit":
|
|
||||||
sign = "+";
|
|
||||||
break;
|
|
||||||
case "debit":
|
|
||||||
sign = "-";
|
|
||||||
break;
|
|
||||||
case "unknown":
|
|
||||||
sign = "";
|
|
||||||
}
|
|
||||||
const style: JSX.AllCSSProperties = {
|
|
||||||
marginLeft: "auto",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
alignSelf: "center"
|
|
||||||
};
|
|
||||||
if (props.pending) {
|
|
||||||
style.color = "gray";
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div style={{ ...style }}>
|
|
||||||
<div style={{ fontSize: "x-large" }}>
|
|
||||||
{sign}
|
|
||||||
{amount}
|
|
||||||
</div>
|
|
||||||
<div>{currency}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransactionLayoutProps {
|
|
||||||
debitCreditIndicator: "debit" | "credit" | "unknown";
|
|
||||||
amount: AmountString | "unknown";
|
|
||||||
timestamp: Timestamp;
|
|
||||||
title: string;
|
|
||||||
id: string;
|
|
||||||
subtitle: string;
|
|
||||||
iconPath: string;
|
|
||||||
pending: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TransactionLayout(props: TransactionLayoutProps): JSX.Element {
|
|
||||||
const date = new Date(props.timestamp.t_ms);
|
|
||||||
const dateStr = date.toLocaleString([], {
|
|
||||||
dateStyle: "medium",
|
|
||||||
timeStyle: "short",
|
|
||||||
} as any);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
border: "1px solid gray",
|
|
||||||
borderRadius: "0.5em",
|
|
||||||
margin: "0.5em 0",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
padding: "0.5em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={props.iconPath} />
|
|
||||||
<div
|
|
||||||
style={{ display: "flex", flexDirection: "column", marginLeft: "1em" }}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: "small", color: "gray" }}>{dateStr}</div>
|
|
||||||
<div style={{ fontVariant: "small-caps", fontSize: "x-large" }}>
|
|
||||||
<a href={Pages.transaction.replace(':tid', props.id)}><span>{props.title}</span></a>
|
|
||||||
{props.pending ? (
|
|
||||||
<span style={{ color: "darkblue" }}> (Pending)</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>{props.subtitle}</div>
|
|
||||||
</div>
|
|
||||||
<TransactionAmount
|
|
||||||
pending={props.pending}
|
|
||||||
amount={props.amount}
|
|
||||||
debitCreditIndicator={props.debitCreditIndicator}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TransactionItem(props: { tx: Transaction }): JSX.Element {
|
|
||||||
const tx = props.tx;
|
|
||||||
switch (tx.type) {
|
|
||||||
case TransactionType.Withdrawal:
|
|
||||||
return (
|
|
||||||
<TransactionLayout
|
|
||||||
id={tx.transactionId}
|
|
||||||
amount={tx.amountEffective}
|
|
||||||
debitCreditIndicator={"credit"}
|
|
||||||
title="Withdrawal"
|
|
||||||
subtitle={`via ${tx.exchangeBaseUrl}`}
|
|
||||||
timestamp={tx.timestamp}
|
|
||||||
iconPath="/static/img/ri-bank-line.svg"
|
|
||||||
pending={tx.pending}
|
|
||||||
></TransactionLayout>
|
|
||||||
);
|
|
||||||
case TransactionType.Payment:
|
|
||||||
return (
|
|
||||||
<TransactionLayout
|
|
||||||
id={tx.transactionId}
|
|
||||||
amount={tx.amountEffective}
|
|
||||||
debitCreditIndicator={"debit"}
|
|
||||||
title="Payment"
|
|
||||||
subtitle={tx.info.summary}
|
|
||||||
timestamp={tx.timestamp}
|
|
||||||
iconPath="/static/img/ri-shopping-cart-line.svg"
|
|
||||||
pending={tx.pending}
|
|
||||||
></TransactionLayout>
|
|
||||||
);
|
|
||||||
case TransactionType.Refund:
|
|
||||||
return (
|
|
||||||
<TransactionLayout
|
|
||||||
id={tx.transactionId}
|
|
||||||
amount={tx.amountEffective}
|
|
||||||
debitCreditIndicator={"credit"}
|
|
||||||
title="Refund"
|
|
||||||
subtitle={tx.info.summary}
|
|
||||||
timestamp={tx.timestamp}
|
|
||||||
iconPath="/static/img/ri-refund-2-line.svg"
|
|
||||||
pending={tx.pending}
|
|
||||||
></TransactionLayout>
|
|
||||||
);
|
|
||||||
case TransactionType.Tip:
|
|
||||||
return (
|
|
||||||
<TransactionLayout
|
|
||||||
id={tx.transactionId}
|
|
||||||
amount={tx.amountEffective}
|
|
||||||
debitCreditIndicator={"credit"}
|
|
||||||
title="Tip"
|
|
||||||
subtitle={`from ${new URL(tx.merchantBaseUrl).hostname}`}
|
|
||||||
timestamp={tx.timestamp}
|
|
||||||
iconPath="/static/img/ri-hand-heart-line.svg"
|
|
||||||
pending={tx.pending}
|
|
||||||
></TransactionLayout>
|
|
||||||
);
|
|
||||||
case TransactionType.Refresh:
|
|
||||||
return (
|
|
||||||
<TransactionLayout
|
|
||||||
id={tx.transactionId}
|
|
||||||
amount={tx.amountEffective}
|
|
||||||
debitCreditIndicator={"credit"}
|
|
||||||
title="Refresh"
|
|
||||||
subtitle={`via exchange ${tx.exchangeBaseUrl}`}
|
|
||||||
timestamp={tx.timestamp}
|
|
||||||
iconPath="/static/img/ri-refresh-line.svg"
|
|
||||||
pending={tx.pending}
|
|
||||||
></TransactionLayout>
|
|
||||||
);
|
|
||||||
case TransactionType.Deposit:
|
|
||||||
return (
|
|
||||||
<TransactionLayout
|
|
||||||
id={tx.transactionId}
|
|
||||||
amount={tx.amountEffective}
|
|
||||||
debitCreditIndicator={"debit"}
|
|
||||||
title="Refresh"
|
|
||||||
subtitle={`to ${tx.targetPaytoUri}`}
|
|
||||||
timestamp={tx.timestamp}
|
|
||||||
iconPath="/static/img/ri-refresh-line.svg"
|
|
||||||
pending={tx.pending}
|
|
||||||
></TransactionLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WalletHistory(props: any): JSX.Element {
|
|
||||||
const [transactions, setTransactions] = useState<
|
|
||||||
TransactionsResponse | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async (): Promise<void> => {
|
|
||||||
const res = await wxApi.getTransactions();
|
|
||||||
setTransactions(res);
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!transactions) {
|
|
||||||
return <div>Loading ...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const txs = [...transactions.transactions].reverse();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{txs.map((tx, i) => (
|
|
||||||
<TransactionItem key={i} tx={tx} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WalletTransactionProps {
|
|
||||||
transaction?: Transaction,
|
|
||||||
onDelete: () => void,
|
|
||||||
onBack: () => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WalletTransactionView({ transaction, onDelete, onBack }: WalletTransactionProps) {
|
|
||||||
if (!transaction) {
|
|
||||||
return <div><i18n.Translate>Loading ...</i18n.Translate></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Footer() {
|
|
||||||
return <footer style={{ marginTop: 'auto', display: 'flex' }}>
|
|
||||||
<button onClick={onBack}><i18n.Translate>back</i18n.Translate></button>
|
|
||||||
<div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}>
|
|
||||||
<button onClick={onDelete}><i18n.Translate>remove</i18n.Translate></button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
}
|
|
||||||
|
|
||||||
function Pending() {
|
|
||||||
if (!transaction?.pending) return null
|
|
||||||
return <span style={{ fontWeight: 'normal', fontSize: 16, color: 'gray' }}>(pending...)</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transaction.type === TransactionType.Withdrawal) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
|
||||||
<section>
|
|
||||||
<h1>Withdrawal <Pending /></h1>
|
|
||||||
<p>
|
|
||||||
From <b>{transaction.exchangeBaseUrl}</b>
|
|
||||||
</p>
|
|
||||||
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
|
||||||
<tr>
|
|
||||||
<td>Amount subtracted</td>
|
|
||||||
<td>{transaction.amountRaw}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Amount received</td>
|
|
||||||
<td>{transaction.amountEffective}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Exchange fee</td>
|
|
||||||
<td>{Amounts.stringify(
|
|
||||||
Amounts.sub(
|
|
||||||
Amounts.parseOrThrow(transaction.amountRaw),
|
|
||||||
Amounts.parseOrThrow(transaction.amountEffective),
|
|
||||||
).amount
|
|
||||||
)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>When</td>
|
|
||||||
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transaction.type === TransactionType.Payment) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
|
||||||
<section>
|
|
||||||
<h1>Payment ({transaction.proposalId.substring(0, 10)}...) <Pending /></h1>
|
|
||||||
<p>
|
|
||||||
To <b>{transaction.info.merchant.name}</b>
|
|
||||||
</p>
|
|
||||||
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
|
||||||
<tr>
|
|
||||||
<td>Order id</td>
|
|
||||||
<td>{transaction.info.orderId}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Summary</td>
|
|
||||||
<td>{transaction.info.summary}</td>
|
|
||||||
</tr>
|
|
||||||
{transaction.info.products && transaction.info.products.length > 0 &&
|
|
||||||
<tr>
|
|
||||||
<td>Products</td>
|
|
||||||
<td><ol style={{ margin: 0, textAlign: 'left' }}>
|
|
||||||
{transaction.info.products.map(p =>
|
|
||||||
<li>{p.description}</li>
|
|
||||||
)}</ol></td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
<tr>
|
|
||||||
<td>Order amount</td>
|
|
||||||
<td>{transaction.amountRaw}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Order amount and fees</td>
|
|
||||||
<td>{transaction.amountEffective}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Exchange fee</td>
|
|
||||||
<td>{Amounts.stringify(
|
|
||||||
Amounts.sub(
|
|
||||||
Amounts.parseOrThrow(transaction.amountEffective),
|
|
||||||
Amounts.parseOrThrow(transaction.amountRaw),
|
|
||||||
).amount
|
|
||||||
)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>When</td>
|
|
||||||
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transaction.type === TransactionType.Deposit) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
|
||||||
<section>
|
|
||||||
<h1>Deposit ({transaction.depositGroupId}) <Pending /></h1>
|
|
||||||
<p>
|
|
||||||
To <b>{transaction.targetPaytoUri}</b>
|
|
||||||
</p>
|
|
||||||
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
|
||||||
<tr>
|
|
||||||
<td>Amount deposit</td>
|
|
||||||
<td>{transaction.amountRaw}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Amount deposit and fees</td>
|
|
||||||
<td>{transaction.amountEffective}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Exchange fee</td>
|
|
||||||
<td>{Amounts.stringify(
|
|
||||||
Amounts.sub(
|
|
||||||
Amounts.parseOrThrow(transaction.amountEffective),
|
|
||||||
Amounts.parseOrThrow(transaction.amountRaw),
|
|
||||||
).amount
|
|
||||||
)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>When</td>
|
|
||||||
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transaction.type === TransactionType.Refresh) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
|
||||||
<section>
|
|
||||||
<h1>Refresh <Pending /></h1>
|
|
||||||
<p>
|
|
||||||
From <b>{transaction.exchangeBaseUrl}</b>
|
|
||||||
</p>
|
|
||||||
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
|
||||||
<tr>
|
|
||||||
<td>Amount refreshed</td>
|
|
||||||
<td>{transaction.amountRaw}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Fees</td>
|
|
||||||
<td>{transaction.amountEffective}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>When</td>
|
|
||||||
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transaction.type === TransactionType.Tip) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
|
||||||
<section>
|
|
||||||
<h1>Tip <Pending /></h1>
|
|
||||||
<p>
|
|
||||||
From <b>{transaction.merchantBaseUrl}</b>
|
|
||||||
</p>
|
|
||||||
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
|
||||||
<tr>
|
|
||||||
<td>Amount deduce</td>
|
|
||||||
<td>{transaction.amountRaw}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Amount received</td>
|
|
||||||
<td>{transaction.amountEffective}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Exchange fee</td>
|
|
||||||
<td>{Amounts.stringify(
|
|
||||||
Amounts.sub(
|
|
||||||
Amounts.parseOrThrow(transaction.amountRaw),
|
|
||||||
Amounts.parseOrThrow(transaction.amountEffective),
|
|
||||||
).amount
|
|
||||||
)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>When</td>
|
|
||||||
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TRANSACTION_FROM_REFUND = /[a-z]*:([\w]{10}).*/
|
|
||||||
if (transaction.type === TransactionType.Refund) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '20rem' }} >
|
|
||||||
<section>
|
|
||||||
<h1>Refund ({TRANSACTION_FROM_REFUND.exec(transaction.refundedTransactionId)![1]}...) <Pending /></h1>
|
|
||||||
<p>
|
|
||||||
From <b>{transaction.info.merchant.name}</b>
|
|
||||||
</p>
|
|
||||||
<table class={transaction.pending ? "detailsTable pending" : "detailsTable"}>
|
|
||||||
<tr>
|
|
||||||
<td>Order id</td>
|
|
||||||
<td>{transaction.info.orderId}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Summary</td>
|
|
||||||
<td>{transaction.info.summary}</td>
|
|
||||||
</tr>
|
|
||||||
{transaction.info.products && transaction.info.products.length > 0 &&
|
|
||||||
<tr>
|
|
||||||
<td>Products</td>
|
|
||||||
<td><ol>
|
|
||||||
{transaction.info.products.map(p =>
|
|
||||||
<li>{p.description}</li>
|
|
||||||
)}</ol></td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
<tr>
|
|
||||||
<td>Amount deduce</td>
|
|
||||||
<td>{transaction.amountRaw}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Amount received</td>
|
|
||||||
<td>{transaction.amountEffective}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Exchange fee</td>
|
|
||||||
<td>{Amounts.stringify(
|
|
||||||
Amounts.sub(
|
|
||||||
Amounts.parseOrThrow(transaction.amountRaw),
|
|
||||||
Amounts.parseOrThrow(transaction.amountEffective),
|
|
||||||
).amount
|
|
||||||
)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>When</td>
|
|
||||||
<td>{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return <div></div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WalletTransaction({ tid }: { tid: string }): JSX.Element {
|
|
||||||
const [transaction, setTransaction] = useState<
|
|
||||||
Transaction | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async (): Promise<void> => {
|
|
||||||
const res = await wxApi.getTransactions();
|
|
||||||
const ts = res.transactions.filter(t => t.transactionId === tid)
|
|
||||||
if (ts.length === 1) {
|
|
||||||
setTransaction(ts[0]);
|
|
||||||
} else {
|
|
||||||
route(Pages.history)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <WalletTransactionView
|
|
||||||
transaction={transaction}
|
|
||||||
onDelete={() => wxApi.deleteTransaction(tid).then(_ => history.go(-1))}
|
|
||||||
onBack={() => { history.go(-1) }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WalletSettings() {
|
|
||||||
const [permissionsEnabled, togglePermissions] = useExtendedPermissions()
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Permissions</h2>
|
|
||||||
<PermissionsCheckbox enabled={permissionsEnabled} onToggle={togglePermissions} />
|
|
||||||
{/*
|
|
||||||
<h2>Developer mode</h2>
|
|
||||||
<DebugCheckbox enabled={permissionsEnabled} onToggle={togglePermissions} />
|
|
||||||
*/}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function DebugCheckbox({ enabled, onToggle }: { enabled: boolean, onToggle: () => void }): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
checked={enabled}
|
|
||||||
onClick={onToggle}
|
|
||||||
type="checkbox"
|
|
||||||
id="checkbox-perm"
|
|
||||||
style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="checkbox-perm"
|
|
||||||
style={{ marginLeft: "0.5em", fontWeight: "bold" }}
|
|
||||||
>
|
|
||||||
Automatically open wallet based on page content
|
|
||||||
</label>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
color: "#383838",
|
|
||||||
fontSize: "smaller",
|
|
||||||
display: "block",
|
|
||||||
marginLeft: "2em",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
(Enabling this option below will make using the wallet faster, but
|
|
||||||
requires more permissions from your browser.)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload(): void {
|
|
||||||
try {
|
|
||||||
chrome.runtime.reload();
|
|
||||||
window.close();
|
|
||||||
} catch (e) {
|
|
||||||
// Functionality missing in firefox, ignore!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmReset(): Promise<void> {
|
|
||||||
if (
|
|
||||||
confirm(
|
|
||||||
"Do you want to IRREVOCABLY DESTROY everything inside your" +
|
|
||||||
" wallet and LOSE ALL YOUR COINS?",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
await wxApi.resetDb();
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WalletDebug(props: any): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>Debug tools:</p>
|
|
||||||
<button onClick={openExtensionPage("/static/popup.html")}>wallet tab</button>
|
|
||||||
<br />
|
|
||||||
<button onClick={confirmReset}>reset</button>
|
|
||||||
<button onClick={reload}>reload chrome extension</button>
|
|
||||||
<Diagnostics />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openExtensionPage(page: string) {
|
|
||||||
return () => {
|
|
||||||
chrome.tabs.create({
|
|
||||||
url: chrome.extension.getURL(page),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// function openTab(page: string) {
|
|
||||||
// return (evt: React.SyntheticEvent<any>) => {
|
|
||||||
// evt.preventDefault();
|
|
||||||
// chrome.tabs.create({
|
|
||||||
// url: page,
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
function makeExtensionUrlWithParams(
|
|
||||||
url: string,
|
|
||||||
params?: { [name: string]: string | undefined },
|
|
||||||
): string {
|
|
||||||
const innerUrl = new URL(chrome.extension.getURL("/" + url));
|
|
||||||
if (params) {
|
|
||||||
for (const key in params) {
|
|
||||||
const p = params[key];
|
|
||||||
if (p) {
|
|
||||||
innerUrl.searchParams.set(key, p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return innerUrl.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function actionForTalerUri(talerUri: string): string | undefined {
|
|
||||||
const uriType = classifyTalerUri(talerUri);
|
|
||||||
switch (uriType) {
|
|
||||||
case TalerUriType.TalerWithdraw:
|
|
||||||
return makeExtensionUrlWithParams("static/wallet.html#/withdraw", {
|
|
||||||
talerWithdrawUri: talerUri,
|
|
||||||
});
|
|
||||||
case TalerUriType.TalerPay:
|
|
||||||
return makeExtensionUrlWithParams("static/wallet.html#/pay", {
|
|
||||||
talerPayUri: talerUri,
|
|
||||||
});
|
|
||||||
case TalerUriType.TalerTip:
|
|
||||||
return makeExtensionUrlWithParams("static/wallet.html#/tip", {
|
|
||||||
talerTipUri: talerUri,
|
|
||||||
});
|
|
||||||
case TalerUriType.TalerRefund:
|
|
||||||
return makeExtensionUrlWithParams("static/wallet.html#/refund", {
|
|
||||||
talerRefundUri: talerUri,
|
|
||||||
});
|
|
||||||
case TalerUriType.TalerNotifyReserve:
|
|
||||||
// FIXME: implement
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn(
|
|
||||||
"Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function findTalerUriInActiveTab(): Promise<string | undefined> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
chrome.tabs.executeScript(
|
|
||||||
{
|
|
||||||
code: `
|
|
||||||
(() => {
|
|
||||||
let x = document.querySelector("a[href^='taler://'") || document.querySelector("a[href^='taler+http://'");
|
|
||||||
return x ? x.href.toString() : null;
|
|
||||||
})();
|
|
||||||
`,
|
|
||||||
allFrames: false,
|
|
||||||
},
|
|
||||||
(result) => {
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
console.error(chrome.runtime.lastError);
|
|
||||||
resolve(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("got result", result);
|
|
||||||
resolve(result[0]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// export function WalletPopup(): JSX.Element {
|
|
||||||
// const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>(
|
|
||||||
// undefined,
|
|
||||||
// );
|
|
||||||
// const [dismissed, setDismissed] = useState(false);
|
|
||||||
// useEffect(() => {
|
|
||||||
// async function check(): Promise<void> {
|
|
||||||
// const talerUri = await findTalerUriInActiveTab();
|
|
||||||
// if (talerUri) {
|
|
||||||
// const actionUrl = actionForTalerUri(talerUri);
|
|
||||||
// setTalerActionUrl(actionUrl);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// check();
|
|
||||||
// }, []);
|
|
||||||
// if (talerActionUrl && !dismissed) {
|
|
||||||
// return (
|
|
||||||
// <div style={{ padding: "1em", width: 400 }}>
|
|
||||||
// <h1>Taler Action</h1>
|
|
||||||
// <p>This page has a Taler action. </p>
|
|
||||||
// <p>
|
|
||||||
// <button
|
|
||||||
// onClick={() => {
|
|
||||||
// window.open(talerActionUrl, "_blank");
|
|
||||||
// }}
|
|
||||||
// >
|
|
||||||
// Open
|
|
||||||
// </button>
|
|
||||||
// </p>
|
|
||||||
// <p>
|
|
||||||
// <button onClick={() => setDismissed(true)}>Dismiss</button>
|
|
||||||
// </p>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// return (
|
|
||||||
// <div>
|
|
||||||
// <Match>{({ path }: any) => <WalletNavBar current={path} />}</Match>
|
|
||||||
// <div style={{ margin: "1em", width: 400 }}>
|
|
||||||
// <Router>
|
|
||||||
// <Route path={Pages.balance} component={WalletBalanceView} />
|
|
||||||
// <Route path={Pages.settings} component={WalletSettings} />
|
|
||||||
// <Route path={Pages.debug} component={WalletDebug} />
|
|
||||||
// <Route path={Pages.history} component={WalletHistory} />
|
|
||||||
// <Route path={Pages.transaction} component={WalletTransaction} />
|
|
||||||
// </Router>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
@ -23,13 +23,17 @@
|
|||||||
import { render } from "preact";
|
import { render } from "preact";
|
||||||
import { setupI18n } from "@gnu-taler/taler-util";
|
import { setupI18n } from "@gnu-taler/taler-util";
|
||||||
import { strings } from "./i18n/strings";
|
import { strings } from "./i18n/strings";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import {
|
import {
|
||||||
actionForTalerUri, findTalerUriInActiveTab, Pages, WalletBalanceView, WalletDebug, WalletHistory,
|
Pages, WalletNavBar} from "./popup/popup";
|
||||||
WalletNavBar, WalletSettings, WalletTransaction, WalletTransactionView
|
import { HistoryPage } from "./popup/History";
|
||||||
} from "./popup/popup";
|
import { DebugPage } from "./popup/Debug";
|
||||||
|
import { SettingsPage } from "./popup/Settings";
|
||||||
|
import { TransactionPage } from "./popup/Transaction";
|
||||||
|
import { BalancePage } from "./popup/Balance";
|
||||||
import Match from "preact-router/match";
|
import Match from "preact-router/match";
|
||||||
import Router, { route, Route } from "preact-router";
|
import Router, { route, Route } from "preact-router";
|
||||||
|
import { useTalerActionURL } from "./hooks/useTalerActionURL";
|
||||||
// import { Application } from "./Application";
|
// import { Application } from "./Application";
|
||||||
|
|
||||||
function main(): void {
|
function main(): void {
|
||||||
@ -53,25 +57,6 @@ if (document.readyState === "loading") {
|
|||||||
main();
|
main();
|
||||||
}
|
}
|
||||||
|
|
||||||
function useTalerActionURL(): [string | undefined, (s: boolean) => void] {
|
|
||||||
const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
const [dismissed, setDismissed] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
async function check(): Promise<void> {
|
|
||||||
const talerUri = await findTalerUriInActiveTab();
|
|
||||||
if (talerUri) {
|
|
||||||
const actionUrl = actionForTalerUri(talerUri);
|
|
||||||
setTalerActionUrl(actionUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check();
|
|
||||||
}, []);
|
|
||||||
const url = dismissed ? undefined : talerActionUrl
|
|
||||||
return [url, setDismissed]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string;
|
url: string;
|
||||||
onDismiss: (s: boolean) => void;
|
onDismiss: (s: boolean) => void;
|
||||||
@ -105,11 +90,11 @@ function Application() {
|
|||||||
<Match>{({ path }: any) => <WalletNavBar current={path} />}</Match >
|
<Match>{({ path }: any) => <WalletNavBar current={path} />}</Match >
|
||||||
<div style={{ margin: "1em", width: 400 }}>
|
<div style={{ margin: "1em", width: 400 }}>
|
||||||
<Router>
|
<Router>
|
||||||
<Route path={Pages.balance} component={WalletBalanceView} />
|
<Route path={Pages.balance} component={BalancePage} />
|
||||||
<Route path={Pages.settings} component={WalletSettings} />
|
<Route path={Pages.settings} component={SettingsPage} />
|
||||||
<Route path={Pages.debug} component={WalletDebug} />
|
<Route path={Pages.debug} component={DebugPage} />
|
||||||
<Route path={Pages.history} component={WalletHistory} />
|
<Route path={Pages.history} component={HistoryPage} />
|
||||||
<Route path={Pages.transaction} component={WalletTransaction} />
|
<Route path={Pages.transaction} component={TransactionPage} />
|
||||||
<Route default component={Redirect} to={Pages.balance} />
|
<Route default component={Redirect} to={Pages.balance} />
|
||||||
</Router>
|
</Router>
|
||||||
</div>
|
</div>
|
||||||
@ -118,8 +103,6 @@ function Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Redirect({ to }: { to: string }): null {
|
function Redirect({ to }: { to: string }): null {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
route(to, true)
|
route(to, true)
|
||||||
|
@ -45,7 +45,7 @@ interface Props {
|
|||||||
talerPayUri?: string
|
talerPayUri?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TalerPayDialog({ talerPayUri }: Props): JSX.Element {
|
export function PayPage({ talerPayUri }: Props): JSX.Element {
|
||||||
const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(undefined);
|
const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(undefined);
|
||||||
const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(undefined);
|
const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(undefined);
|
||||||
const [payErrMsg, setPayErrMsg] = useState<string | undefined>("");
|
const [payErrMsg, setPayErrMsg] = useState<string | undefined>("");
|
||||||
@ -222,14 +222,3 @@ export function TalerPayDialog({ talerPayUri }: Props): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated to be removed
|
|
||||||
*/
|
|
||||||
export function createPayPage(): JSX.Element {
|
|
||||||
const url = new URL(document.location.href);
|
|
||||||
const talerPayUri = url.searchParams.get("talerPayUri");
|
|
||||||
if (!talerPayUri) {
|
|
||||||
throw Error("invalid parameter");
|
|
||||||
}
|
|
||||||
return <TalerPayDialog talerPayUri={talerPayUri} />;
|
|
||||||
}
|
|
@ -33,7 +33,7 @@ interface Props {
|
|||||||
talerRefundUri?: string
|
talerRefundUri?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RefundStatusView({ talerRefundUri }: Props): JSX.Element {
|
export function RefundPage({ talerRefundUri }: Props): JSX.Element {
|
||||||
const [applyResult, setApplyResult] = useState<ApplyRefundResponse | undefined>(undefined);
|
const [applyResult, setApplyResult] = useState<ApplyRefundResponse | undefined>(undefined);
|
||||||
const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
|
const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
@ -87,22 +87,3 @@ export function RefundStatusView({ talerRefundUri }: Props): JSX.Element {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated to be removed
|
|
||||||
*/
|
|
||||||
export function createRefundPage(): JSX.Element {
|
|
||||||
const url = new URL(document.location.href);
|
|
||||||
|
|
||||||
const container = document.getElementById("container");
|
|
||||||
if (!container) {
|
|
||||||
throw Error("fatal: can't mount component, container missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
const talerRefundUri = url.searchParams.get("talerRefundUri");
|
|
||||||
if (!talerRefundUri) {
|
|
||||||
throw Error("taler refund URI required");
|
|
||||||
}
|
|
||||||
|
|
||||||
return <RefundStatusView talerRefundUri={talerRefundUri} />;
|
|
||||||
}
|
|
@ -30,7 +30,7 @@ interface Props {
|
|||||||
talerTipUri?: string
|
talerTipUri?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TalerTipDialog({ talerTipUri }: Props): JSX.Element {
|
export function TipPage({ talerTipUri }: Props): JSX.Element {
|
||||||
const [updateCounter, setUpdateCounter] = useState<number>(0);
|
const [updateCounter, setUpdateCounter] = useState<number>(0);
|
||||||
const [prepareTipResult, setPrepareTipResult] = useState<
|
const [prepareTipResult, setPrepareTipResult] = useState<
|
||||||
PrepareTipResult | undefined
|
PrepareTipResult | undefined
|
||||||
@ -95,15 +95,3 @@ export function TalerTipDialog({ talerTipUri }: Props): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated to be removed
|
|
||||||
*/
|
|
||||||
export function createTipPage(): JSX.Element {
|
|
||||||
const url = new URL(document.location.href);
|
|
||||||
const talerTipUri = url.searchParams.get("talerTipUri");
|
|
||||||
if (!talerTipUri) {
|
|
||||||
throw Error("invalid parameter");
|
|
||||||
}
|
|
||||||
return <TalerTipDialog talerTipUri={talerTipUri} />;
|
|
||||||
}
|
|
@ -20,43 +20,12 @@
|
|||||||
* @author Florian Dold
|
* @author Florian Dold
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as wxApi from "../wxApi";
|
import { JSX } from "preact/jsx-runtime";
|
||||||
import { getPermissionsApi } from "../compat";
|
|
||||||
import { extendedPermissions } from "../permissions";
|
|
||||||
import { Fragment, JSX } from "preact/jsx-runtime";
|
|
||||||
import { PermissionsCheckbox } from "../components/PermissionsCheckbox";
|
import { PermissionsCheckbox } from "../components/PermissionsCheckbox";
|
||||||
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
|
import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
|
||||||
import { Diagnostics } from "../components/Diagnostics";
|
import { Diagnostics } from "../components/Diagnostics";
|
||||||
|
|
||||||
export async function handleExtendedPerm(isEnabled: boolean): Promise<boolean> {
|
export function WelcomePage(): JSX.Element {
|
||||||
let nextVal: boolean | undefined;
|
|
||||||
|
|
||||||
if (!isEnabled) {
|
|
||||||
const granted = await new Promise<boolean>((resolve, reject) => {
|
|
||||||
// We set permissions here, since apparently FF wants this to be done
|
|
||||||
// as the result of an input event ...
|
|
||||||
getPermissionsApi().request(extendedPermissions, (granted: boolean) => {
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
console.error("error requesting permissions");
|
|
||||||
console.error(chrome.runtime.lastError);
|
|
||||||
reject(chrome.runtime.lastError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("permissions granted:", granted);
|
|
||||||
resolve(granted);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const res = await wxApi.setExtendedPermissions(granted);
|
|
||||||
nextVal = res.newValue;
|
|
||||||
} else {
|
|
||||||
const res = await wxApi.setExtendedPermissions(false);
|
|
||||||
nextVal = res.newValue;
|
|
||||||
}
|
|
||||||
console.log("new permissions applied:", nextVal ?? false);
|
|
||||||
return nextVal ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Welcome(): JSX.Element {
|
|
||||||
const [permissionsEnabled, togglePermissions] = useExtendedPermissions()
|
const [permissionsEnabled, togglePermissions] = useExtendedPermissions()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -74,10 +43,3 @@ export function Welcome(): JSX.Element {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated to be removed
|
|
||||||
*/
|
|
||||||
export function createWelcomePage(): JSX.Element {
|
|
||||||
return <Welcome />;
|
|
||||||
}
|
|
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { View, ViewProps } from './withdraw';
|
import { View, ViewProps } from './Withdraw';
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
@ -111,7 +111,7 @@ export function View({ talerWithdrawUri, details, cancelled, selectedExchange, a
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WithdrawalDialog({ talerWithdrawUri }: Props): JSX.Element {
|
export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element {
|
||||||
const [details, setDetails] = useState<WithdrawUriInfoResponse | undefined>(undefined);
|
const [details, setDetails] = useState<WithdrawUriInfoResponse | undefined>(undefined);
|
||||||
const [selectedExchange, setSelectedExchange] = useState<
|
const [selectedExchange, setSelectedExchange] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
@ -159,15 +159,3 @@ export function WithdrawalDialog({ talerWithdrawUri }: Props): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated to be removed
|
|
||||||
*/
|
|
||||||
export function createWithdrawPage(): JSX.Element {
|
|
||||||
const url = new URL(document.location.href);
|
|
||||||
const talerWithdrawUri = url.searchParams.get("talerWithdrawUri");
|
|
||||||
if (!talerWithdrawUri) {
|
|
||||||
throw Error("withdraw URI required");
|
|
||||||
}
|
|
||||||
return <WithdrawalDialog talerWithdrawUri={talerWithdrawUri} />;
|
|
||||||
}
|
|
@ -25,11 +25,11 @@ import { setupI18n } from "@gnu-taler/taler-util";
|
|||||||
import { strings } from "./i18n/strings";
|
import { strings } from "./i18n/strings";
|
||||||
import { createHashHistory } from 'history';
|
import { createHashHistory } from 'history';
|
||||||
|
|
||||||
import { WithdrawalDialog } from "./wallet/withdraw";
|
import { WithdrawPage } from "./wallet/Withdraw";
|
||||||
import { Welcome } from "./wallet/welcome";
|
import { WelcomePage } from "./wallet/Welcome";
|
||||||
import { TalerPayDialog } from "./wallet/pay";
|
import { PayPage } from "./wallet/Pay";
|
||||||
import { RefundStatusView } from "./wallet/refund";
|
import { RefundPage } from "./wallet/Refund";
|
||||||
import { TalerTipDialog } from './wallet/tip';
|
import { TipPage } from './wallet/Tip';
|
||||||
import Router, { route, Route } from "preact-router";
|
import Router, { route, Route } from "preact-router";
|
||||||
|
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ function Application() {
|
|||||||
</div>
|
</div>
|
||||||
<h1>Browser Extension Installed!</h1>
|
<h1>Browser Extension Installed!</h1>
|
||||||
<div>
|
<div>
|
||||||
<Welcome />
|
<WelcomePage />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}} />
|
}} />
|
||||||
@ -91,7 +91,7 @@ function Application() {
|
|||||||
return <section id="main">
|
return <section id="main">
|
||||||
<h1>GNU Taler Wallet</h1>
|
<h1>GNU Taler Wallet</h1>
|
||||||
<article class="fade">
|
<article class="fade">
|
||||||
<TalerPayDialog talerPayUri={queryParams.talerPayUri} />
|
<PayPage talerPayUri={queryParams.talerPayUri} />
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
}} />
|
}} />
|
||||||
@ -100,7 +100,7 @@ function Application() {
|
|||||||
return <section id="main">
|
return <section id="main">
|
||||||
<h1>GNU Taler Wallet</h1>
|
<h1>GNU Taler Wallet</h1>
|
||||||
<article class="fade">
|
<article class="fade">
|
||||||
<RefundStatusView talerRefundUri={queryParams.talerRefundUri} />
|
<RefundPage talerRefundUri={queryParams.talerRefundUri} />
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
}} />
|
}} />
|
||||||
@ -109,7 +109,7 @@ function Application() {
|
|||||||
return <section id="main">
|
return <section id="main">
|
||||||
<h1>GNU Taler Wallet</h1>
|
<h1>GNU Taler Wallet</h1>
|
||||||
<div>
|
<div>
|
||||||
<TalerTipDialog talerTipUri={queryParams.talerTipUri} />
|
<TipPage talerTipUri={queryParams.talerTipUri} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}} />
|
}} />
|
||||||
@ -121,7 +121,7 @@ function Application() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="fade">
|
<div class="fade">
|
||||||
<WithdrawalDialog talerWithdrawUri={queryParams.talerWithdrawUri} />
|
<WithdrawPage talerWithdrawUri={queryParams.talerWithdrawUri} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}} />
|
}} />
|
||||||
|
Loading…
Reference in New Issue
Block a user