diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/pages')
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'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'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} />; +} |