gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: p2p tx rendering


From: gnunet
Subject: [taler-wallet-core] branch master updated: p2p tx rendering
Date: Wed, 31 Aug 2022 05:20:44 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new d8442420 p2p tx rendering
d8442420 is described below

commit d84424202dca22fff22cb1d304286f627642187b
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Wed Aug 31 00:20:35 2022 -0300

    p2p tx rendering
---
 .../src/operations/peer-to-peer.ts                 |   2 +-
 .../src/NavigationBar.tsx                          |  12 +-
 .../src/components/TransactionItem.tsx             |  54 ++++-
 .../{wallet/Invoice => cta/InvoiceCreate}/index.ts |  25 ++-
 .../src/cta/InvoiceCreate/state.ts                 | 113 ++++++++++
 .../Invoice => cta/InvoiceCreate}/stories.tsx      |  34 ++-
 .../{wallet/Invoice => cta/InvoiceCreate}/test.ts  |   0
 .../src/cta/InvoiceCreate/views.tsx                | 150 +++++++++++++
 .../{wallet/Invoice => cta/InvoicePay}/index.ts    |  15 +-
 .../src/{wallet/Send => cta/InvoicePay}/state.ts   |  52 +++--
 .../{wallet/Invoice => cta/InvoicePay}/stories.tsx |  11 +-
 .../src/{wallet/Send => cta/InvoicePay}/test.ts    |   0
 .../src/cta/InvoicePay/views.tsx                   |  85 +++++++
 .../{wallet/Send => cta/TransferCreate}/index.ts   |  25 ++-
 .../src/cta/TransferCreate/state.ts                |  86 +++++++
 .../Invoice => cta/TransferCreate}/stories.tsx     |  33 ++-
 .../{wallet/Invoice => cta/TransferCreate}/test.ts |   0
 .../src/cta/TransferCreate/views.tsx               | 117 ++++++++++
 .../{wallet/Send => cta/TransferPickup}/index.ts   |  16 +-
 .../{wallet/Send => cta/TransferPickup}/state.ts   |  50 +++--
 .../Send => cta/TransferPickup}/stories.tsx        |  11 +-
 .../{wallet/Invoice => cta/TransferPickup}/test.ts |   0
 .../Invoice => cta/TransferPickup}/views.tsx       |  56 +++--
 .../src/cta/index.stories.ts                       |   6 +-
 .../src/platform/chrome.ts                         |  26 ++-
 packages/taler-wallet-webextension/src/stories.tsx |   7 +-
 .../src/wallet/Application.tsx                     |  26 ++-
 .../src/wallet/History.stories.tsx                 |  47 ++++
 .../src/wallet/Invoice/state.ts                    |  37 ---
 .../src/wallet/Send/views.tsx                      |  58 -----
 .../src/wallet/Transaction.stories.tsx             |  44 ++++
 .../src/wallet/Transaction.tsx                     | 249 +++++++++++++++++++++
 packages/taler-wallet-webextension/src/wxApi.ts    |  31 +++
 .../taler-wallet-webextension/src/wxBackend.ts     |  26 ++-
 34 files changed, 1287 insertions(+), 217 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 19306845..88bfecd3 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 1c4873b0..9b222cdb 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 8033b3ee..6dcee6ec 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/wallet/Invoice/index.ts 
b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
similarity index 68%
copy from packages/taler-wallet-webextension/src/wallet/Invoice/index.ts
copy to packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
index 20d902e6..f12fd80e 100644
--- a/packages/taler-wallet-webextension/src/wallet/Invoice/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
@@ -17,19 +17,20 @@
 import { Loading } from "../../components/Loading.js";
 import { HookError } from "../../hooks/useAsyncAsHook.js";
 import { compose, StateViewMap } from "../../utils/index.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { LoadingUriView, ReadyView, ShowQrView } 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 { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
+import { ButtonHandler, SelectFieldHandler, TextFieldHandler } from 
"../../mui/handlers.js";
 
 export interface Props {
-  p: string;
+  amount: string;
 }
 
 export type State =
   | State.Loading
   | State.LoadingUriError
+  | State.ShowQr
   | State.Ready;
 
 export namespace State {
@@ -47,20 +48,32 @@ export namespace State {
   export interface BaseInfo {
     error: undefined;
   }
+  export interface ShowQr extends BaseInfo {
+    status: "show-qr";
+    talerUri: string;
+    close: () => void;
+  }
   export interface Ready extends BaseInfo {
     status: "ready";
-    amount: AmountJson;
+    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 InvoicePage = compose("InvoicePage", (p: Props) => 
useComponentState(p, wxApi), viewMapping)
+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 00000000..dd922048
--- /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/wallet/Invoice/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
similarity index 55%
copy from packages/taler-wallet-webextension/src/wallet/Invoice/stories.tsx
copy to packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
index 75f78be1..e6252062 100644
--- a/packages/taler-wallet-webextension/src/wallet/Invoice/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
@@ -20,10 +20,38 @@
  */
 
 import { createExample } from "../../test-utils.js";
-import { ReadyView } from "./views.js";
+import { ReadyView, ShowQrView } from "./views.js";
 
 export default {
-  title: "wallet/invoice",
+  title: "wallet/invoice create",
 };
 
-export const Ready = createExample(ReadyView, {});
+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
similarity index 100%
copy from packages/taler-wallet-webextension/src/wallet/Invoice/test.ts
copy to 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 00000000..ebb15e75
--- /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
similarity index 81%
rename from packages/taler-wallet-webextension/src/wallet/Invoice/index.ts
rename to packages/taler-wallet-webextension/src/cta/InvoicePay/index.ts
index 20d902e6..8d1612c7 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
similarity index 58%
copy from packages/taler-wallet-webextension/src/wallet/Send/state.ts
copy to packages/taler-wallet-webextension/src/cta/InvoicePay/state.ts
index 1359c180..53db117f 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/Invoice/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
similarity index 83%
copy from packages/taler-wallet-webextension/src/wallet/Invoice/stories.tsx
copy to packages/taler-wallet-webextension/src/cta/InvoicePay/stories.tsx
index 75f78be1..6adaa5c2 100644
--- a/packages/taler-wallet-webextension/src/wallet/Invoice/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
similarity index 100%
rename from packages/taler-wallet-webextension/src/wallet/Send/test.ts
rename to 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 00000000..2960c316
--- /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/wallet/Send/index.ts 
b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
similarity index 68%
copy from packages/taler-wallet-webextension/src/wallet/Send/index.ts
copy to packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
index fb69c628..fd034341 100644
--- a/packages/taler-wallet-webextension/src/wallet/Send/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts
@@ -17,19 +17,20 @@
 import { Loading } from "../../components/Loading.js";
 import { HookError } from "../../hooks/useAsyncAsHook.js";
 import { compose, StateViewMap } from "../../utils/index.js";
-import { LoadingUriView, ReadyView } from "./views.js";
+import { LoadingUriView, ReadyView, ShowQrView } 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 { AmountJson, TalerErrorDetail } from "@gnu-taler/taler-util";
+import { ButtonHandler, SelectFieldHandler, TextFieldHandler } from 
"../../mui/handlers.js";
 
 export interface Props {
-  p: string;
+  amount: string;
 }
 
 export type State =
   | State.Loading
   | State.LoadingUriError
+  | State.ShowQr
   | State.Ready;
 
 export namespace State {
@@ -47,21 +48,31 @@ export namespace State {
   export interface BaseInfo {
     error: undefined;
   }
+  export interface ShowQr extends BaseInfo {
+    status: "show-qr";
+    talerUri: string;
+    close: () => void;
+  }
   export interface Ready extends BaseInfo {
     status: "ready";
-    amount: AmountJson;
-    exchange: SelectFieldHandler,
+    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 SendPage = compose("SendPage", (p: Props) => useComponentState(p, 
wxApi), viewMapping)
+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 00000000..9853f05f
--- /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/wallet/Invoice/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
similarity index 57%
rename from packages/taler-wallet-webextension/src/wallet/Invoice/stories.tsx
rename to packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
index 75f78be1..1e528df7 100644
--- a/packages/taler-wallet-webextension/src/wallet/Invoice/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx
@@ -20,10 +20,37 @@
  */
 
 import { createExample } from "../../test-utils.js";
-import { ReadyView } from "./views.js";
+import { ReadyView, ShowQrView } from "./views.js";
 
 export default {
-  title: "wallet/invoice",
+  title: "wallet/transfer create",
 };
 
-export const Ready = createExample(ReadyView, {});
+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/wallet/Invoice/test.ts 
b/packages/taler-wallet-webextension/src/cta/TransferCreate/test.ts
similarity index 100%
copy from packages/taler-wallet-webextension/src/wallet/Invoice/test.ts
copy to packages/taler-wallet-webextension/src/cta/TransferCreate/test.ts
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 00000000..ebdf1e74
--- /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
similarity index 81%
rename from packages/taler-wallet-webextension/src/wallet/Send/index.ts
rename to packages/taler-wallet-webextension/src/cta/TransferPickup/index.ts
index fb69c628..60320074 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/wallet/Send/state.ts 
b/packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
similarity index 58%
rename from packages/taler-wallet-webextension/src/wallet/Send/state.ts
rename to packages/taler-wallet-webextension/src/cta/TransferPickup/state.ts
index 1359c180..a1638970 100644
--- a/packages/taler-wallet-webextension/src/wallet/Send/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/TransferPickup/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,
+  { talerPayPushUri }: 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.checkPeerPushPayment({
+      talerUri: talerPayPushUri,
+    })
+  }, [])
+  const [operationError, setOperationError] = useState<TalerErrorDetail | 
undefined>(undefined)
 
   if (!hook) {
     return {
@@ -43,24 +45,28 @@ 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, 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",
-    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/TransferPickup/stories.tsx
similarity index 83%
rename from packages/taler-wallet-webextension/src/wallet/Send/stories.tsx
rename to packages/taler-wallet-webextension/src/cta/TransferPickup/stories.tsx
index 75f78be1..7a3ccb60 100644
--- a/packages/taler-wallet-webextension/src/wallet/Send/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/wallet/Invoice/test.ts 
b/packages/taler-wallet-webextension/src/cta/TransferPickup/test.ts
similarity index 100%
rename from packages/taler-wallet-webextension/src/wallet/Invoice/test.ts
rename to packages/taler-wallet-webextension/src/cta/TransferPickup/test.ts
diff --git a/packages/taler-wallet-webextension/src/wallet/Invoice/views.tsx 
b/packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
similarity index 51%
rename from packages/taler-wallet-webextension/src/wallet/Invoice/views.tsx
rename to packages/taler-wallet-webextension/src/cta/TransferPickup/views.tsx
index 94e8f862..22ef8c77 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 92f4bbcb..2f0ef33f 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 21fc180c..f2c7e9d2 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 a032222a..09261d6f 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 caed45d3..253d5fba 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 9577bb9d..335d5ea9 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 48cfd359..00000000
--- 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 63310f44..00000000
--- 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 ba61e35f..6c591611 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 ff3b70b6..c8c4e3ae 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 9700c475..a796755e 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 c6d9aa70..ae010355 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;
+    }
   }
 }
 

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]