taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 0b4976601fe2ecb0462fe72ae188b5cbba06d9cc
parent d58945c830a33910dd93bc159c1ffe5d490df846
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 16 Jun 2021 18:21:03 -0300

components renaming to follow react pattern

Diffstat:
Apackages/taler-wallet-webextension/src/components/DebugCheckbox.tsx | 47+++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/taler-wallet-webextension/src/hooks/useExtendedPermissions.tsx | 24------------------------
Apackages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/Balance.tsx | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/Debug.tsx | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/History.tsx | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/Settings.tsx | 34++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/Transaction.stories.tsx | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/Transaction.tsx | 327+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/taler-wallet-webextension/src/popup/popup.stories.tsx | 197-------------------------------------------------------------------------------
Mpackages/taler-wallet-webextension/src/popup/popup.tsx | 899+------------------------------------------------------------------------------
Mpackages/taler-wallet-webextension/src/popupEntryPoint.tsx | 43+++++++++++++------------------------------
Apackages/taler-wallet-webextension/src/wallet/Pay.tsx | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/wallet/Refund.tsx | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/wallet/Tip.tsx | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/wallet/Welcome.tsx | 45+++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/wallet/Withdraw.stories.tsx | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/wallet/Withdraw.tsx | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/taler-wallet-webextension/src/wallet/pay.tsx | 235-------------------------------------------------------------------------------
Dpackages/taler-wallet-webextension/src/wallet/refund.tsx | 108-------------------------------------------------------------------------------
Dpackages/taler-wallet-webextension/src/wallet/tip.tsx | 109-------------------------------------------------------------------------------
Dpackages/taler-wallet-webextension/src/wallet/welcome.tsx | 83-------------------------------------------------------------------------------
Dpackages/taler-wallet-webextension/src/wallet/withdraw.stories.tsx | 66------------------------------------------------------------------
Dpackages/taler-wallet-webextension/src/wallet/withdraw.tsx | 173-------------------------------------------------------------------------------
Mpackages/taler-wallet-webextension/src/walletEntryPoint.tsx | 20++++++++++----------
26 files changed, 1923 insertions(+), 1932 deletions(-)

diff --git a/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx b/packages/taler-wallet-webextension/src/components/DebugCheckbox.tsx @@ -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> + ); +} diff --git a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts b/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.ts @@ -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 +} +\ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.tsx b/packages/taler-wallet-webextension/src/hooks/useExtendedPermissions.tsx @@ -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]; -} diff --git a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.ts @@ -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]); + }, + ); + }); +} diff --git a/packages/taler-wallet-webextension/src/popup/Balance.tsx b/packages/taler-wallet-webextension/src/popup/Balance.tsx @@ -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 /> + ); + } +} diff --git a/packages/taler-wallet-webextension/src/popup/Debug.tsx b/packages/taler-wallet-webextension/src/popup/Debug.tsx @@ -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), + }); + }; +} + diff --git a/packages/taler-wallet-webextension/src/popup/History.tsx b/packages/taler-wallet-webextension/src/popup/History.tsx @@ -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> + ); +} + diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx b/packages/taler-wallet-webextension/src/popup/Settings.tsx @@ -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> + ); +} diff --git a/packages/taler-wallet-webextension/src/popup/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/popup/Transaction.stories.tsx @@ -0,0 +1,198 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + 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/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { + PaymentStatus, + TransactionCommon, TransactionDeposit, TransactionPayment, + TransactionRefresh, TransactionRefund, TransactionTip, TransactionType, + TransactionWithdrawal, + WithdrawalType +} from '@gnu-taler/taler-util'; +import { FunctionalComponent } from 'preact'; +import { TransactionView as TestedComponent } from './Transaction'; + +export default { + title: 'popup/transaction/details', + component: TestedComponent, + decorators: [ + (Story: any) => <div> + <link key="1" rel="stylesheet" type="text/css" href="/style/pure.css" /> + <link key="2" rel="stylesheet" type="text/css" href="/style/popup.css" /> + <link key="3" rel="stylesheet" type="text/css" href="/style/wallet.css" /> + <div style={{ margin: "1em", width: 400 }}> + <Story /> + </div> + </div> + ], +}; + +const commonTransaction = { + amountRaw: 'USD:10', + amountEffective: 'USD:9', + pending: false, + timestamp: { + t_ms: new Date().getTime() + }, + transactionId: '12', +} as TransactionCommon + +const exampleData = { + withdraw: { + ...commonTransaction, + type: TransactionType.Withdrawal, + exchangeBaseUrl: 'http://exchange.taler', + withdrawalDetails: { + confirmed: false, + exchangePaytoUris: ['payto://x-taler-bank/bank/account'], + type: WithdrawalType.ManualTransfer, + } + } as TransactionWithdrawal, + payment: { + ...commonTransaction, + amountEffective: 'USD:11', + type: TransactionType.Payment, + info: { + contractTermsHash: 'ASDZXCASD', + merchant: { + name: 'the merchant', + }, + orderId: '#12345', + products: [], + summary: 'the summary', + fulfillmentMessage: '', + }, + proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + status: PaymentStatus.Accepted, + } as TransactionPayment, + deposit: { + ...commonTransaction, + type: TransactionType.Deposit, + depositGroupId: '#groupId', + targetPaytoUri: 'payto://x-taler-bank/bank/account', + } as TransactionDeposit, + refresh: { + ...commonTransaction, + type: TransactionType.Refresh, + exchangeBaseUrl: 'http://exchange.taler', + } as TransactionRefresh, + tip: { + ...commonTransaction, + type: TransactionType.Tip, + merchantBaseUrl: 'http://merchant.taler', + } as TransactionTip, + refund: { + ...commonTransaction, + type: TransactionType.Refund, + refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + info: { + contractTermsHash: 'ASDZXCASD', + merchant: { + name: 'the merchant', + }, + orderId: '#12345', + products: [], + summary: 'the summary', + fulfillmentMessage: '', + }, + } as TransactionRefund, +} + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +export const NotYetLoaded = createExample(TestedComponent,{}); + +export const Withdraw = createExample(TestedComponent,{ + transaction: exampleData.withdraw +}); + +export const WithdrawPending = createExample(TestedComponent,{ + transaction: { ...exampleData.withdraw, pending: true }, +}); + + +export const Payment = createExample(TestedComponent,{ + transaction: exampleData.payment +}); + +export const PaymentPending = createExample(TestedComponent,{ + transaction: { ...exampleData.payment, pending: true }, +}); + +export const PaymentWithProducts = createExample(TestedComponent,{ + transaction: { + ...exampleData.payment, + info: { + ...exampleData.payment.info, + products: [{ + description: 't-shirt', + }, { + description: 'beer', + }] + } + } as TransactionPayment, +}); + + +export const Deposit = createExample(TestedComponent,{ + transaction: exampleData.deposit +}); + +export const DepositPending = createExample(TestedComponent,{ + transaction: { ...exampleData.deposit, pending: true } +}); + +export const Refresh = createExample(TestedComponent,{ + transaction: exampleData.refresh +}); + +export const Tip = createExample(TestedComponent,{ + transaction: exampleData.tip +}); + +export const TipPending = createExample(TestedComponent,{ + transaction: { ...exampleData.tip, pending: true } +}); + +export const Refund = createExample(TestedComponent,{ + transaction: exampleData.refund +}); + +export const RefundPending = createExample(TestedComponent,{ + transaction: { ...exampleData.refund, pending: true } +}); + +export const RefundWithProducts = createExample(TestedComponent,{ + transaction: { + ...exampleData.refund, + info: { + ...exampleData.refund.info, + products: [{ + description: 't-shirt', + }, { + description: 'beer', + }] + } + } as TransactionRefund, +}); diff --git a/packages/taler-wallet-webextension/src/popup/Transaction.tsx b/packages/taler-wallet-webextension/src/popup/Transaction.tsx @@ -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> +} diff --git a/packages/taler-wallet-webextension/src/popup/popup.stories.tsx b/packages/taler-wallet-webextension/src/popup/popup.stories.tsx @@ -1,197 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - 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/> - */ - -/** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { - PaymentStatus, - TransactionCommon, TransactionDeposit, TransactionPayment, - TransactionRefresh, TransactionRefund, TransactionTip, TransactionType, - TransactionWithdrawal, - WithdrawalType -} from '@gnu-taler/taler-util'; -import { WalletTransactionView as Component } from './popup'; - -export default { - title: 'popup/transaction details', - component: Component, - decorators: [ - (Story: any) => <div> - <link key="1" rel="stylesheet" type="text/css" href="/style/pure.css" /> - <link key="2" rel="stylesheet" type="text/css" href="/style/popup.css" /> - <link key="3" rel="stylesheet" type="text/css" href="/style/wallet.css" /> - <div style={{ margin: "1em", width: 400 }}> - <Story /> - </div> - </div> - ], -}; - -const commonTransaction = { - amountRaw: 'USD:10', - amountEffective: 'USD:9', - pending: false, - timestamp: { - t_ms: new Date().getTime() - }, - transactionId: '12', -} as TransactionCommon - -const exampleData = { - withdraw: { - ...commonTransaction, - type: TransactionType.Withdrawal, - exchangeBaseUrl: 'http://exchange.taler', - withdrawalDetails: { - confirmed: false, - exchangePaytoUris: ['payto://x-taler-bank/bank/account'], - type: WithdrawalType.ManualTransfer, - } - } as TransactionWithdrawal, - payment: { - ...commonTransaction, - amountEffective: 'USD:11', - type: TransactionType.Payment, - info: { - contractTermsHash: 'ASDZXCASD', - merchant: { - name: 'the merchant', - }, - orderId: '#12345', - products: [], - summary: 'the summary', - fulfillmentMessage: '', - }, - proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', - status: PaymentStatus.Accepted, - } as TransactionPayment, - deposit: { - ...commonTransaction, - type: TransactionType.Deposit, - depositGroupId: '#groupId', - targetPaytoUri: 'payto://x-taler-bank/bank/account', - } as TransactionDeposit, - refresh: { - ...commonTransaction, - type: TransactionType.Refresh, - exchangeBaseUrl: 'http://exchange.taler', - } as TransactionRefresh, - tip: { - ...commonTransaction, - type: TransactionType.Tip, - merchantBaseUrl: 'http://merchant.taler', - } as TransactionTip, - refund: { - ...commonTransaction, - type: TransactionType.Refund, - refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', - info: { - contractTermsHash: 'ASDZXCASD', - merchant: { - name: 'the merchant', - }, - orderId: '#12345', - products: [], - summary: 'the summary', - fulfillmentMessage: '', - }, - } as TransactionRefund, -} - -function dynamic<T>(props: any) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r -} - -export const NotYetLoaded = dynamic({}); - -export const Withdraw = dynamic({ - transaction: exampleData.withdraw -}); - -export const WithdrawPending = dynamic({ - transaction: { ...exampleData.withdraw, pending: true }, -}); - - -export const Payment = dynamic({ - transaction: exampleData.payment -}); - -export const PaymentPending = dynamic({ - transaction: { ...exampleData.payment, pending: true }, -}); - -export const PaymentWithProducts = dynamic({ - transaction: { - ...exampleData.payment, - info: { - ...exampleData.payment.info, - products: [{ - description: 't-shirt', - }, { - description: 'beer', - }] - } - } as TransactionPayment, -}); - - -export const Deposit = dynamic({ - transaction: exampleData.deposit -}); - -export const DepositPending = dynamic({ - transaction: { ...exampleData.deposit, pending: true } -}); - -export const Refresh = dynamic({ - transaction: exampleData.refresh -}); - -export const Tip = dynamic({ - transaction: exampleData.tip -}); - -export const TipPending = dynamic({ - transaction: { ...exampleData.tip, pending: true } -}); - -export const Refund = dynamic({ - transaction: exampleData.refund -}); - -export const RefundPending = dynamic({ - transaction: { ...exampleData.refund, pending: true } -}); - -export const RefundWithProducts = dynamic({ - transaction: { - ...exampleData.refund, - info: { - ...exampleData.refund.info, - products: [{ - description: 't-shirt', - }, { - description: 'beer', - }] - } - } as TransactionRefund, -}); diff --git a/packages/taler-wallet-webextension/src/popup/popup.tsx b/packages/taler-wallet-webextension/src/popup/popup.tsx @@ -25,29 +25,9 @@ * Imports. */ import { - AmountJson, - Amounts, - BalancesResponse, - Balance, - classifyTalerUri, - TalerUriType, - TransactionsResponse, - Transaction, - TransactionType, - AmountString, - Timestamp, - amountFractionalBase, - i18n, + classifyTalerUri, i18n, TalerUriType } from "@gnu-taler/taler-util"; -import { format } from "date-fns"; -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"; +import { ComponentChildren, JSX } from "preact"; export enum Pages { 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> -// ); -// } - diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -23,13 +23,17 @@ import { render } from "preact"; import { setupI18n } from "@gnu-taler/taler-util"; import { strings } from "./i18n/strings"; -import { useEffect, useState } from "preact/hooks"; +import { useEffect } from "preact/hooks"; import { - actionForTalerUri, findTalerUriInActiveTab, Pages, WalletBalanceView, WalletDebug, WalletHistory, - WalletNavBar, WalletSettings, WalletTransaction, WalletTransactionView -} from "./popup/popup"; + Pages, WalletNavBar} from "./popup/popup"; +import { HistoryPage } from "./popup/History"; +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 Router, { route, Route } from "preact-router"; +import { useTalerActionURL } from "./hooks/useTalerActionURL"; // import { Application } from "./Application"; function main(): void { @@ -53,25 +57,6 @@ if (document.readyState === "loading") { 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 { url: string; onDismiss: (s: boolean) => void; @@ -105,11 +90,11 @@ function Application() { <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} /> + <Route path={Pages.balance} component={BalancePage} /> + <Route path={Pages.settings} component={SettingsPage} /> + <Route path={Pages.debug} component={DebugPage} /> + <Route path={Pages.history} component={HistoryPage} /> + <Route path={Pages.transaction} component={TransactionPage} /> <Route default component={Redirect} to={Pages.balance} /> </Router> </div> @@ -118,8 +103,6 @@ function Application() { } - - function Redirect({ to }: { to: string }): null { useEffect(() => { route(to, true) diff --git a/packages/taler-wallet-webextension/src/wallet/Pay.tsx b/packages/taler-wallet-webextension/src/wallet/Pay.tsx @@ -0,0 +1,224 @@ +/* + 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 { useState, useEffect } from "preact/hooks"; + +import { getJsonI18n, i18n } from "@gnu-taler/taler-util"; +import { + PreparePayResult, + ConfirmPayResult, + AmountJson, + PreparePayResultType, + Amounts, + ContractTerms, + ConfirmPayResultType, +} from "@gnu-taler/taler-util"; +import { JSX, VNode } from "preact"; + +interface Props { + talerPayUri?: string +} + +export function PayPage({ talerPayUri }: Props): JSX.Element { + const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(undefined); + const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(undefined); + const [payErrMsg, setPayErrMsg] = useState<string | undefined>(""); + const [numTries, setNumTries] = useState(0); + const [loading, setLoading] = useState(false); + let totalFees: AmountJson | undefined = undefined; + + useEffect(() => { + if (!talerPayUri) return; + const doFetch = async (): Promise<void> => { + const p = await wxApi.preparePay(talerPayUri); + setPayStatus(p); + }; + doFetch(); + }, [numTries, talerPayUri]); + + if (!talerPayUri) { + return <span>missing pay uri</span> + } + + if (!payStatus) { + return <span>Loading payment information ...</span>; + } + + let insufficientBalance = false; + if (payStatus.status == PreparePayResultType.InsufficientBalance) { + insufficientBalance = true; + } + + if (payStatus.status === PreparePayResultType.PaymentPossible) { + const amountRaw = Amounts.parseOrThrow(payStatus.amountRaw); + const amountEffective: AmountJson = Amounts.parseOrThrow( + payStatus.amountEffective, + ); + totalFees = Amounts.sub(amountEffective, amountRaw).amount; + } + + if ( + payStatus.status === PreparePayResultType.AlreadyConfirmed && + numTries === 0 + ) { + const fulfillmentUrl = payStatus.contractTerms.fulfillment_url; + if (fulfillmentUrl) { + return ( + <span> + You have already paid for this article. Click{" "} + <a href={fulfillmentUrl} target="_bank" rel="external">here</a> to view it again. + </span> + ); + } else { + <span> + You have already paid for this article:{" "} + <em> + {payStatus.contractTerms.fulfillment_message ?? "no message given"} + </em> + </span>; + } + } + + const contractTerms: ContractTerms = payStatus.contractTerms; + + if (!contractTerms) { + return ( + <span> + Error: did not get contract terms from merchant or wallet backend. + </span> + ); + } + + let merchantName: VNode; + 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); + if (res.type !== ConfirmPayResultType.Done) { + throw Error("payment pending"); + } + const fu = res.contractTerms.fulfillment_url; + if (fu) { + document.location.href = fu; + } + setPayResult(res); + } catch (e) { + console.error(e); + setPayErrMsg(e.message); + } + }; + + if (payResult && payResult.type === ConfirmPayResultType.Done) { + if (payResult.contractTerms.fulfillment_message) { + const obj = { + fulfillment_message: payResult.contractTerms.fulfillment_message, + fulfillment_message_i18n: + payResult.contractTerms.fulfillment_message_i18n, + }; + const msg = getJsonI18n(obj, "fulfillment_message"); + return ( + <div> + <p>Payment succeeded.</p> + <p>{msg}</p> + </div> + ); + } else { + return <span>Redirecting ...</span>; + } + } + + return ( + <div> + <p> + <i18n.Translate> + The merchant <span>{merchantName}</span> offers you to purchase: + </i18n.Translate> + <div style={{ textAlign: "center" }}> + <strong>{contractTerms.summary}</strong> + </div> + {totalFees ? ( + <i18n.Translate> + The total price is <span>{amount} </span> + (plus <span>{renderAmount(totalFees)}</span> fees). + </i18n.Translate> + ) : ( + <i18n.Translate> + 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 + isLoading={loading} + disabled={insufficientBalance} + onClick={() => doPayment()} + > + {i18n.str`Confirm payment`} + </ProgressButton> + </div> + )} + </div> + ); +} + diff --git a/packages/taler-wallet-webextension/src/wallet/Refund.tsx b/packages/taler-wallet-webextension/src/wallet/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 * as wxApi from "../wxApi"; +import { AmountView } from "../renderHtml"; +import { + ApplyRefundResponse, + Amounts, +} from "@gnu-taler/taler-util"; +import { useEffect, useState } from "preact/hooks"; +import { JSX } from "preact/jsx-runtime"; + +interface Props { + talerRefundUri?: string +} + +export function RefundPage({ talerRefundUri }: Props): JSX.Element { + const [applyResult, setApplyResult] = useState<ApplyRefundResponse | undefined>(undefined); + const [errMsg, setErrMsg] = useState<string | undefined>(undefined); + + useEffect(() => { + if (!talerRefundUri) return; + const doFetch = async (): Promise<void> => { + try { + const result = await wxApi.applyRefund(talerRefundUri); + setApplyResult(result); + } catch (e) { + console.error(e); + setErrMsg(e.message); + console.log("err message", e.message); + } + }; + doFetch(); + }, [talerRefundUri]); + + console.log("rendering"); + + if (!talerRefundUri) { + return <span>missing taler refund uri</span>; + } + + if (errMsg) { + return <span>Error: {errMsg}</span>; + } + + if (!applyResult) { + return <span>Updating refund status</span>; + } + + return ( + <> + <h2>Refund Status</h2> + <p> + The product <em>{applyResult.info.summary}</em> has received a total + effective refund of{" "} + <AmountView amount={applyResult.amountRefundGranted} />. + </p> + {applyResult.pendingAtExchange ? ( + <p>Refund processing is still in progress.</p> + ) : null} + {!Amounts.isZero(applyResult.amountRefundGone) ? ( + <p> + The refund amount of{" "} + <AmountView amount={applyResult.amountRefundGone} /> + could not be applied. + </p> + ) : null} + </> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/Tip.tsx b/packages/taler-wallet-webextension/src/wallet/Tip.tsx @@ -0,0 +1,97 @@ +/* + 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 accept or ignore a tip from a merchant. + * + * @author Florian Dold <dold@taler.net> + */ + +import { useEffect, useState } from "preact/hooks"; +import { PrepareTipResult } from "@gnu-taler/taler-util"; +import { AmountView } from "../renderHtml"; +import * as wxApi from "../wxApi"; +import { JSX } from "preact/jsx-runtime"; + +interface Props { + talerTipUri?: string +} + +export function TipPage({ talerTipUri }: Props): JSX.Element { + const [updateCounter, setUpdateCounter] = useState<number>(0); + const [prepareTipResult, setPrepareTipResult] = useState< + PrepareTipResult | undefined + >(undefined); + + const [tipIgnored, setTipIgnored] = useState(false); + + useEffect(() => { + if (!talerTipUri) return; + const doFetch = async (): Promise<void> => { + const p = await wxApi.prepareTip({ talerTipUri }); + setPrepareTipResult(p); + }; + doFetch(); + }, [talerTipUri, updateCounter]); + + const doAccept = async () => { + if (!prepareTipResult) { + return; + } + await wxApi.acceptTip({ walletTipId: prepareTipResult?.walletTipId }); + setUpdateCounter(updateCounter + 1); + }; + + const doIgnore = () => { + setTipIgnored(true); + }; + + if (!talerTipUri) { + return <span>missing tip uri</span>; + } + + if (tipIgnored) { + return <span>You've ignored the tip.</span>; + } + + if (!prepareTipResult) { + return <span>Loading ...</span>; + } + + if (prepareTipResult.accepted) { + return ( + <span> + Tip from <code>{prepareTipResult.merchantBaseUrl}</code> accepted. Check + your transactions list for more details. + </span> + ); + } else { + return ( + <div> + <p> + The merchant <code>{prepareTipResult.merchantBaseUrl}</code> is + offering you a tip of{" "} + <strong> + <AmountView amount={prepareTipResult.tipAmountEffective} /> + </strong>{" "} + via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code> + </p> + <button onClick={doAccept}>Accept tip</button> + <button onClick={doIgnore}>Ignore</button> + </div> + ); + } +} diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx @@ -0,0 +1,45 @@ +/* + 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 { JSX } from "preact/jsx-runtime"; +import { PermissionsCheckbox } from "../components/PermissionsCheckbox"; +import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; +import { Diagnostics } from "../components/Diagnostics"; + +export function WelcomePage(): JSX.Element { + const [permissionsEnabled, togglePermissions] = useExtendedPermissions() + return ( + <> + <p>Thank you for installing the wallet.</p> + <Diagnostics /> + <h2>Permissions</h2> + <PermissionsCheckbox enabled={permissionsEnabled} onToggle={togglePermissions}/> + <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> + </> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/Withdraw.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Withdraw.stories.tsx @@ -0,0 +1,66 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + 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/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { h } from 'preact'; +import { View, ViewProps } from './Withdraw'; + + +export default { + title: 'wallet/withdraw', + component: View, + argTypes: { + }, +}; + +export const WithoutURI = (a: any) => <View {...a} />; +WithoutURI.args = { +} as ViewProps + +export const WithoutDetails = (a: any) => <View {...a} />; +WithoutDetails.args = { + talerWithdrawUri: 'http://something' +} as ViewProps + +export const Cancelled = (a: any) => <View {...a} />; +Cancelled.args = { + talerWithdrawUri: 'http://something', + details: { + amount: 'USD:2', + }, + cancelled: true +} as ViewProps + +export const CompleteWithExchange = (a: any) => <View {...a} />; +CompleteWithExchange.args = { + talerWithdrawUri: 'http://something', + details: { + amount: 'USD:2', + }, + selectedExchange: 'Some exchange' +} as ViewProps + +export const CompleteWithoutExchange = (a: any) => <View {...a} />; +CompleteWithoutExchange.args = { + talerWithdrawUri: 'http://something', + details: { + amount: 'USD:2', + }, +} as ViewProps diff --git a/packages/taler-wallet-webextension/src/wallet/Withdraw.tsx b/packages/taler-wallet-webextension/src/wallet/Withdraw.tsx @@ -0,0 +1,161 @@ +/* + 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 { i18n } from '@gnu-taler/taler-util' +import { renderAmount } from "../renderHtml"; + +import { useState, useEffect } from "preact/hooks"; +import { + acceptWithdrawal, + onUpdateNotification, + getWithdrawalDetailsForUri, +} from "../wxApi"; +import { WithdrawUriInfoResponse } from "@gnu-taler/taler-util"; +import { JSX } from "preact/jsx-runtime"; + +interface Props { + talerWithdrawUri?: string; +} + +export interface ViewProps { + talerWithdrawUri?: string; + details?: WithdrawUriInfoResponse; + cancelled?: boolean; + selectedExchange?: string; + accept: () => Promise<void>; + setCancelled: (b: boolean) => void; + setSelecting: (b: boolean) => void; +}; + +export function View({ talerWithdrawUri, details, cancelled, selectedExchange, accept, setCancelled, setSelecting }: ViewProps) { + const [state, setState] = useState(1) + setTimeout(() => { + setState(s => s + 1) + }, 1000); + if (!talerWithdrawUri) { + return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>; + } + + if (!details) { + return <span><i18n.Translate>Loading...</i18n.Translate></span>; + } + + if (cancelled) { + return <span><i18n.Translate>Withdraw operation has been cancelled.{state}</i18n.Translate></span>; + } + + return ( + <div> + <h1><i18n.Translate>Digital Cash Withdrawal</i18n.Translate></h1> + <p><i18n.Translate> + You are about to withdraw{" "} + <strong>{renderAmount(details.amount)}</strong> from your bank account + into your wallet. + </i18n.Translate></p> + {selectedExchange ? ( + <p><i18n.Translate> + The exchange <strong>{selectedExchange}</strong> will be used as the + Taler payment service provider. + </i18n.Translate></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> + </div> + </div> + ) +} + +export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element { + const [details, setDetails] = useState<WithdrawUriInfoResponse | undefined>(undefined); + const [selectedExchange, setSelectedExchange] = useState< + string | undefined + >(undefined); + const [cancelled, setCancelled] = useState(false); + const [selecting, setSelecting] = useState(false); + const [errMsg, setErrMsg] = useState<string | undefined>(""); + const [updateCounter, setUpdateCounter] = useState(1); + + useEffect(() => { + return onUpdateNotification(() => { + setUpdateCounter(updateCounter + 1); + }); + }, []); + + useEffect(() => { + if (!talerWithdrawUri) return + const fetchData = async (): Promise<void> => { + const res = await getWithdrawalDetailsForUri({ talerWithdrawUri }); + setDetails(res); + if (res.defaultExchangeBaseUrl) { + setSelectedExchange(res.defaultExchangeBaseUrl); + } + }; + fetchData(); + }, [selectedExchange, errMsg, selecting, talerWithdrawUri, updateCounter]); + + const accept = async (): Promise<void> => { + if (!talerWithdrawUri) return + 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 <View accept={accept} + setCancelled={setCancelled} setSelecting={setSelecting} + cancelled={cancelled} details={details} selectedExchange={selectedExchange} + talerWithdrawUri={talerWithdrawUri} + /> +} + diff --git a/packages/taler-wallet-webextension/src/wallet/pay.tsx b/packages/taler-wallet-webextension/src/wallet/pay.tsx @@ -1,235 +0,0 @@ -/* - 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 { useState, useEffect } from "preact/hooks"; - -import { getJsonI18n, i18n } from "@gnu-taler/taler-util"; -import { - PreparePayResult, - ConfirmPayResult, - AmountJson, - PreparePayResultType, - Amounts, - ContractTerms, - ConfirmPayResultType, -} from "@gnu-taler/taler-util"; -import { JSX, VNode } from "preact"; - -interface Props { - talerPayUri?: string -} - -export function TalerPayDialog({ talerPayUri }: Props): JSX.Element { - const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(undefined); - const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(undefined); - const [payErrMsg, setPayErrMsg] = useState<string | undefined>(""); - const [numTries, setNumTries] = useState(0); - const [loading, setLoading] = useState(false); - let totalFees: AmountJson | undefined = undefined; - - useEffect(() => { - if (!talerPayUri) return; - const doFetch = async (): Promise<void> => { - const p = await wxApi.preparePay(talerPayUri); - setPayStatus(p); - }; - doFetch(); - }, [numTries, talerPayUri]); - - if (!talerPayUri) { - return <span>missing pay uri</span> - } - - if (!payStatus) { - return <span>Loading payment information ...</span>; - } - - let insufficientBalance = false; - if (payStatus.status == PreparePayResultType.InsufficientBalance) { - insufficientBalance = true; - } - - if (payStatus.status === PreparePayResultType.PaymentPossible) { - const amountRaw = Amounts.parseOrThrow(payStatus.amountRaw); - const amountEffective: AmountJson = Amounts.parseOrThrow( - payStatus.amountEffective, - ); - totalFees = Amounts.sub(amountEffective, amountRaw).amount; - } - - if ( - payStatus.status === PreparePayResultType.AlreadyConfirmed && - numTries === 0 - ) { - const fulfillmentUrl = payStatus.contractTerms.fulfillment_url; - if (fulfillmentUrl) { - return ( - <span> - You have already paid for this article. Click{" "} - <a href={fulfillmentUrl} target="_bank" rel="external">here</a> to view it again. - </span> - ); - } else { - <span> - You have already paid for this article:{" "} - <em> - {payStatus.contractTerms.fulfillment_message ?? "no message given"} - </em> - </span>; - } - } - - const contractTerms: ContractTerms = payStatus.contractTerms; - - if (!contractTerms) { - return ( - <span> - Error: did not get contract terms from merchant or wallet backend. - </span> - ); - } - - let merchantName: VNode; - 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); - if (res.type !== ConfirmPayResultType.Done) { - throw Error("payment pending"); - } - const fu = res.contractTerms.fulfillment_url; - if (fu) { - document.location.href = fu; - } - setPayResult(res); - } catch (e) { - console.error(e); - setPayErrMsg(e.message); - } - }; - - if (payResult && payResult.type === ConfirmPayResultType.Done) { - if (payResult.contractTerms.fulfillment_message) { - const obj = { - fulfillment_message: payResult.contractTerms.fulfillment_message, - fulfillment_message_i18n: - payResult.contractTerms.fulfillment_message_i18n, - }; - const msg = getJsonI18n(obj, "fulfillment_message"); - return ( - <div> - <p>Payment succeeded.</p> - <p>{msg}</p> - </div> - ); - } else { - return <span>Redirecting ...</span>; - } - } - - return ( - <div> - <p> - <i18n.Translate> - The merchant <span>{merchantName}</span> offers you to purchase: - </i18n.Translate> - <div style={{ textAlign: "center" }}> - <strong>{contractTerms.summary}</strong> - </div> - {totalFees ? ( - <i18n.Translate> - The total price is <span>{amount} </span> - (plus <span>{renderAmount(totalFees)}</span> fees). - </i18n.Translate> - ) : ( - <i18n.Translate> - 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 - isLoading={loading} - disabled={insufficientBalance} - onClick={() => doPayment()} - > - {i18n.str`Confirm payment`} - </ProgressButton> - </div> - )} - </div> - ); -} - -/** - * @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} />; -} diff --git a/packages/taler-wallet-webextension/src/wallet/refund.tsx b/packages/taler-wallet-webextension/src/wallet/refund.tsx @@ -1,108 +0,0 @@ -/* - 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 * as wxApi from "../wxApi"; -import { AmountView } from "../renderHtml"; -import { - ApplyRefundResponse, - Amounts, -} from "@gnu-taler/taler-util"; -import { useEffect, useState } from "preact/hooks"; -import { JSX } from "preact/jsx-runtime"; - -interface Props { - talerRefundUri?: string -} - -export function RefundStatusView({ talerRefundUri }: Props): JSX.Element { - const [applyResult, setApplyResult] = useState<ApplyRefundResponse | undefined>(undefined); - const [errMsg, setErrMsg] = useState<string | undefined>(undefined); - - useEffect(() => { - if (!talerRefundUri) return; - const doFetch = async (): Promise<void> => { - try { - const result = await wxApi.applyRefund(talerRefundUri); - setApplyResult(result); - } catch (e) { - console.error(e); - setErrMsg(e.message); - console.log("err message", e.message); - } - }; - doFetch(); - }, [talerRefundUri]); - - console.log("rendering"); - - if (!talerRefundUri) { - return <span>missing taler refund uri</span>; - } - - if (errMsg) { - return <span>Error: {errMsg}</span>; - } - - if (!applyResult) { - return <span>Updating refund status</span>; - } - - return ( - <> - <h2>Refund Status</h2> - <p> - The product <em>{applyResult.info.summary}</em> has received a total - effective refund of{" "} - <AmountView amount={applyResult.amountRefundGranted} />. - </p> - {applyResult.pendingAtExchange ? ( - <p>Refund processing is still in progress.</p> - ) : null} - {!Amounts.isZero(applyResult.amountRefundGone) ? ( - <p> - The refund amount of{" "} - <AmountView amount={applyResult.amountRefundGone} /> - could not be applied. - </p> - ) : null} - </> - ); -} - -/** - * @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} />; -} diff --git a/packages/taler-wallet-webextension/src/wallet/tip.tsx b/packages/taler-wallet-webextension/src/wallet/tip.tsx @@ -1,109 +0,0 @@ -/* - 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 accept or ignore a tip from a merchant. - * - * @author Florian Dold <dold@taler.net> - */ - -import { useEffect, useState } from "preact/hooks"; -import { PrepareTipResult } from "@gnu-taler/taler-util"; -import { AmountView } from "../renderHtml"; -import * as wxApi from "../wxApi"; -import { JSX } from "preact/jsx-runtime"; - -interface Props { - talerTipUri?: string -} - -export function TalerTipDialog({ talerTipUri }: Props): JSX.Element { - const [updateCounter, setUpdateCounter] = useState<number>(0); - const [prepareTipResult, setPrepareTipResult] = useState< - PrepareTipResult | undefined - >(undefined); - - const [tipIgnored, setTipIgnored] = useState(false); - - useEffect(() => { - if (!talerTipUri) return; - const doFetch = async (): Promise<void> => { - const p = await wxApi.prepareTip({ talerTipUri }); - setPrepareTipResult(p); - }; - doFetch(); - }, [talerTipUri, updateCounter]); - - const doAccept = async () => { - if (!prepareTipResult) { - return; - } - await wxApi.acceptTip({ walletTipId: prepareTipResult?.walletTipId }); - setUpdateCounter(updateCounter + 1); - }; - - const doIgnore = () => { - setTipIgnored(true); - }; - - if (!talerTipUri) { - return <span>missing tip uri</span>; - } - - if (tipIgnored) { - return <span>You've ignored the tip.</span>; - } - - if (!prepareTipResult) { - return <span>Loading ...</span>; - } - - if (prepareTipResult.accepted) { - return ( - <span> - Tip from <code>{prepareTipResult.merchantBaseUrl}</code> accepted. Check - your transactions list for more details. - </span> - ); - } else { - return ( - <div> - <p> - The merchant <code>{prepareTipResult.merchantBaseUrl}</code> is - offering you a tip of{" "} - <strong> - <AmountView amount={prepareTipResult.tipAmountEffective} /> - </strong>{" "} - via the exchange <code>{prepareTipResult.exchangeBaseUrl}</code> - </p> - <button onClick={doAccept}>Accept tip</button> - <button onClick={doIgnore}>Ignore</button> - </div> - ); - } -} - -/** - * @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} />; -} diff --git a/packages/taler-wallet-webextension/src/wallet/welcome.tsx b/packages/taler-wallet-webextension/src/wallet/welcome.tsx @@ -1,83 +0,0 @@ -/* - 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 * as wxApi from "../wxApi"; -import { getPermissionsApi } from "../compat"; -import { extendedPermissions } from "../permissions"; -import { Fragment, JSX } from "preact/jsx-runtime"; -import { PermissionsCheckbox } from "../components/PermissionsCheckbox"; -import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; -import { Diagnostics } from "../components/Diagnostics"; - -export 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 -} - -export function Welcome(): JSX.Element { - const [permissionsEnabled, togglePermissions] = useExtendedPermissions() - return ( - <> - <p>Thank you for installing the wallet.</p> - <Diagnostics /> - <h2>Permissions</h2> - <PermissionsCheckbox enabled={permissionsEnabled} onToggle={togglePermissions}/> - <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> - </> - ); -} - -/** - * @deprecated to be removed - */ -export function createWelcomePage(): JSX.Element { - return <Welcome />; -} diff --git a/packages/taler-wallet-webextension/src/wallet/withdraw.stories.tsx b/packages/taler-wallet-webextension/src/wallet/withdraw.stories.tsx @@ -1,66 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - 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/> - */ - -/** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { h } from 'preact'; -import { View, ViewProps } from './withdraw'; - - -export default { - title: 'wallet/withdraw', - component: View, - argTypes: { - }, -}; - -export const WithoutURI = (a: any) => <View {...a} />; -WithoutURI.args = { -} as ViewProps - -export const WithoutDetails = (a: any) => <View {...a} />; -WithoutDetails.args = { - talerWithdrawUri: 'http://something' -} as ViewProps - -export const Cancelled = (a: any) => <View {...a} />; -Cancelled.args = { - talerWithdrawUri: 'http://something', - details: { - amount: 'USD:2', - }, - cancelled: true -} as ViewProps - -export const CompleteWithExchange = (a: any) => <View {...a} />; -CompleteWithExchange.args = { - talerWithdrawUri: 'http://something', - details: { - amount: 'USD:2', - }, - selectedExchange: 'Some exchange' -} as ViewProps - -export const CompleteWithoutExchange = (a: any) => <View {...a} />; -CompleteWithoutExchange.args = { - talerWithdrawUri: 'http://something', - details: { - amount: 'USD:2', - }, -} as ViewProps diff --git a/packages/taler-wallet-webextension/src/wallet/withdraw.tsx b/packages/taler-wallet-webextension/src/wallet/withdraw.tsx @@ -1,173 +0,0 @@ -/* - 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 { i18n } from '@gnu-taler/taler-util' -import { renderAmount } from "../renderHtml"; - -import { useState, useEffect } from "preact/hooks"; -import { - acceptWithdrawal, - onUpdateNotification, - getWithdrawalDetailsForUri, -} from "../wxApi"; -import { WithdrawUriInfoResponse } from "@gnu-taler/taler-util"; -import { JSX } from "preact/jsx-runtime"; - -interface Props { - talerWithdrawUri?: string; -} - -export interface ViewProps { - talerWithdrawUri?: string; - details?: WithdrawUriInfoResponse; - cancelled?: boolean; - selectedExchange?: string; - accept: () => Promise<void>; - setCancelled: (b: boolean) => void; - setSelecting: (b: boolean) => void; -}; - -export function View({ talerWithdrawUri, details, cancelled, selectedExchange, accept, setCancelled, setSelecting }: ViewProps) { - const [state, setState] = useState(1) - setTimeout(() => { - setState(s => s + 1) - }, 1000); - if (!talerWithdrawUri) { - return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>; - } - - if (!details) { - return <span><i18n.Translate>Loading...</i18n.Translate></span>; - } - - if (cancelled) { - return <span><i18n.Translate>Withdraw operation has been cancelled.{state}</i18n.Translate></span>; - } - - return ( - <div> - <h1><i18n.Translate>Digital Cash Withdrawal</i18n.Translate></h1> - <p><i18n.Translate> - You are about to withdraw{" "} - <strong>{renderAmount(details.amount)}</strong> from your bank account - into your wallet. - </i18n.Translate></p> - {selectedExchange ? ( - <p><i18n.Translate> - The exchange <strong>{selectedExchange}</strong> will be used as the - Taler payment service provider. - </i18n.Translate></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> - </div> - </div> - ) -} - -export function WithdrawalDialog({ talerWithdrawUri }: Props): JSX.Element { - const [details, setDetails] = useState<WithdrawUriInfoResponse | undefined>(undefined); - const [selectedExchange, setSelectedExchange] = useState< - string | undefined - >(undefined); - const [cancelled, setCancelled] = useState(false); - const [selecting, setSelecting] = useState(false); - const [errMsg, setErrMsg] = useState<string | undefined>(""); - const [updateCounter, setUpdateCounter] = useState(1); - - useEffect(() => { - return onUpdateNotification(() => { - setUpdateCounter(updateCounter + 1); - }); - }, []); - - useEffect(() => { - if (!talerWithdrawUri) return - const fetchData = async (): Promise<void> => { - const res = await getWithdrawalDetailsForUri({ talerWithdrawUri }); - setDetails(res); - if (res.defaultExchangeBaseUrl) { - setSelectedExchange(res.defaultExchangeBaseUrl); - } - }; - fetchData(); - }, [selectedExchange, errMsg, selecting, talerWithdrawUri, updateCounter]); - - const accept = async (): Promise<void> => { - if (!talerWithdrawUri) return - 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 <View accept={accept} - setCancelled={setCancelled} setSelecting={setSelecting} - cancelled={cancelled} details={details} selectedExchange={selectedExchange} - talerWithdrawUri={talerWithdrawUri} - /> -} - - -/** - * @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} />; -} diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx @@ -25,11 +25,11 @@ import { setupI18n } from "@gnu-taler/taler-util"; import { strings } from "./i18n/strings"; import { createHashHistory } from 'history'; -import { WithdrawalDialog } from "./wallet/withdraw"; -import { Welcome } from "./wallet/welcome"; -import { TalerPayDialog } from "./wallet/pay"; -import { RefundStatusView } from "./wallet/refund"; -import { TalerTipDialog } from './wallet/tip'; +import { WithdrawPage } from "./wallet/Withdraw"; +import { WelcomePage } from "./wallet/Welcome"; +import { PayPage } from "./wallet/Pay"; +import { RefundPage } from "./wallet/Refund"; +import { TipPage } from './wallet/Tip'; import Router, { route, Route } from "preact-router"; @@ -82,7 +82,7 @@ function Application() { </div> <h1>Browser Extension Installed!</h1> <div> - <Welcome /> + <WelcomePage /> </div> </section> }} /> @@ -91,7 +91,7 @@ function Application() { return <section id="main"> <h1>GNU Taler Wallet</h1> <article class="fade"> - <TalerPayDialog talerPayUri={queryParams.talerPayUri} /> + <PayPage talerPayUri={queryParams.talerPayUri} /> </article> </section> }} /> @@ -100,7 +100,7 @@ function Application() { return <section id="main"> <h1>GNU Taler Wallet</h1> <article class="fade"> - <RefundStatusView talerRefundUri={queryParams.talerRefundUri} /> + <RefundPage talerRefundUri={queryParams.talerRefundUri} /> </article> </section> }} /> @@ -109,7 +109,7 @@ function Application() { return <section id="main"> <h1>GNU Taler Wallet</h1> <div> - <TalerTipDialog talerTipUri={queryParams.talerTipUri} /> + <TipPage talerTipUri={queryParams.talerTipUri} /> </div> </section> }} /> @@ -121,7 +121,7 @@ function Application() { </h1> </div> <div class="fade"> - <WithdrawalDialog talerWithdrawUri={queryParams.talerWithdrawUri} /> + <WithdrawPage talerWithdrawUri={queryParams.talerWithdrawUri} /> </div> </section> }} />