diff options
author | Sebastian <sebasjm@gmail.com> | 2022-08-31 00:20:35 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-08-31 00:20:35 -0300 |
commit | d84424202dca22fff22cb1d304286f627642187b (patch) | |
tree | 39b69521a1e5a84c71b7752d355423705941bdd7 | |
parent | 7dc66c2441c4b77cfed0c4add592d4b7d5912ec3 (diff) |
p2p tx rendering
34 files changed, 1565 insertions, 176 deletions
diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts index 19306845a..88bfecd3b 100644 --- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -471,7 +471,7 @@ async function getMergeReserveInfo( export async function acceptPeerPushPayment( ws: InternalWalletState, req: AcceptPeerPushPaymentRequest, -) { +): Promise<void> { const peerInc = await ws.db .mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming })) .runReadOnly(async (tx) => { diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index 1c4873b04..9b222cdb4 100644 --- a/packages/taler-wallet-webextension/src/NavigationBar.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -108,16 +108,20 @@ export const Pages = { "/settings/exchange/add/:currency?", ), - invoice: pageDefinition<{ amount?: string }>("/invoice/:amount?"), - send: pageDefinition<{ amount?: string }>("/send/:amount?"), - cta: pageDefinition<{ action: string }>("/cta/:action"), ctaPay: "/cta/pay", ctaRefund: "/cta/refund", ctaTips: "/cta/tip", ctaWithdraw: "/cta/withdraw", ctaDeposit: "/cta/deposit", - + ctaInvoiceCreate: pageDefinition<{ amount?: string }>( + "/cta/invoice/create/:amount?", + ), + ctaTransferCreate: pageDefinition<{ amount?: string }>( + "/cta/transfer/create/:amount?", + ), + ctaInvoicePay: "/cta/invoice/pay", + ctaTransferPickup: "/cta/transfer/pickup", ctaWithdrawManual: pageDefinition<{ amount?: string }>( "/cta/manual-withdraw/:amount?", ), diff --git a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx index 8033b3ee3..6dcee6eca 100644 --- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx +++ b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx @@ -113,8 +113,58 @@ export function TransactionItem(props: { tx: Transaction }): VNode { pending={tx.pending} /> ); - default: - throw Error("unsupported transaction type"); + case TransactionType.PeerPullCredit: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"credit"} + title={"Invoice credit"} + timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} + iconPath={"I"} + pending={tx.pending} + /> + ); + case TransactionType.PeerPullDebit: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"debit"} + title={"Invoice debit"} + timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} + iconPath={"I"} + pending={tx.pending} + /> + ); + case TransactionType.PeerPushCredit: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"credit"} + title={"Transfer credit"} + timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} + iconPath={"T"} + pending={tx.pending} + /> + ); + case TransactionType.PeerPushDebit: + return ( + <TransactionLayout + id={tx.transactionId} + amount={tx.amountEffective} + debitCreditIndicator={"debit"} + title={"Transfer debit"} + timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} + iconPath={"T"} + pending={tx.pending} + /> + ); + default: { + const pe: never = tx; + throw Error(`unsupported transaction type ${pe}`); + } } } diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts new file mode 100644 index 000000000..f12fd80e0 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts @@ -0,0 +1,79 @@ +/* + This file is part of GNU Taler + (C) 2022 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/> + */ + +import { Loading } from "../../components/Loading.js"; +import { HookError } from "../../hooks/useAsyncAsHook.js"; +import { compose, StateViewMap } from "../../utils/index.js"; +import { LoadingUriView, ReadyView, ShowQrView } from "./views.js"; +import * as wxApi from "../../wxApi.js"; +import { useComponentState } from "./state.js"; +import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { ButtonHandler, SelectFieldHandler, TextFieldHandler } from "../../mui/handlers.js"; + +export interface Props { + amount: string; +} + +export type State = + | State.Loading + | State.LoadingUriError + | State.ShowQr + | State.Ready; + +export namespace State { + + export interface Loading { + status: "loading"; + error: undefined; + } + + export interface LoadingUriError { + status: "loading-uri"; + error: HookError; + } + + export interface BaseInfo { + error: undefined; + } + export interface ShowQr extends BaseInfo { + status: "show-qr"; + talerUri: string; + close: () => void; + } + export interface Ready extends BaseInfo { + status: "ready"; + showQr: ButtonHandler; + copyToClipboard: ButtonHandler; + subject: TextFieldHandler; + toBeReceived: AmountJson, + chosenAmount: AmountJson, + exchangeUrl: string; + invalid: boolean; + error: undefined; + operationError?: TalerErrorDetail; + } +} + +const viewMapping: StateViewMap<State> = { + loading: Loading, + "loading-uri": LoadingUriView, + "show-qr": ShowQrView, + "ready": ReadyView, +}; + + +export const InvoiceCreatePage = compose("InvoiceCreatePage", (p: Props) => useComponentState(p, wxApi), viewMapping) + diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts new file mode 100644 index 000000000..dd9220480 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts @@ -0,0 +1,113 @@ +/* + This file is part of GNU Taler + (C) 2022 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/> + */ + +import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { TalerError } from "@gnu-taler/taler-wallet-core"; +import { useState } from "preact/hooks"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; +import * as wxApi from "../../wxApi.js"; +import { Props, State } from "./index.js"; + +export function useComponentState( + { amount: amountStr }: Props, + api: typeof wxApi, +): State { + const amount = Amounts.parseOrThrow(amountStr) + + const [subject, setSubject] = useState(""); + const [talerUri, setTalerUri] = useState("") + + const hook = useAsyncAsHook(api.listExchanges); + const [exchangeIdx, setExchangeIdx] = useState("0") + const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) + + + if (!hook) { + return { + status: "loading", + error: undefined, + } + } + if (hook.hasError) { + return { + status: "loading-uri", + error: hook, + }; + } + + if (talerUri) { + return { + status: "show-qr", + talerUri, + error: undefined, + close: () => { null }, + // chosenAmount: amount, + // toBeReceived: amount, + } + } + + const exchanges = hook.response.exchanges.filter(e => e.currency === amount.currency); + const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [String(idx)]: cur.exchangeBaseUrl }), {} as Record<string, string>) + const selected = exchanges[Number(exchangeIdx)]; + + async function accept(): Promise<string> { + try { + + const resp = await api.initiatePeerPullPayment({ + amount: Amounts.stringify(amount), + exchangeBaseUrl: selected.exchangeBaseUrl, + partialContractTerms: { + summary: subject + } + }) + return resp.talerUri + } catch (e) { + if (e instanceof TalerError) { + setOperationError(e.errorDetail) + } + console.error(e) + throw Error("error trying to accept") + } + } + + + return { + status: "ready", + subject: { + error: !subject ? "cant be empty" : undefined, + value: subject, + onInput: async (e) => setSubject(e) + }, + invalid: !subject || Amounts.isZero(amount), + exchangeUrl: selected.exchangeBaseUrl, + copyToClipboard: { + onClick: async () => { + const uri = await accept(); + navigator.clipboard.writeText(uri || ""); + } + }, + showQr: { + onClick: async () => { + const uri = await accept(); + setTalerUri(uri) + } + }, + chosenAmount: amount, + toBeReceived: amount, + error: undefined, + operationError + } +} diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx new file mode 100644 index 000000000..e6252062e --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx @@ -0,0 +1,57 @@ +/* + This file is part of GNU Taler + (C) 2022 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 { createExample } from "../../test-utils.js"; +import { ReadyView, ShowQrView } from "./views.js"; + +export default { + title: "wallet/invoice create", +}; + +export const ShowQr = createExample(ShowQrView, { + talerUri: + "taler://pay-pull/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0", + close: () => { + null; + }, +}); + +export const Ready = createExample(ReadyView, { + chosenAmount: { + currency: "ARS", + value: 1, + fraction: 0, + }, + toBeReceived: { + currency: "ARS", + value: 1, + fraction: 0, + }, + exchangeUrl: "https://exchange.taler.ar", + subject: { + value: "some subject", + onInput: async () => { + null; + }, + }, + copyToClipboard: {}, + showQr: {}, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/Invoice/test.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/test.ts index 631e76d01..631e76d01 100644 --- a/packages/taler-wallet-webextension/src/wallet/Invoice/test.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/test.ts diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx new file mode 100644 index 000000000..ebb15e75c --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx @@ -0,0 +1,150 @@ +/* + This file is part of GNU Taler + (C) 2022 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/> + */ + +import { h, VNode } from "preact"; +import { LoadingError } from "../../components/LoadingError.js"; +import { LogoHeader } from "../../components/LogoHeader.js"; +import { Part } from "../../components/Part.js"; +import { QR } from "../../components/QR.js"; +import { + Link, + SubTitle, + SvgIcon, + WalletAction, +} from "../../components/styled/index.js"; +import { useTranslationContext } from "../../context/translation.js"; +import { Button } from "../../mui/Button.js"; +import { Grid } from "../../mui/Grid.js"; +import { TextField } from "../../mui/TextField.js"; +import editIcon from "../../svg/edit_24px.svg"; +import { ExchangeDetails, InvoiceDetails } from "../../wallet/Transaction.js"; +import { State } from "./index.js"; + +export function LoadingUriView({ error }: State.LoadingUriError): VNode { + const { i18n } = useTranslationContext(); + + return ( + <LoadingError + title={<i18n.Translate>Could not load</i18n.Translate>} + error={error} + /> + ); +} + +export function ShowQrView({ talerUri, close }: State.ShowQr): VNode { + const { i18n } = useTranslationContext(); + return ( + <WalletAction> + <LogoHeader /> + <SubTitle> + <i18n.Translate>Digital invoice</i18n.Translate> + </SubTitle> + <section> + <p>Scan this QR code with the wallet</p> + <QR text={talerUri} /> + </section> + <section> + <Link upperCased onClick={close}> + <i18n.Translate>Close</i18n.Translate> + </Link> + </section> + </WalletAction> + ); +} + +export function ReadyView({ + invalid, + exchangeUrl, + subject, + showQr, + operationError, + copyToClipboard, + toBeReceived, + chosenAmount, +}: State.Ready): VNode { + const { i18n } = useTranslationContext(); + + return ( + <WalletAction> + <LogoHeader /> + <SubTitle> + <i18n.Translate>Digital invoice</i18n.Translate> + </SubTitle> + <section style={{ textAlign: "left" }}> + <TextField + label="Subject" + variant="filled" + error={!!subject.error} + required + fullWidth + value={subject.value} + onChange={subject.onInput} + /> + + <Part + title={ + <div + style={{ + display: "flex", + alignItems: "center", + }} + > + <i18n.Translate>Exchange</i18n.Translate> + <Link> + <SvgIcon + title="Edit" + dangerouslySetInnerHTML={{ __html: editIcon }} + color="black" + /> + </Link> + </div> + } + text={<ExchangeDetails exchange={exchangeUrl} />} + kind="neutral" + big + /> + + <Part + title={<i18n.Translate>Details</i18n.Translate>} + text={ + <InvoiceDetails + amount={{ + effective: toBeReceived, + raw: chosenAmount, + }} + /> + } + /> + </section> + <section> + <p>How do you want to send the invoice?</p> + + <Grid item container columns={1} spacing={1}> + <Grid item xs={1}> + <Button disabled={invalid} onClick={copyToClipboard.onClick}> + Copy request URI to clipboard + </Button> + </Grid> + <Grid item xs={1}> + <Button disabled={invalid} onClick={showQr.onClick}> + Show QR + </Button> + </Grid> + </Grid> + </section> + </WalletAction> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/Invoice/index.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts index 20d902e65..8d1612c7a 100644 --- a/packages/taler-wallet-webextension/src/wallet/Invoice/index.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts @@ -14,17 +14,17 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; +import { ButtonHandler } from "../../mui/handlers.js"; import { compose, StateViewMap } from "../../utils/index.js"; -import { LoadingUriView, ReadyView } from "./views.js"; import * as wxApi from "../../wxApi.js"; import { useComponentState } from "./state.js"; -import { AmountJson } from "@gnu-taler/taler-util"; -import { TextFieldHandler } from "../../mui/handlers.js"; +import { LoadingUriView, ReadyView } from "./views.js"; export interface Props { - p: string; + talerPayPullUri: string; } export type State = @@ -49,9 +49,10 @@ export namespace State { } export interface Ready extends BaseInfo { status: "ready"; - amount: AmountJson; - subject: TextFieldHandler; + amount: AmountJson, error: undefined; + accept: ButtonHandler; + operationError?: TalerErrorDetail; } } @@ -62,5 +63,5 @@ const viewMapping: StateViewMap<State> = { }; -export const InvoicePage = compose("InvoicePage", (p: Props) => useComponentState(p, wxApi), viewMapping) +export const InvoicePayPage = compose("InvoicePayPage", (p: Props) => useComponentState(p, wxApi), viewMapping) diff --git a/packages/taler-wallet-webextension/src/wallet/Send/state.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts index 1359c1804..53db117f9 100644 --- a/packages/taler-wallet-webextension/src/wallet/Send/state.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts @@ -14,21 +14,23 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts } from "@gnu-taler/taler-util"; +import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { TalerError } from "@gnu-taler/taler-wallet-core"; import { useState } from "preact/hooks"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import * as wxApi from "../../wxApi.js"; import { Props, State } from "./index.js"; export function useComponentState( - { p }: Props, + { talerPayPullUri }: Props, api: typeof wxApi, ): State { - const [subject, setSubject] = useState(""); - const amount = Amounts.parseOrThrow("ARS:0") - - const hook = useAsyncAsHook(api.listExchanges); - const [exchangeIdx, setExchangeIdx] = useState("0") + const hook = useAsyncAsHook(async () => { + return await api.checkPeerPullPayment({ + talerUri: talerPayPullUri + }) + }, []) + const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) if (!hook) { return { @@ -43,24 +45,30 @@ export function useComponentState( }; } - const exchanges = hook.response.exchanges; - const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }), {} as Record<string, string>) - const selected = exchanges[Number(exchangeIdx)]; + const { amount, peerPullPaymentIncomingId } = hook.response + + async function accept(): Promise<void> { + try { + const resp = await api.acceptPeerPullPayment({ + peerPullPaymentIncomingId + }) + } catch (e) { + if (e instanceof TalerError) { + setOperationError(e.errorDetail) + } + console.error(e) + throw Error("error trying to accept") + } + } + return { status: "ready", - exchange: { - list: exchangeMap, - value: exchangeIdx, - onChange: async (v) => { - setExchangeIdx(v) - } - }, - subject: { - value: subject, - onInput: async (e) => setSubject(e) - }, - amount, + amount: Amounts.parseOrThrow(amount), error: undefined, + accept: { + onClick: accept + }, + operationError } } diff --git a/packages/taler-wallet-webextension/src/wallet/Send/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx index 75f78be1d..6adaa5c22 100644 --- a/packages/taler-wallet-webextension/src/wallet/Send/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx @@ -23,7 +23,14 @@ import { createExample } from "../../test-utils.js"; import { ReadyView } from "./views.js"; export default { - title: "wallet/invoice", + title: "wallet/invoice payment", }; -export const Ready = createExample(ReadyView, {}); +export const Ready = createExample(ReadyView, { + amount: { + currency: "ARS", + value: 1, + fraction: 0, + }, + accept: {}, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/Send/test.ts b/packages/taler-wallet-webextension/src/cta/InvoicePay/test.ts index 631e76d01..631e76d01 100644 --- a/packages/taler-wallet-webextension/src/wallet/Send/test.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/test.ts diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx new file mode 100644 index 000000000..2960c3168 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx @@ -0,0 +1,85 @@ +/* + This file is part of GNU Taler + (C) 2022 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/> + */ + +import { h, VNode } from "preact"; +import { Amount } from "../../components/Amount.js"; +import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; +import { LoadingError } from "../../components/LoadingError.js"; +import { LogoHeader } from "../../components/LogoHeader.js"; +import { Part } from "../../components/Part.js"; +import { QR } from "../../components/QR.js"; +import { + Link, + SubTitle, + SvgIcon, + WalletAction, +} from "../../components/styled/index.js"; +import { useTranslationContext } from "../../context/translation.js"; +import { Button } from "../../mui/Button.js"; +import { Grid } from "../../mui/Grid.js"; +import { TextField } from "../../mui/TextField.js"; +import editIcon from "../../svg/edit_24px.svg"; +import { ExchangeDetails, InvoiceDetails } from "../../wallet/Transaction.js"; +import { State } from "./index.js"; + +export function LoadingUriView({ error }: State.LoadingUriError): VNode { + const { i18n } = useTranslationContext(); + + return ( + <LoadingError + title={<i18n.Translate>Could not load</i18n.Translate>} + error={error} + /> + ); +} + +export function ReadyView({ + operationError, + accept, + amount, +}: State.Ready): VNode { + const { i18n } = useTranslationContext(); + + return ( + <WalletAction> + <LogoHeader /> + <SubTitle> + <i18n.Translate>Digital invoice</i18n.Translate> + </SubTitle> + {operationError && ( + <ErrorTalerOperation + title={ + <i18n.Translate> + Could not finish the payment operation + </i18n.Translate> + } + error={operationError} + /> + )} + <section style={{ textAlign: "left" }}> + <Part + title={<i18n.Translate>Amount</i18n.Translate>} + text={<Amount value={amount} />} + /> + </section> + <section> + <Button variant="contained" color="success" onClick={accept.onClick}> + <i18n.Translate>Pay</i18n.Translate> + </Button> + </section> + </WalletAction> + ); +} diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts new file mode 100644 index 000000000..fd034341e --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts @@ -0,0 +1,78 @@ +/* + This file is part of GNU Taler + (C) 2022 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/> + */ + +import { Loading } from "../../components/Loading.js"; +import { HookError } from "../../hooks/useAsyncAsHook.js"; +import { compose, StateViewMap } from "../../utils/index.js"; +import { LoadingUriView, ReadyView, ShowQrView } from "./views.js"; +import * as wxApi from "../../wxApi.js"; +import { useComponentState } from "./state.js"; +import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { ButtonHandler, SelectFieldHandler, TextFieldHandler } from "../../mui/handlers.js"; + +export interface Props { + amount: string; +} + +export type State = + | State.Loading + | State.LoadingUriError + | State.ShowQr + | State.Ready; + +export namespace State { + + export interface Loading { + status: "loading"; + error: undefined; + } + + export interface LoadingUriError { + status: "loading-uri"; + error: HookError; + } + + export interface BaseInfo { + error: undefined; + } + export interface ShowQr extends BaseInfo { + status: "show-qr"; + talerUri: string; + close: () => void; + } + export interface Ready extends BaseInfo { + status: "ready"; + showQr: ButtonHandler; + invalid: boolean; + copyToClipboard: ButtonHandler; + toBeReceived: AmountJson, + chosenAmount: AmountJson, + subject: TextFieldHandler, + error: undefined; + operationError?: TalerErrorDetail; + } +} + +const viewMapping: StateViewMap<State> = { + loading: Loading, + "loading-uri": LoadingUriView, + "show-qr": ShowQrView, + "ready": ReadyView, +}; + + +export const TransferCreatePage = compose("TransferCreatePage", (p: Props) => useComponentState(p, wxApi), viewMapping) + diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts new file mode 100644 index 000000000..9853f05ff --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts @@ -0,0 +1,86 @@ +/* + This file is part of GNU Taler + (C) 2022 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/> + */ + +import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { TalerError } from "@gnu-taler/taler-wallet-core"; +import { useState } from "preact/hooks"; +import * as wxApi from "../../wxApi.js"; +import { Props, State } from "./index.js"; + +export function useComponentState( + { amount: amountStr }: Props, + api: typeof wxApi, +): State { + const amount = Amounts.parseOrThrow(amountStr) + + const [subject, setSubject] = useState(""); + const [talerUri, setTalerUri] = useState("") + const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) + + + if (talerUri) { + return { + status: "show-qr", + talerUri, + error: undefined, + close: () => { null }, + } + } + + + async function accept(): Promise<string> { + try { + + const resp = await api.initiatePeerPushPayment({ + amount: Amounts.stringify(amount), + partialContractTerms: { + summary: subject + } + }) + return resp.talerUri + } catch (e) { + if (e instanceof TalerError) { + setOperationError(e.errorDetail) + } + console.error(e) + throw Error("error trying to accept") + } + } + return { + status: "ready", + invalid: !subject || Amounts.isZero(amount), + subject: { + value: subject, + onInput: async (e) => setSubject(e) + }, + copyToClipboard: { + onClick: async () => { + const uri = await accept(); + navigator.clipboard.writeText(uri || ""); + } + }, + showQr: { + onClick: async () => { + const uri = await accept(); + setTalerUri(uri) + } + }, + chosenAmount: amount, + toBeReceived: amount, + error: undefined, + operationError + } +} diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx new file mode 100644 index 000000000..1e528df73 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx @@ -0,0 +1,56 @@ +/* + This file is part of GNU Taler + (C) 2022 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 { createExample } from "../../test-utils.js"; +import { ReadyView, ShowQrView } from "./views.js"; + +export default { + title: "wallet/transfer create", +}; + +export const ShowQr = createExample(ShowQrView, { + talerUri: + "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0", + close: () => { + null; + }, +}); + +export const Ready = createExample(ReadyView, { + chosenAmount: { + currency: "ARS", + value: 1, + fraction: 0, + }, + toBeReceived: { + currency: "ARS", + value: 1, + fraction: 0, + }, + copyToClipboard: {}, + showQr: {}, + subject: { + value: "the subject", + onInput: async () => { + null; + }, + }, +}); diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/test.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/test.ts new file mode 100644 index 000000000..631e76d01 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/test.ts @@ -0,0 +1,31 @@ +/* + This file is part of GNU Taler + (C) 2022 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 { expect } from "chai"; + +describe("test description", () => { + + it("should assert", () => { + + expect([]).deep.equals([]) + }); +}) + diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx new file mode 100644 index 000000000..ebdf1e746 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx @@ -0,0 +1,117 @@ +/* + This file is part of GNU Taler + (C) 2022 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/> + */ + +import { h, VNode } from "preact"; +import { LoadingError } from "../../components/LoadingError.js"; +import { LogoHeader } from "../../components/LogoHeader.js"; +import { Part } from "../../components/Part.js"; +import { QR } from "../../components/QR.js"; +import { Link, SubTitle, WalletAction } from "../../components/styled/index.js"; +import { useTranslationContext } from "../../context/translation.js"; +import { Button } from "../../mui/Button.js"; +import { Grid } from "../../mui/Grid.js"; +import { TextField } from "../../mui/TextField.js"; +import { TransferDetails } from "../../wallet/Transaction.js"; +import { State } from "./index.js"; + +export function LoadingUriView({ error }: State.LoadingUriError): VNode { + const { i18n } = useTranslationContext(); + + return ( + <LoadingError + title={<i18n.Translate>Could not load</i18n.Translate>} + error={error} + /> + ); +} + +export function ShowQrView({ talerUri, close }: State.ShowQr): VNode { + const { i18n } = useTranslationContext(); + return ( + <WalletAction> + <LogoHeader /> + <SubTitle> + <i18n.Translate>Digital invoice</i18n.Translate> + </SubTitle> + <section> + <p>Scan this QR code with the wallet</p> + <QR text={talerUri} /> + </section> + <section> + <Link upperCased onClick={close}> + <i18n.Translate>Close</i18n.Translate> + </Link> + </section> + </WalletAction> + ); +} + +export function ReadyView({ + subject, + toBeReceived, + chosenAmount, + showQr, + copyToClipboard, + invalid, +}: State.Ready): VNode { + const { i18n } = useTranslationContext(); + return ( + <WalletAction> + <LogoHeader /> + <SubTitle> + <i18n.Translate>Digital cash transfer</i18n.Translate> + </SubTitle> + <section style={{ textAlign: "left" }}> + <TextField + label="Subject" + variant="filled" + error={!!subject.error} + required + fullWidth + value={subject.value} + onChange={subject.onInput} + /> + <Part + title={<i18n.Translate>Details</i18n.Translate>} + text={ + <TransferDetails + amount={{ + effective: toBeReceived, + raw: chosenAmount, + }} + /> + } + /> + </section> + <section> + <p>How do you want to transfer?</p> + + <Grid item container columns={1} spacing={1}> + <Grid item xs={1}> + <Button disabled={invalid} onClick={copyToClipboard.onClick}> + Copy transfer URI to clipboard + </Button> + </Grid> + <Grid item xs={1}> + <Button disabled={invalid} onClick={showQr.onClick}> + Show QR + </Button> + </Grid> + </Grid> + </section> + </WalletAction> + ); +} diff --git a/packages/taler-wallet-webextension/src/wallet/Send/index.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts index fb69c6280..603200747 100644 --- a/packages/taler-wallet-webextension/src/wallet/Send/index.ts +++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts @@ -14,17 +14,17 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util"; import { Loading } from "../../components/Loading.js"; import { HookError } from "../../hooks/useAsyncAsHook.js"; +import { ButtonHandler } from "../../mui/handlers.js"; import { compose, StateViewMap } from "../../utils/index.js"; -import { LoadingUriView, ReadyView } from "./views.js"; import * as wxApi from "../../wxApi.js"; import { useComponentState } from "./state.js"; -import { AmountJson } from "@gnu-taler/taler-util"; -import { SelectFieldHandler, TextFieldHandler } from "../../mui/handlers.js"; +import { LoadingUriView, ReadyView } from "./views.js"; export interface Props { - p: string; + talerPayPushUri: string; } export type State = @@ -49,10 +49,10 @@ export namespace State { } export interface Ready extends BaseInfo { status: "ready"; - amount: AmountJson; - exchange: SelectFieldHandler, - subject: TextFieldHandler, + amount: AmountJson, error: undefined; + accept: ButtonHandler; + operationError?: TalerErrorDetail; } } @@ -63,5 +63,5 @@ const viewMapping: StateViewMap<State> = { }; -export const SendPage = compose("SendPage", (p: Props) => useComponentState(p, wxApi), viewMapping) +export const TransferPickupPage = compose("TransferPickupPage", (p: Props) => useComponentState(p, wxApi), viewMapping) diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts new file mode 100644 index 000000000..a16389709 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts @@ -0,0 +1,72 @@ +/* + This file is part of GNU Taler + (C) 2022 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/> + */ + +import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { TalerError } from "@gnu-taler/taler-wallet-core"; +import { useState } from "preact/hooks"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; +import * as wxApi from "../../wxApi.js"; +import { Props, State } from "./index.js"; + +export function useComponentState( + { talerPayPushUri }: Props, + api: typeof wxApi, +): State { + const hook = useAsyncAsHook(async () => { + return await api.checkPeerPushPayment({ + talerUri: talerPayPushUri, + }) + }, []) + const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) + + if (!hook) { + return { + status: "loading", + error: undefined, + } + } + if (hook.hasError) { + return { + status: "loading-uri", + error: hook, + }; + } + + const { amount, peerPushPaymentIncomingId } = hook.response + + async function accept(): Promise<void> { + try { + const resp = await api.acceptPeerPushPayment({ + peerPushPaymentIncomingId + }) + } catch (e) { + if (e instanceof TalerError) { + setOperationError(e.errorDetail) + } + console.error(e) + throw Error("error trying to accept") + } + } + return { + status: "ready", + amount: Amounts.parseOrThrow(amount), + error: undefined, + accept: { + onClick: accept + }, + operationError + } +} diff --git a/packages/taler-wallet-webextension/src/wallet/Invoice/stories.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx index 75f78be1d..7a3ccb60d 100644 --- a/packages/taler-wallet-webextension/src/wallet/Invoice/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx @@ -23,7 +23,14 @@ import { createExample } from "../../test-utils.js"; import { ReadyView } from "./views.js"; export default { - title: "wallet/invoice", + title: "wallet/transfer pickup", }; -export const Ready = createExample(ReadyView, {}); +export const Ready = createExample(ReadyView, { + amount: { + currency: "ARS", + value: 1, + fraction: 0, + }, + accept: {}, +}); diff --git a/packages/taler-wallet-webextension/src/cta/TransferPickup/test.ts b/packages/taler-wallet-webextension/src/cta/TransferPickup/test.ts new file mode 100644 index 000000000..631e76d01 --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/test.ts @@ -0,0 +1,31 @@ +/* + This file is part of GNU Taler + (C) 2022 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 { expect } from "chai"; + +describe("test description", () => { + + it("should assert", () => { + + expect([]).deep.equals([]) + }); +}) + diff --git a/packages/taler-wallet-webextension/src/wallet/Invoice/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx index 94e8f8625..22ef8c776 100644 --- a/packages/taler-wallet-webextension/src/wallet/Invoice/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx @@ -14,13 +14,15 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts } from "@gnu-taler/taler-util"; -import { styled } from "@linaria/react"; import { h, VNode } from "preact"; +import { Amount } from "../../components/Amount.js"; +import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { LoadingError } from "../../components/LoadingError.js"; +import { LogoHeader } from "../../components/LogoHeader.js"; +import { Part } from "../../components/Part.js"; +import { SubTitle, WalletAction } from "../../components/styled/index.js"; import { useTranslationContext } from "../../context/translation.js"; import { Button } from "../../mui/Button.js"; -import { TextField } from "../../mui/TextField.js"; import { State } from "./index.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { @@ -34,23 +36,39 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode { ); } -const Container = styled.div``; - -export function ReadyView({ amount, subject }: State.Ready): VNode { +export function ReadyView({ + accept, + amount, + operationError, +}: State.Ready): VNode { const { i18n } = useTranslationContext(); - return ( - <Container> - <p>Creating an invoice of {Amounts.stringify(amount)}</p> - <TextField - label="Subject" - variant="filled" - required - value={subject.value} - onChange={subject.onInput} - /> - <p>to:</p> - <Button>Scan QR code</Button> - </Container> + <WalletAction> + <LogoHeader /> + <SubTitle> + <i18n.Translate>Digital cash transfer</i18n.Translate> + </SubTitle> + {operationError && ( + <ErrorTalerOperation + title={ + <i18n.Translate> + Could not finish the pickup operation + </i18n.Translate> + } + error={operationError} + /> + )} + <section style={{ textAlign: "left" }}> + <Part + title={<i18n.Translate>Amount</i18n.Translate>} + text={<Amount value={amount} />} + /> + </section> + <section> + <Button variant="contained" color="success" onClick={accept.onClick}> + <i18n.Translate>Pickup</i18n.Translate> + </Button> + </section> + </WalletAction> ); } diff --git a/packages/taler-wallet-webextension/src/cta/index.stories.ts b/packages/taler-wallet-webextension/src/cta/index.stories.ts index 92f4bbcb1..2f0ef33fb 100644 --- a/packages/taler-wallet-webextension/src/cta/index.stories.ts +++ b/packages/taler-wallet-webextension/src/cta/index.stories.ts @@ -25,5 +25,9 @@ import * as a4 from "./Refund/stories.jsx"; import * as a5 from "./Tip/stories.jsx"; import * as a6 from "./Withdraw/stories.jsx"; import * as a7 from "./TermsOfServiceSection.stories.js"; +import * as a8 from "./InvoiceCreate/stories.js"; +import * as a9 from "./InvoicePay/stories.js"; +import * as a10 from "./TransferCreate/stories.js"; +import * as a11 from "./TransferPickup/stories.js"; -export default [a1, a3, a4, a5, a6, a7]; +export default [a1, a3, a4, a5, a6, a7, a8, a9, a10, a11]; diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index 21fc180c3..f2c7e9d2d 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -201,11 +201,33 @@ function openWalletURIFromPopup(talerUri: string): void { `static/wallet.html#/cta/refund?talerRefundUri=${talerUri}`, ); break; - default: + case TalerUriType.TalerPayPull: + url = chrome.runtime.getURL( + `static/wallet.html#/cta/invoice/pay?talerPayPullUri=${talerUri}`, + ); + break; + case TalerUriType.TalerPayPush: + url = chrome.runtime.getURL( + `static/wallet.html#/cta/transfer/pickup?talerPayPushUri=${talerUri}`, + ); + break; + case TalerUriType.TalerNotifyReserve: logger.warn( - "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", + `Response with HTTP 402 the Taler header but it is deprecated ${talerUri}`, + ); + break; + case TalerUriType.Unknown: + logger.warn( + `Response with HTTP 402 the Taler header but could not classify ${talerUri}`, + ); + return; + default: { + const error: never = uriType; + logger.warn( + `Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`, ); return; + } } chrome.tabs.create({ active: true, url }, () => { diff --git a/packages/taler-wallet-webextension/src/stories.tsx b/packages/taler-wallet-webextension/src/stories.tsx index a032222a0..09261d6f7 100644 --- a/packages/taler-wallet-webextension/src/stories.tsx +++ b/packages/taler-wallet-webextension/src/stories.tsx @@ -181,10 +181,11 @@ function getContentForExample(item: ExampleItem | undefined): () => VNode { item.component, item.name, ); - if (!example) + if (!example) { return function ExampleNotFoundMessage() { return <div>example not found</div>; }; + } return () => example.render(example.render.args); } @@ -314,7 +315,9 @@ function ErrorReport({ children: ComponentChild; selected: ExampleItem | undefined; }): VNode { - const [error] = useErrorBoundary(); + const [error, resetError] = useErrorBoundary(); + //if there is an error, reset when unloading this component + useEffect(() => (error ? resetError : undefined)); if (error) { return ( <div> diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index caed45d33..253d5fba7 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -60,8 +60,10 @@ import { DestinationSelectionSendCash, } from "./DestinationSelection.js"; import { ExchangeSelectionPage } from "./ExchangeSelection/index.js"; -import { InvoicePage } from "./Invoice/index.js"; -import { SendPage } from "./Send/index.js"; +import { TransferCreatePage } from "../cta/TransferCreate/index.js"; +import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js"; +import { TransferPickupPage } from "../cta/TransferPickup/index.js"; +import { InvoicePayPage } from "../cta/InvoicePay/index.js"; export function Application(): VNode { const [globalNotification, setGlobalNotification] = useState< @@ -153,7 +155,7 @@ export function Application(): VNode { redirectTo(Pages.balanceDeposit({ amount })) } goToWalletWalletSend={(amount: string) => - redirectTo(Pages.send({ amount })) + redirectTo(Pages.ctaTransferCreate({ amount })) } /> <Route @@ -163,11 +165,9 @@ export function Application(): VNode { redirectTo(Pages.ctaWithdrawManual({ amount })) } goToWalletWalletInvoice={(amount?: string) => - redirectTo(Pages.invoice({ amount })) + redirectTo(Pages.ctaInvoiceCreate({ amount })) } /> - <Route path={Pages.invoice.pattern} component={InvoicePage} /> - <Route path={Pages.send.pattern} component={SendPage} /> <Route path={Pages.balanceTransaction.pattern} @@ -275,6 +275,20 @@ export function Application(): VNode { component={DepositPageCTA} cancel={() => redirectTo(Pages.balance)} /> + <Route + path={Pages.ctaInvoiceCreate.pattern} + component={InvoiceCreatePage} + /> + <Route + path={Pages.ctaTransferCreate.pattern} + component={TransferCreatePage} + /> + + <Route path={Pages.ctaInvoicePay} component={InvoicePayPage} /> + <Route + path={Pages.ctaTransferPickup} + component={TransferPickupPage} + /> {/** * NOT FOUND diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx index 9577bb9d7..335d5ea9c 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx @@ -25,6 +25,10 @@ import { TransactionCommon, TransactionDeposit, TransactionPayment, + TransactionPeerPullCredit, + TransactionPeerPullDebit, + TransactionPeerPushCredit, + TransactionPeerPushDebit, TransactionRefresh, TransactionRefund, TransactionTip, @@ -118,6 +122,31 @@ const exampleData = { }, refundPending: undefined, } as TransactionRefund, + push_credit: { + ...commonTransaction(), + type: TransactionType.PeerPushCredit, + + exchangeBaseUrl: "https://exchange.taler.net", + } as TransactionPeerPushCredit, + push_debit: { + ...commonTransaction(), + type: TransactionType.PeerPushDebit, + talerUri: + "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0", + exchangeBaseUrl: "https://exchange.taler.net", + } as TransactionPeerPushDebit, + pull_credit: { + ...commonTransaction(), + type: TransactionType.PeerPullCredit, + talerUri: + "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0", + exchangeBaseUrl: "https://exchange.taler.net", + } as TransactionPeerPullCredit, + pull_debit: { + ...commonTransaction(), + type: TransactionType.PeerPullDebit, + exchangeBaseUrl: "https://exchange.taler.net", + } as TransactionPeerPullDebit, }; export const NoBalance = createExample(TestedComponent, { @@ -327,3 +356,21 @@ export const FiveOfficialCurrenciesWithHighValue = createExample( ], }, ); + +export const PeerToPeer = createExample(TestedComponent, { + transactions: [ + exampleData.pull_credit, + exampleData.pull_debit, + exampleData.push_credit, + exampleData.push_debit, + ], + balances: [ + { + available: "USD:10", + pendingIncoming: "USD:0", + pendingOutgoing: "USD:0", + hasPendingTransactions: false, + requiresUserInput: false, + }, + ], +}); diff --git a/packages/taler-wallet-webextension/src/wallet/Invoice/state.ts b/packages/taler-wallet-webextension/src/wallet/Invoice/state.ts deleted file mode 100644 index 48cfd359b..000000000 --- a/packages/taler-wallet-webextension/src/wallet/Invoice/state.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 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/> - */ - -import { Amounts } from "@gnu-taler/taler-util"; -import { useState } from "preact/hooks"; -import * as wxApi from "../../wxApi.js"; -import { Props, State } from "./index.js"; - -export function useComponentState( - { p }: Props, - api: typeof wxApi, -): State { - const [subject, setSubject] = useState(""); - const amount = Amounts.parseOrThrow("ARS:0") - return { - status: "ready", - subject: { - value: subject, - onInput: async (e) => setSubject(e) - }, - amount, - error: undefined, - } -} diff --git a/packages/taler-wallet-webextension/src/wallet/Send/views.tsx b/packages/taler-wallet-webextension/src/wallet/Send/views.tsx deleted file mode 100644 index 63310f443..000000000 --- a/packages/taler-wallet-webextension/src/wallet/Send/views.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 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/> - */ - -import { Amounts } from "@gnu-taler/taler-util"; -import { styled } from "@linaria/react"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { LoadingError } from "../../components/LoadingError.js"; -import { SelectList } from "../../components/SelectList.js"; -import { Input } from "../../components/styled/index.js"; -import { useTranslationContext } from "../../context/translation.js"; -import { Button } from "../../mui/Button.js"; -import { TextField } from "../../mui/TextField.js"; -import { State } from "./index.js"; - -export function LoadingUriView({ error }: State.LoadingUriError): VNode { - const { i18n } = useTranslationContext(); - - return ( - <LoadingError - title={<i18n.Translate>Could not load</i18n.Translate>} - error={error} - /> - ); -} - -const Container = styled.div``; - -export function ReadyView({ amount, exchange, subject }: State.Ready): VNode { - const { i18n } = useTranslationContext(); - return ( - <Container> - <p>Sending {Amounts.stringify(amount)}</p> - <TextField - label="Subject" - variant="filled" - required - value={subject.value} - onChange={subject.onInput} - /> - <p>to:</p> - <Button>Scan QR code</Button> - </Container> - ); -} diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx index ba61e35f3..6c591611a 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx @@ -25,6 +25,10 @@ import { TransactionCommon, TransactionDeposit, TransactionPayment, + TransactionPeerPullCredit, + TransactionPeerPullDebit, + TransactionPeerPushCredit, + TransactionPeerPushDebit, TransactionRefresh, TransactionRefund, TransactionTip, @@ -139,6 +143,30 @@ const exampleData = { }, refundPending: undefined, } as TransactionRefund, + push_credit: { + ...commonTransaction, + type: TransactionType.PeerPushCredit, + exchangeBaseUrl: "https://exchange.taler.net", + } as TransactionPeerPushCredit, + push_debit: { + ...commonTransaction, + type: TransactionType.PeerPushDebit, + talerUri: + "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0", + exchangeBaseUrl: "https://exchange.taler.net", + } as TransactionPeerPushDebit, + pull_credit: { + ...commonTransaction, + type: TransactionType.PeerPullCredit, + talerUri: + "taler://pay-push/exchange.taler.ar/HS585JK0QCXHJ8Z8QWZA3EBAY5WY7XNC1RR2MHJXSH2Z4WP0YPJ0", + exchangeBaseUrl: "https://exchange.taler.net", + } as TransactionPeerPullCredit, + pull_debit: { + ...commonTransaction, + type: TransactionType.PeerPullDebit, + exchangeBaseUrl: "https://exchange.taler.net", + } as TransactionPeerPullDebit, }; const transactionError = { @@ -498,3 +526,19 @@ export const RefundError = createExample(TestedComponent, { export const RefundPending = createExample(TestedComponent, { transaction: { ...exampleData.refund, pending: true }, }); + +export const InvoiceCredit = createExample(TestedComponent, { + transaction: { ...exampleData.pull_credit }, +}); + +export const InvoiceDebit = createExample(TestedComponent, { + transaction: { ...exampleData.pull_debit }, +}); + +export const TransferCredit = createExample(TestedComponent, { + transaction: { ...exampleData.push_credit }, +}); + +export const TransferDebit = createExample(TestedComponent, { + transaction: { ...exampleData.push_debit }, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index ff3b70b65..c8c4e3ae1 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -45,6 +45,7 @@ import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js"; import { Loading } from "../components/Loading.js"; import { LoadingError } from "../components/LoadingError.js"; import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js"; +import { QR } from "../components/QR.js"; import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js"; import { CenteredDialog, @@ -557,6 +558,172 @@ export function TransactionView({ ); } + function ShowQrWithCopy({ text }: { text: string }): VNode { + const [showing, setShowing] = useState(false); + async function copy(): Promise<void> { + navigator.clipboard.writeText(text); + } + async function toggle(): Promise<void> { + setShowing((s) => !s); + } + if (showing) { + return ( + <div> + <QR text={text} /> + <Button onClick={copy}>copy</Button> + <Button onClick={toggle}>hide qr</Button> + </div> + ); + } + return ( + <div> + <div>{text.substring(0, 64)}...</div> + <Button onClick={copy}>copy</Button> + <Button onClick={toggle}>show qr</Button> + </div> + ); + } + + if (transaction.type === TransactionType.PeerPullCredit) { + const total = Amounts.parseOrThrow(transaction.amountEffective); + return ( + <TransactionTemplate> + <Header + timestamp={transaction.timestamp} + type={i18n.str`Credit`} + total={total} + kind="positive" + > + Invoice + </Header> + + <Part + title={<i18n.Translate>Exchange</i18n.Translate>} + text={transaction.exchangeBaseUrl} + kind="neutral" + /> + <Part + title={<i18n.Translate>URI</i18n.Translate>} + text={<ShowQrWithCopy text={transaction.talerUri} />} + kind="neutral" + /> + <Part + title={<i18n.Translate>Details</i18n.Translate>} + text={ + <InvoiceDetails + amount={{ + effective: Amounts.parseOrThrow(transaction.amountEffective), + raw: Amounts.parseOrThrow(transaction.amountRaw), + }} + /> + } + /> + </TransactionTemplate> + ); + } + + if (transaction.type === TransactionType.PeerPullDebit) { + const total = Amounts.parseOrThrow(transaction.amountEffective); + return ( + <TransactionTemplate> + <Header + timestamp={transaction.timestamp} + type={i18n.str`Debit`} + total={total} + kind="negative" + > + Invoice + </Header> + + <Part + title={<i18n.Translate>Exchange</i18n.Translate>} + text={transaction.exchangeBaseUrl} + kind="neutral" + /> + <Part + title={<i18n.Translate>Details</i18n.Translate>} + text={ + <InvoiceDetails + amount={{ + effective: Amounts.parseOrThrow(transaction.amountEffective), + raw: Amounts.parseOrThrow(transaction.amountRaw), + }} + /> + } + /> + </TransactionTemplate> + ); + } + if (transaction.type === TransactionType.PeerPushDebit) { + const total = Amounts.parseOrThrow(transaction.amountEffective); + return ( + <TransactionTemplate> + <Header + timestamp={transaction.timestamp} + type={i18n.str`Debit`} + total={total} + kind="negative" + > + Transfer + </Header> + + <Part + title={<i18n.Translate>Exchange</i18n.Translate>} + text={transaction.exchangeBaseUrl} + kind="neutral" + /> + <Part + title={<i18n.Translate>URI</i18n.Translate>} + text={<QR text={transaction.talerUri} />} + kind="neutral" + /> + <Part + title={<i18n.Translate>Details</i18n.Translate>} + text={ + <TransferDetails + amount={{ + effective: Amounts.parseOrThrow(transaction.amountEffective), + raw: Amounts.parseOrThrow(transaction.amountRaw), + }} + /> + } + /> + </TransactionTemplate> + ); + } + + if (transaction.type === TransactionType.PeerPushCredit) { + const total = Amounts.parseOrThrow(transaction.amountEffective); + return ( + <TransactionTemplate> + <Header + timestamp={transaction.timestamp} + type={i18n.str`Credit`} + total={total} + kind="positive" + > + Transfer + </Header> + + <Part + title={<i18n.Translate>Exchange</i18n.Translate>} + text={transaction.exchangeBaseUrl} + kind="neutral" + /> + <Part + title={<i18n.Translate>Details</i18n.Translate>} + text={ + <TransferDetails + amount={{ + effective: Amounts.parseOrThrow(transaction.amountEffective), + raw: Amounts.parseOrThrow(transaction.amountRaw), + }} + /> + } + /> + </TransactionTemplate> + ); + } return <div />; } @@ -736,6 +903,88 @@ export interface AmountWithFee { raw: AmountJson; } +export function InvoiceDetails({ amount }: { amount: AmountWithFee }): VNode { + const { i18n } = useTranslationContext(); + + const fee = Amounts.sub(amount.raw, amount.effective).amount; + + const maxFrac = [amount.raw, amount.effective, fee] + .map((a) => Amounts.maxFractionalDigits(a)) + .reduce((c, p) => Math.max(c, p), 0); + + return ( + <PurchaseDetailsTable> + <tr> + <td>Invoice</td> + <td> + <Amount value={amount.raw} maxFracSize={maxFrac} /> + </td> + </tr> + + {Amounts.isNonZero(fee) && ( + <tr> + <td>Transaction fees</td> + <td> + <Amount value={fee} negative maxFracSize={maxFrac} /> + </td> + </tr> + )} + <tr> + <td colSpan={2}> + <hr /> + </td> + </tr> + <tr> + <td>Total</td> + <td> + <Amount value={amount.effective} maxFracSize={maxFrac} /> + </td> + </tr> + </PurchaseDetailsTable> + ); +} + +export function TransferDetails({ amount }: { amount: AmountWithFee }): VNode { + const { i18n } = useTranslationContext(); + + const fee = Amounts.sub(amount.raw, amount.effective).amount; + + const maxFrac = [amount.raw, amount.effective, fee] + .map((a) => Amounts.maxFractionalDigits(a)) + .reduce((c, p) => Math.max(c, p), 0); + + return ( + <PurchaseDetailsTable> + <tr> + <td>Transfer</td> + <td> + <Amount value={amount.raw} maxFracSize={maxFrac} /> + </td> + </tr> + + {Amounts.isNonZero(fee) && ( + <tr> + <td>Transaction fees</td> + <td> + <Amount value={fee} negative maxFracSize={maxFrac} /> + </td> + </tr> + )} + <tr> + <td colSpan={2}> + <hr /> + </td> + </tr> + <tr> + <td>Total</td> + <td> + <Amount value={amount.effective} maxFracSize={maxFrac} /> + </td> + </tr> + </PurchaseDetailsTable> + ); +} + export function WithdrawDetails({ amount }: { amount: AmountWithFee }): VNode { const { i18n } = useTranslationContext(); diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 9700c475e..a796755e5 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -24,12 +24,18 @@ import { AcceptExchangeTosRequest, AcceptManualWithdrawalResult, + AcceptPeerPullPaymentRequest, + AcceptPeerPushPaymentRequest, AcceptTipRequest, AcceptWithdrawalResponse, AddExchangeRequest, AmountString, ApplyRefundResponse, BalancesResponse, + CheckPeerPullPaymentRequest, + CheckPeerPullPaymentResponse, + CheckPeerPushPaymentRequest, + CheckPeerPushPaymentResponse, CoinDumpJson, ConfirmPayResult, CoreApiResponse, @@ -41,6 +47,10 @@ import { GetExchangeWithdrawalInfo, GetFeeForDepositRequest, GetWithdrawalDetailsForUriRequest, + InitiatePeerPullPaymentRequest, + InitiatePeerPullPaymentResponse, + InitiatePeerPushPaymentRequest, + InitiatePeerPushPaymentResponse, KnownBankAccounts, Logger, NotificationType, @@ -473,3 +483,24 @@ export function onUpdateNotification( }; return platform.listenToWalletBackground(onNewMessage); } + +export function initiatePeerPushPayment(req: InitiatePeerPushPaymentRequest): Promise<InitiatePeerPushPaymentResponse> { + return callBackend("initiatePeerPushPayment", req); +} +export function checkPeerPushPayment(req: CheckPeerPushPaymentRequest): Promise<CheckPeerPushPaymentResponse> { + return callBackend("checkPeerPushPayment", req); +} +export function acceptPeerPushPayment(req: AcceptPeerPushPaymentRequest): Promise<void> { + return callBackend("acceptPeerPushPayment", req); +} +export function initiatePeerPullPayment(req: InitiatePeerPullPaymentRequest): Promise<InitiatePeerPullPaymentResponse> { + return callBackend("initiatePeerPullPayment", req); +} +export function checkPeerPullPayment(req: CheckPeerPullPaymentRequest): Promise<CheckPeerPullPaymentResponse> { + return callBackend("checkPeerPullPayment", req); +} +export function acceptPeerPullPayment(req: AcceptPeerPullPaymentRequest): Promise<void> { + return callBackend("acceptPeerPullPayment", req); +} + + diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts index c6d9aa70e..ae0103555 100644 --- a/packages/taler-wallet-webextension/src/wxBackend.ts +++ b/packages/taler-wallet-webextension/src/wxBackend.ts @@ -276,15 +276,35 @@ function parseTalerUriAndRedirect(tabId: number, talerUri: string): void { tabId, `/cta/refund?talerRefundUri=${talerUri}`, ); + case TalerUriType.TalerPayPull: + return platform.redirectTabToWalletPage( + tabId, + `/cta/invoice/pay?talerPayPullUri=${talerUri}`, + ); + case TalerUriType.TalerPayPush: + return platform.redirectTabToWalletPage( + tabId, + `/cta/transfer/pickup?talerPayPushUri=${talerUri}`, + ); case TalerUriType.TalerNotifyReserve: // FIXME: Is this still useful? // handleNotifyReserve(w); - break; - default: logger.warn( - "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.", + `Response with HTTP 402 the Taler header but it is deprecated ${talerUri}`, ); break; + case TalerUriType.Unknown: + logger.warn( + `Response with HTTP 402 the Taler header but could not classify ${talerUri}`, + ); + return; + default: { + const error: never = uriType; + logger.warn( + `Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`, + ); + return; + } } } |