summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/pages')
-rw-r--r--packages/taler-wallet-webextension/src/pages/pay.tsx180
-rw-r--r--packages/taler-wallet-webextension/src/pages/payback.tsx30
-rw-r--r--packages/taler-wallet-webextension/src/pages/popup.tsx502
-rw-r--r--packages/taler-wallet-webextension/src/pages/refund.tsx89
-rw-r--r--packages/taler-wallet-webextension/src/pages/reset-required.tsx93
-rw-r--r--packages/taler-wallet-webextension/src/pages/return-coins.tsx30
-rw-r--r--packages/taler-wallet-webextension/src/pages/tip.tsx103
-rw-r--r--packages/taler-wallet-webextension/src/pages/welcome.tsx190
-rw-r--r--packages/taler-wallet-webextension/src/pages/withdraw.tsx229
9 files changed, 1446 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/pages/pay.tsx b/packages/taler-wallet-webextension/src/pages/pay.tsx
new file mode 100644
index 000000000..2abd423bd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/pay.tsx
@@ -0,0 +1,180 @@
+/*
+ This file is part of TALER
+ (C) 2015 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/>
+ */
+
+/**
+ * Page shown to the user to confirm entering
+ * a contract.
+ */
+
+/**
+ * Imports.
+ */
+import * as i18n from "../i18n";
+
+
+import { renderAmount, ProgressButton } from "../renderHtml";
+import * as wxApi from "../wxApi";
+
+import React, { useState, useEffect } from "react";
+
+import { Amounts, AmountJson, walletTypes, talerTypes } from "taler-wallet-core";
+
+function TalerPayDialog({ talerPayUri }: { talerPayUri: string }): JSX.Element {
+ const [payStatus, setPayStatus] = useState<walletTypes.PreparePayResult | undefined>();
+ const [payErrMsg, setPayErrMsg] = useState<string | undefined>("");
+ const [numTries, setNumTries] = useState(0);
+ const [loading, setLoading] = useState(false);
+ let amountEffective: AmountJson | undefined = undefined;
+
+ useEffect(() => {
+ const doFetch = async (): Promise<void> => {
+ const p = await wxApi.preparePay(talerPayUri);
+ setPayStatus(p);
+ };
+ doFetch();
+ }, [numTries, talerPayUri]);
+
+ if (!payStatus) {
+ return <span>Loading payment information ...</span>;
+ }
+
+ let insufficientBalance = false;
+ if (payStatus.status == "insufficient-balance") {
+ insufficientBalance = true;
+ }
+
+ if (payStatus.status === "payment-possible") {
+ amountEffective = Amounts.parseOrThrow(payStatus.amountEffective);
+ }
+
+ if (payStatus.status === walletTypes.PreparePayResultType.AlreadyConfirmed && numTries === 0) {
+ return (
+ <span>
+ You have already paid for this article. Click{" "}
+ <a href={payStatus.nextUrl}>here</a> to view it again.
+ </span>
+ );
+ }
+
+ let contractTerms: talerTypes.ContractTerms;
+
+ try {
+ contractTerms = talerTypes.codecForContractTerms().decode(payStatus.contractTerms);
+ } catch (e) {
+ // This should never happen, as the wallet is supposed to check the contract terms
+ // before storing them.
+ console.error(e);
+ console.log("raw contract terms were", payStatus.contractTerms);
+ return <span>Invalid contract terms.</span>;
+ }
+
+ if (!contractTerms) {
+ return (
+ <span>
+ Error: did not get contract terms from merchant or wallet backend.
+ </span>
+ );
+ }
+
+ let merchantName: React.ReactElement;
+ if (contractTerms.merchant && contractTerms.merchant.name) {
+ merchantName = <strong>{contractTerms.merchant.name}</strong>;
+ } else {
+ merchantName = <strong>(pub: {contractTerms.merchant_pub})</strong>;
+ }
+
+ const amount = (
+ <strong>{renderAmount(Amounts.parseOrThrow(contractTerms.amount))}</strong>
+ );
+
+ const doPayment = async (): Promise<void> => {
+ if (payStatus.status !== "payment-possible") {
+ throw Error(`invalid state: ${payStatus.status}`);
+ }
+ const proposalId = payStatus.proposalId;
+ setNumTries(numTries + 1);
+ try {
+ setLoading(true);
+ const res = await wxApi.confirmPay(proposalId, undefined);
+ document.location.href = res.nextUrl;
+ } catch (e) {
+ console.error(e);
+ setPayErrMsg(e.message);
+ }
+ };
+
+ return (
+ <div>
+ <p>
+ <i18n.Translate wrap="p">
+ The merchant <span>{merchantName}</span> offers you to purchase:
+ </i18n.Translate>
+ <div style={{ textAlign: "center" }}>
+ <strong>{contractTerms.summary}</strong>
+ </div>
+ {amountEffective ? (
+ <i18n.Translate wrap="p">
+ The total price is <span>{amount} </span>
+ (plus <span>{renderAmount(amountEffective)}</span> fees).
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate wrap="p">
+ The total price is <span>{amount}</span>.
+ </i18n.Translate>
+ )}
+ </p>
+
+ {insufficientBalance ? (
+ <div>
+ <p style={{ color: "red", fontWeight: "bold" }}>
+ Unable to pay: Your balance is insufficient.
+ </p>
+ </div>
+ ) : null}
+
+ {payErrMsg ? (
+ <div>
+ <p>Payment failed: {payErrMsg}</p>
+ <button
+ className="pure-button button-success"
+ onClick={() => doPayment()}
+ >
+ {i18n.str`Retry`}
+ </button>
+ </div>
+ ) : (
+ <div>
+ <ProgressButton
+ loading={loading}
+ disabled={insufficientBalance}
+ onClick={() => doPayment()}
+ >
+ {i18n.str`Confirm payment`}
+ </ProgressButton>
+ </div>
+ )}
+ </div>
+ );
+}
+
+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} />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/payback.tsx b/packages/taler-wallet-webextension/src/pages/payback.tsx
new file mode 100644
index 000000000..5d42f5f47
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/payback.tsx
@@ -0,0 +1,30 @@
+/*
+ This file is part of TALER
+ (C) 2017 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/>
+ */
+
+/**
+ * View and edit auditors.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import * as React from "react";
+
+export function makePaybackPage(): JSX.Element {
+ return <div>not implemented</div>;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/popup.tsx b/packages/taler-wallet-webextension/src/pages/popup.tsx
new file mode 100644
index 000000000..72c9f4bcb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/popup.tsx
@@ -0,0 +1,502 @@
+/*
+ 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/>
+ */
+
+/**
+ * Popup shown to the user when they click
+ * the Taler browser action button.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import * as i18n from "../i18n";
+
+import {
+ AmountJson,
+ Amounts,
+ time,
+ taleruri,
+ walletTypes,
+} from "taler-wallet-core";
+
+
+import { abbrev, renderAmount, PageLink } from "../renderHtml";
+import * as wxApi from "../wxApi";
+
+import React, { Fragment, useState, useEffect } from "react";
+
+import moment from "moment";
+import { PermissionsCheckbox } from "./welcome";
+
+// FIXME: move to newer react functions
+/* eslint-disable react/no-deprecated */
+
+class Router extends React.Component<any, any> {
+ static setRoute(s: string): void {
+ window.location.hash = s;
+ }
+
+ static getRoute(): string {
+ // Omit the '#' at the beginning
+ return window.location.hash.substring(1);
+ }
+
+ static onRoute(f: any): () => void {
+ Router.routeHandlers.push(f);
+ return () => {
+ const i = Router.routeHandlers.indexOf(f);
+ this.routeHandlers = this.routeHandlers.splice(i, 1);
+ };
+ }
+
+ private static routeHandlers: any[] = [];
+
+ componentWillMount(): void {
+ console.log("router mounted");
+ window.onhashchange = () => {
+ this.setState({});
+ for (const f of Router.routeHandlers) {
+ f();
+ }
+ };
+ }
+
+ 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) => {
+ const childProps: any = (child as any).props;
+ if (!childProps) {
+ return;
+ }
+ if (childProps.default) {
+ defaultChild = child as React.ReactChild;
+ }
+ if (childProps.route === route) {
+ foundChild = child as React.ReactChild;
+ }
+ });
+ const c: React.ReactChild | null = foundChild || defaultChild;
+ if (!c) {
+ throw Error("unknown route");
+ }
+ Router.setRoute((c as any).props.route);
+ return <div>{c}</div>;
+ }
+}
+
+interface TabProps {
+ target: string;
+ children?: React.ReactNode;
+}
+
+function Tab(props: TabProps): JSX.Element {
+ let cssClass = "";
+ if (props.target === Router.getRoute()) {
+ cssClass = "active";
+ }
+ const onClick = (e: React.MouseEvent<HTMLAnchorElement>): void => {
+ Router.setRoute(props.target);
+ e.preventDefault();
+ };
+ return (
+ <a onClick={onClick} href={props.target} className={cssClass}>
+ {props.children}
+ </a>
+ );
+}
+
+class WalletNavBar extends React.Component<any, any> {
+ private cancelSubscription: any;
+
+ componentWillMount(): void {
+ this.cancelSubscription = Router.onRoute(() => {
+ this.setState({});
+ });
+ }
+
+ componentWillUnmount(): void {
+ if (this.cancelSubscription) {
+ this.cancelSubscription();
+ }
+ }
+
+ render(): JSX.Element {
+ 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="/settings">{i18n.str`Settings`}</Tab>
+ <Tab target="/debug">{i18n.str`Debug`}</Tab>
+ </div>
+ );
+ }
+}
+
+/**
+ * Render an amount as a large number with a small currency symbol.
+ */
+function bigAmount(amount: AmountJson): JSX.Element {
+ const v = amount.value + amount.fraction / Amounts.fractionalBase;
+ return (
+ <span>
+ <span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
+ <span>{amount.currency}</span>
+ </span>
+ );
+}
+
+function EmptyBalanceView(): JSX.Element {
+ return (
+ <i18n.Translate wrap="p">
+ You have no balance to show. Need some{" "}
+ <PageLink pageName="welcome.html">help</PageLink> getting started?
+ </i18n.Translate>
+ );
+}
+
+class WalletBalanceView extends React.Component<any, any> {
+ private balance?: walletTypes.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: walletTypes.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: walletTypes.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 = (
+ <i18n.Translate wrap="span">
+ <span style={{ color: "darkgreen" }}>
+ {"+"}
+ {renderAmount(entry.pendingIncoming)}
+ </span>{" "}
+ incoming
+ </i18n.Translate>
+ );
+ }
+
+ 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 />
+ );
+ }
+}
+
+function Icon({ l }: { l: string }): JSX.Element {
+ return <div className={"icon"}>{l}</div>;
+}
+
+function formatAndCapitalize(text: string): string {
+ text = text.replace("-", " ");
+ text = text.replace(/^./, text[0].toUpperCase());
+ return text;
+}
+
+const HistoryComponent = (props: any): JSX.Element => {
+ return <span>TBD</span>;
+};
+
+class WalletSettings extends React.Component<any, any> {
+ render(): JSX.Element {
+ return (
+ <div>
+ <h2>Permissions</h2>
+ <PermissionsCheckbox />
+ </div>
+ );
+ }
+}
+
+function reload(): void {
+ try {
+ chrome.runtime.reload();
+ window.close();
+ } catch (e) {
+ // Functionality missing in firefox, ignore!
+ }
+}
+
+function confirmReset(): void {
+ 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): JSX.Element {
+ return (
+ <div>
+ <p>Debug tools:</p>
+ <button onClick={openExtensionPage("/popup.html")}>wallet tab</button>
+ <button onClick={openExtensionPage("/benchmark.html")}>benchmark</button>
+ <button onClick={openExtensionPage("/show-db.html")}>show db</button>
+ <button onClick={openExtensionPage("/tree.html")}>show tree</button>
+ <br />
+ <button onClick={confirmReset}>reset</button>
+ <button onClick={reload}>reload chrome extension</button>
+ </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;
+}
+
+function actionForTalerUri(talerUri: string): string | undefined {
+ const uriType = taleruri.classifyTalerUri(talerUri);
+ switch (uriType) {
+ case taleruri.TalerUriType.TalerWithdraw:
+ return makeExtensionUrlWithParams("withdraw.html", {
+ talerWithdrawUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerPay:
+ return makeExtensionUrlWithParams("pay.html", {
+ talerPayUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerTip:
+ return makeExtensionUrlWithParams("tip.html", {
+ talerTipUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerRefund:
+ return makeExtensionUrlWithParams("refund.html", {
+ talerRefundUri: talerUri,
+ });
+ case taleruri.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;
+}
+
+async function findTalerUriInActiveTab(): Promise<string | undefined> {
+ return new Promise((resolve, reject) => {
+ chrome.tabs.executeScript(
+ {
+ code: `
+ (() => {
+ let x = document.querySelector("a[href^='taler://'");
+ 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]);
+ },
+ );
+ });
+}
+
+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" }}>
+ <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>
+ <WalletNavBar />
+ <div style={{ margin: "1em" }}>
+ <Router>
+ <WalletBalanceView route="/balance" default />
+ <WalletSettings route="/settings" />
+ <WalletDebug route="/debug" />
+ </Router>
+ </div>
+ </div>
+ );
+}
+
+export function createPopup(): JSX.Element {
+ return <WalletPopup />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/refund.tsx b/packages/taler-wallet-webextension/src/pages/refund.tsx
new file mode 100644
index 000000000..7326dfc88
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/refund.tsx
@@ -0,0 +1,89 @@
+/*
+ This file is part of TALER
+ (C) 2015-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/>
+ */
+
+/**
+ * Page that shows refund status for purchases.
+ *
+ * @author Florian Dold
+ */
+
+import React, { useEffect, useState } from "react";
+
+import * as wxApi from "../wxApi";
+import { AmountView } from "../renderHtml";
+import { walletTypes } from "taler-wallet-core";
+
+function RefundStatusView(props: { talerRefundUri: string }): JSX.Element {
+ const [applied, setApplied] = useState(false);
+ const [purchaseDetails, setPurchaseDetails] = useState<
+ walletTypes.PurchaseDetails | undefined
+ >(undefined);
+ const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
+
+ useEffect(() => {
+ const doFetch = async (): Promise<void> => {
+ try {
+ const result = await wxApi.applyRefund(props.talerRefundUri);
+ setApplied(true);
+ const r = await wxApi.getPurchaseDetails(result.proposalId);
+ setPurchaseDetails(r);
+ } catch (e) {
+ console.error(e);
+ setErrMsg(e.message);
+ console.log("err message", e.message);
+ }
+ };
+ doFetch();
+ }, [props.talerRefundUri]);
+
+ console.log("rendering");
+
+ if (errMsg) {
+ return <span>Error: {errMsg}</span>;
+ }
+
+ if (!applied || !purchaseDetails) {
+ return <span>Updating refund status</span>;
+ }
+
+ return (
+ <>
+ <h2>Refund Status</h2>
+ <p>
+ The product <em>{purchaseDetails.contractTerms.summary}</em> has
+ received a total refund of{" "}
+ <AmountView amount={purchaseDetails.totalRefundAmount} />.
+ </p>
+ <p>Note that additional fees from the exchange may apply.</p>
+ </>
+ );
+}
+
+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 requred");
+ }
+
+ return <RefundStatusView talerRefundUri={talerRefundUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/reset-required.tsx b/packages/taler-wallet-webextension/src/pages/reset-required.tsx
new file mode 100644
index 000000000..0ef5fe8b7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/reset-required.tsx
@@ -0,0 +1,93 @@
+/*
+ This file is part of TALER
+ (C) 2017 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/>
+ */
+
+/**
+ * Page to inform the user when a database reset is required.
+ *
+ * @author Florian Dold
+ */
+
+import * as React from "react";
+
+import * as wxApi from "../wxApi";
+
+interface State {
+ /**
+ * Did the user check the confirmation check box?
+ */
+ checked: boolean;
+
+ /**
+ * Do we actually need to reset the db?
+ */
+ resetRequired: boolean;
+}
+
+class ResetNotification extends React.Component<any, State> {
+ constructor(props: any) {
+ super(props);
+ this.state = { checked: false, resetRequired: true };
+ setInterval(() => this.update(), 500);
+ }
+ async update(): Promise<void> {
+ const res = await wxApi.checkUpgrade();
+ this.setState({ resetRequired: res.dbResetRequired });
+ }
+ render(): JSX.Element {
+ if (this.state.resetRequired) {
+ return (
+ <div>
+ <h1>Manual Reset Reqired</h1>
+ <p>
+ The wallet&apos;s database in your browser is incompatible with the{" "}
+ currently installed wallet. Please reset manually.
+ </p>
+ <p>
+ Once the database format has stabilized, we will provide automatic
+ upgrades.
+ </p>
+ <input
+ id="check"
+ type="checkbox"
+ checked={this.state.checked}
+ onChange={(e) => this.setState({ checked: e.target.checked })}
+ />{" "}
+ <label htmlFor="check">
+ I understand that I will lose all my data
+ </label>
+ <br />
+ <button
+ className="pure-button"
+ disabled={!this.state.checked}
+ onClick={() => wxApi.resetDb()}
+ >
+ Reset
+ </button>
+ </div>
+ );
+ }
+ return (
+ <div>
+ <h1>Everything is fine!</h1>A reset is not required anymore, you can
+ close this page.
+ </div>
+ );
+ }
+}
+
+export function createResetRequiredPage(): JSX.Element {
+ return <ResetNotification />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/return-coins.tsx b/packages/taler-wallet-webextension/src/pages/return-coins.tsx
new file mode 100644
index 000000000..e8cf8c9dd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/return-coins.tsx
@@ -0,0 +1,30 @@
+/*
+ This file is part of TALER
+ (C) 2017 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/>
+ */
+
+/**
+ * Return coins to own bank account.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import * as React from "react";
+
+export function createReturnCoinsPage(): JSX.Element {
+ return <span>Not implemented yet.</span>;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/tip.tsx b/packages/taler-wallet-webextension/src/pages/tip.tsx
new file mode 100644
index 000000000..6cf4e1875
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/tip.tsx
@@ -0,0 +1,103 @@
+/*
+ This file is part of TALER
+ (C) 2017 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/>
+ */
+
+/**
+ * Page shown to the user to confirm creation
+ * of a reserve, usually requested by the bank.
+ *
+ * @author Florian Dold
+ */
+
+import * as React from "react";
+
+import { acceptTip, getTipStatus } from "../wxApi";
+
+import { renderAmount, ProgressButton } from "../renderHtml";
+
+import { useState, useEffect } from "react";
+import { walletTypes } from "taler-wallet-core";
+
+function TipDisplay(props: { talerTipUri: string }): JSX.Element {
+ const [tipStatus, setTipStatus] = useState<walletTypes.TipStatus | undefined>(undefined);
+ const [discarded, setDiscarded] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [finished, setFinished] = useState(false);
+
+ useEffect(() => {
+ const doFetch = async (): Promise<void> => {
+ const ts = await getTipStatus(props.talerTipUri);
+ setTipStatus(ts);
+ };
+ doFetch();
+ }, [props.talerTipUri]);
+
+ if (discarded) {
+ return <span>You&apos;ve discarded the tip.</span>;
+ }
+
+ if (finished) {
+ return <span>Tip has been accepted!</span>;
+ }
+
+ if (!tipStatus) {
+ return <span>Loading ...</span>;
+ }
+
+ const discard = (): void => {
+ setDiscarded(true);
+ };
+
+ const accept = async (): Promise<void> => {
+ setLoading(true);
+ await acceptTip(tipStatus.tipId);
+ setFinished(true);
+ };
+
+ return (
+ <div>
+ <h2>Tip Received!</h2>
+ <p>
+ You received a tip of <strong>{renderAmount(tipStatus.amount)}</strong>{" "}
+ from <span> </span>
+ <strong>{tipStatus.merchantOrigin}</strong>.
+ </p>
+ <p>
+ The tip is handled by the exchange{" "}
+ <strong>{tipStatus.exchangeUrl}</strong>. This exchange will charge fees
+ of <strong>{renderAmount(tipStatus.totalFees)}</strong> for this
+ operation.
+ </p>
+ <form className="pure-form">
+ <ProgressButton loading={loading} onClick={() => accept()}>
+ Accept Tip
+ </ProgressButton>{" "}
+ <button className="pure-button" type="button" onClick={() => discard()}>
+ Discard tip
+ </button>
+ </form>
+ </div>
+ );
+}
+
+export function createTipPage(): JSX.Element {
+ const url = new URL(document.location.href);
+ const talerTipUri = url.searchParams.get("talerTipUri");
+ if (typeof talerTipUri !== "string") {
+ throw Error("talerTipUri must be a string");
+ }
+
+ return <TipDisplay talerTipUri={talerTipUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/welcome.tsx b/packages/taler-wallet-webextension/src/pages/welcome.tsx
new file mode 100644
index 000000000..ff5de572c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/welcome.tsx
@@ -0,0 +1,190 @@
+/*
+ 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 { PageLink } from "../renderHtml";
+import * as wxApi from "../wxApi";
+import { getPermissionsApi } from "../compat";
+import { extendedPermissions } from "../permissions";
+import { walletTypes } from "taler-wallet-core";
+
+function Diagnostics(): JSX.Element | null {
+ const [timedOut, setTimedOut] = useState(false);
+ const [diagnostics, setDiagnostics] = useState<walletTypes.WalletDiagnostics | undefined>(
+ undefined,
+ );
+
+ useEffect(() => {
+ let gotDiagnostics = false;
+ setTimeout(() => {
+ if (!gotDiagnostics) {
+ console.error("timed out");
+ setTimedOut(true);
+ }
+ }, 1000);
+ const doFetch = async (): Promise<void> => {
+ 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 null;
+ } 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 key={errMsg}>{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>;
+}
+
+export function PermissionsCheckbox(): JSX.Element {
+ const [extendedPermissionsEnabled, setExtendedPermissionsEnabled] = useState(
+ false,
+ );
+ async function handleExtendedPerm(requestedVal: boolean): Promise<void> {
+ let nextVal: boolean | undefined;
+ if (requestedVal) {
+ 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);
+ console.log(res);
+ nextVal = res.newValue;
+ } else {
+ const res = await wxApi.setExtendedPermissions(false);
+ console.log(res);
+ nextVal = res.newValue;
+ }
+ console.log("new permissions applied:", nextVal);
+ setExtendedPermissionsEnabled(nextVal ?? false);
+ }
+ useEffect(() => {
+ async function getExtendedPermValue(): Promise<void> {
+ const res = await wxApi.getExtendedPermissions();
+ setExtendedPermissionsEnabled(res.newValue);
+ }
+ getExtendedPermValue();
+ });
+ return (
+ <div>
+ <input
+ checked={extendedPermissionsEnabled}
+ onChange={(x) => handleExtendedPerm(x.target.checked)}
+ 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 Welcome(): JSX.Element {
+ return (
+ <>
+ <p>Thank you for installing the wallet.</p>
+ <Diagnostics />
+ <h2>Permissions</h2>
+ <PermissionsCheckbox />
+ <h2>Next Steps</h2>
+ <a href="https://demo.taler.net/" style={{ display: "block" }}>
+ Try the demo »
+ </a>
+ <a href="https://demo.taler.net/" style={{ display: "block" }}>
+ Learn how to top up your wallet balance »
+ </a>
+ </>
+ );
+}
+
+export function createWelcomePage(): JSX.Element {
+ return <Welcome />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/withdraw.tsx b/packages/taler-wallet-webextension/src/pages/withdraw.tsx
new file mode 100644
index 000000000..4a92704b3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/withdraw.tsx
@@ -0,0 +1,229 @@
+/*
+ This file is part of TALER
+ (C) 2015-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/>
+ */
+
+/**
+ * Page shown to the user to confirm creation
+ * of a reserve, usually requested by the bank.
+ *
+ * @author Florian Dold
+ */
+
+import * as i18n from "../i18n";
+
+import { WithdrawDetailView, renderAmount } from "../renderHtml";
+
+import React, { useState, useEffect } from "react";
+import {
+ acceptWithdrawal,
+ onUpdateNotification,
+} from "../wxApi";
+
+function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element {
+ const [details, setDetails] = useState<
+ any | undefined
+ >();
+ const [selectedExchange, setSelectedExchange] = useState<
+ string | undefined
+ >();
+ const talerWithdrawUri = props.talerWithdrawUri;
+ const [cancelled, setCancelled] = useState(false);
+ const [selecting, setSelecting] = useState(false);
+ const [customUrl, setCustomUrl] = useState<string>("");
+ const [errMsg, setErrMsg] = useState<string | undefined>("");
+ const [updateCounter, setUpdateCounter] = useState(1);
+
+ useEffect(() => {
+ return onUpdateNotification(() => {
+ setUpdateCounter(updateCounter + 1);
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ const fetchData = async (): Promise<void> => {
+ // FIXME: re-implement with new API
+ // console.log("getting from", talerWithdrawUri);
+ // let d: WithdrawalDetailsResponse | undefined = undefined;
+ // try {
+ // d = await getWithdrawDetails(talerWithdrawUri, selectedExchange);
+ // } catch (e) {
+ // console.error(
+ // `error getting withdraw details for uri ${talerWithdrawUri}, exchange ${selectedExchange}`,
+ // e,
+ // );
+ // setErrMsg(e.message);
+ // return;
+ // }
+ // console.log("got withdrawDetails", d);
+ // if (!selectedExchange && d.bankWithdrawDetails.suggestedExchange) {
+ // console.log("setting selected exchange");
+ // setSelectedExchange(d.bankWithdrawDetails.suggestedExchange);
+ // }
+ // setDetails(d);
+ };
+ fetchData();
+ }, [selectedExchange, errMsg, selecting, talerWithdrawUri, updateCounter]);
+
+ if (errMsg) {
+ return (
+ <div>
+ <i18n.Translate wrap="p">
+ Could not get details for withdraw operation:
+ </i18n.Translate>
+ <p style={{ color: "red" }}>{errMsg}</p>
+ <p>
+ <span
+ role="button"
+ tabIndex={0}
+ style={{ textDecoration: "underline", cursor: "pointer" }}
+ onClick={() => {
+ setSelecting(true);
+ setErrMsg(undefined);
+ setSelectedExchange(undefined);
+ setDetails(undefined);
+ }}
+ >
+ {i18n.str`Chose different exchange provider`}
+ </span>
+ </p>
+ </div>
+ );
+ }
+
+ if (!details) {
+ return <span>Loading...</span>;
+ }
+
+ if (cancelled) {
+ return <span>Withdraw operation has been cancelled.</span>;
+ }
+
+ if (selecting) {
+ const bankSuggestion =
+ details && details.bankWithdrawDetails.suggestedExchange;
+ return (
+ <div>
+ {i18n.str`Please select an exchange. You can review the details before after your selection.`}
+ {bankSuggestion && (
+ <div>
+ <h2>Bank Suggestion</h2>
+ <button
+ className="pure-button button-success"
+ onClick={() => {
+ setDetails(undefined);
+ setSelectedExchange(bankSuggestion);
+ setSelecting(false);
+ }}
+ >
+ <i18n.Translate wrap="span">
+ Select <strong>{bankSuggestion}</strong>
+ </i18n.Translate>
+ </button>
+ </div>
+ )}
+ <h2>Custom Selection</h2>
+ <p>
+ <input
+ type="text"
+ onChange={(e) => setCustomUrl(e.target.value)}
+ value={customUrl}
+ />
+ </p>
+ <button
+ className="pure-button button-success"
+ onClick={() => {
+ setDetails(undefined);
+ setSelectedExchange(customUrl);
+ setSelecting(false);
+ }}
+ >
+ <i18n.Translate wrap="span">Select custom exchange</i18n.Translate>
+ </button>
+ </div>
+ );
+ }
+
+ const accept = async (): Promise<void> => {
+ if (!selectedExchange) {
+ throw Error("can't accept, no exchange selected");
+ }
+ console.log("accepting exchange", selectedExchange);
+ const res = await acceptWithdrawal(talerWithdrawUri, selectedExchange);
+ console.log("accept withdrawal response", res);
+ if (res.confirmTransferUrl) {
+ document.location.href = res.confirmTransferUrl;
+ }
+ };
+
+ return (
+ <div>
+ <h1>Digital Cash Withdrawal</h1>
+ <i18n.Translate wrap="p">
+ You are about to withdraw{" "}
+ <strong>{renderAmount(details.bankWithdrawDetails.amount)}</strong> from
+ your bank account into your wallet.
+ </i18n.Translate>
+ {selectedExchange ? (
+ <p>
+ The exchange <strong>{selectedExchange}</strong> will be used as the
+ Taler payment service provider.
+ </p>
+ ) : null}
+
+ <div>
+ <button
+ className="pure-button button-success"
+ disabled={!selectedExchange}
+ onClick={() => accept()}
+ >
+ {i18n.str`Accept fees and withdraw`}
+ </button>
+ <p>
+ <span
+ role="button"
+ tabIndex={0}
+ style={{ textDecoration: "underline", cursor: "pointer" }}
+ onClick={() => setSelecting(true)}
+ >
+ {i18n.str`Chose different exchange provider`}
+ </span>
+ <br />
+ <span
+ role="button"
+ tabIndex={0}
+ style={{ textDecoration: "underline", cursor: "pointer" }}
+ onClick={() => setCancelled(true)}
+ >
+ {i18n.str`Cancel withdraw operation`}
+ </span>
+ </p>
+
+ {details.exchangeWithdrawDetails ? (
+ <WithdrawDetailView rci={details.exchangeWithdrawDetails} />
+ ) : null}
+ </div>
+ </div>
+ );
+}
+
+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} />;
+}