gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: fix #7522


From: gnunet
Subject: [taler-wallet-core] branch master updated: fix #7522
Date: Wed, 04 Jan 2023 15:42:42 +0100

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 24cac493d fix #7522
24cac493d is described below

commit 24cac493dded00ef40e0e30a0d2263e4f35c3e29
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Wed Jan 4 11:24:58 2023 -0300

    fix #7522
---
 .../src/NavigationBar.tsx                          |  31 +-
 .../src/components/Banner.stories.tsx              |  76 ++-
 .../src/components/Banner.tsx                      |  40 +-
 .../src/components/PaymentButtons.tsx              | 153 +++++
 .../src/components/PendingTransactions.tsx         | 102 ++-
 .../src/components/ProductList.tsx                 |  89 +++
 .../src/components/styled/index.tsx                |   2 +-
 .../src/context/backend.ts                         |   2 +-
 .../src/cta/InvoicePay/views.tsx                   |   4 +-
 .../src/cta/Payment/views.tsx                      | 251 +------
 .../src/cta/Refund/views.tsx                       |   2 +-
 .../src/cta/Withdraw/views.tsx                     |  16 +-
 .../src/popup/Application.tsx                      | 246 ++++---
 .../src/serviceWorkerHttpLib.ts                    |   4 +-
 packages/taler-wallet-webextension/src/stories.tsx |   8 +-
 .../taler-wallet-webextension/src/utils/index.ts   |   2 +-
 .../src/wallet/AddBackupProvider/index.ts          |   1 -
 .../src/wallet/AddBackupProvider/state.ts          |   1 -
 .../src/wallet/AddBackupProvider/test.ts           |   1 -
 .../src/wallet/Application.tsx                     | 741 ++++++++++++---------
 .../src/wallet/DeveloperPage.tsx                   |  33 +-
 21 files changed, 997 insertions(+), 808 deletions(-)

diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx 
b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index ab36af376..1c26450f7 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -45,6 +45,7 @@ import warningIcon from "./svg/warning_24px.svg";
  * @author sebasjm
  */
 
+// eslint-disable-next-line @typescript-eslint/ban-types
 type PageLocation<DynamicPart extends object> = {
   pattern: string;
   (params: DynamicPart): string;
@@ -62,6 +63,7 @@ function replaceAll(
   return result;
 }
 
+// eslint-disable-next-line @typescript-eslint/ban-types
 function pageDefinition<T extends object>(pattern: string): PageLocation<T> {
   const patternParams = pattern.match(/(:[\w?]*)/g);
   if (!patternParams)
@@ -133,7 +135,8 @@ export const Pages = {
   ),
 };
 
-export function PopupNavBar({ path = "" }: { path?: string }): VNode {
+export type PopupNavBarOptions = "balance" | "backup" | "dev";
+export function PopupNavBar({ path }: { path?: PopupNavBarOptions }): VNode {
   const api = useBackendContext();
   const hook = useAsyncAsHook(async () => {
     return await api.wallet.call(
@@ -146,13 +149,10 @@ export function PopupNavBar({ path = "" }: { path?: 
string }): VNode {
   const { i18n } = useTranslationContext();
   return (
     <NavigationHeader>
-      <a
-        href={Pages.balance}
-        class={path.startsWith("/balance") ? "active" : ""}
-      >
+      <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
         <i18n.Translate>Balance</i18n.Translate>
       </a>
-      <a href={Pages.backup} class={path.startsWith("/backup") ? "active" : 
""}>
+      <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
         <i18n.Translate>Backup</i18n.Translate>
       </a>
       <div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
@@ -185,8 +185,8 @@ export function PopupNavBar({ path = "" }: { path?: string 
}): VNode {
     </NavigationHeader>
   );
 }
-
-export function WalletNavBar({ path = "" }: { path?: string }): VNode {
+export type WalletNavBarOptions = "balance" | "backup" | "dev";
+export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
   const { i18n } = useTranslationContext();
 
   const api = useBackendContext();
@@ -196,21 +196,16 @@ export function WalletNavBar({ path = "" }: { path?: 
string }): VNode {
       {},
     );
   });
-  const attentionCount = !hook || hook.hasError ? 0 : hook.response.total;
+  const attentionCount =
+    (!hook || hook.hasError ? 0 : hook.response?.total) ?? 0;
 
   return (
     <NavigationHeaderHolder>
       <NavigationHeader>
-        <a
-          href={Pages.balance}
-          class={path.startsWith("/balance") ? "active" : ""}
-        >
+        <a href={Pages.balance} class={path === "balance" ? "active" : ""}>
           <i18n.Translate>Balance</i18n.Translate>
         </a>
-        <a
-          href={Pages.backup}
-          class={path.startsWith("/backup") ? "active" : ""}
-        >
+        <a href={Pages.backup} class={path === "backup" ? "active" : ""}>
           <i18n.Translate>Backup</i18n.Translate>
         </a>
 
@@ -223,7 +218,7 @@ export function WalletNavBar({ path = "" }: { path?: string 
}): VNode {
         )}
 
         <JustInDevMode>
-          <a href={Pages.dev} class={path.startsWith("/dev") ? "active" : ""}>
+          <a href={Pages.dev} class={path === "dev" ? "active" : ""}>
             <i18n.Translate>Dev</i18n.Translate>
           </a>
         </JustInDevMode>
diff --git 
a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx 
b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
index 39012480b..60b100478 100644
--- a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx
@@ -65,23 +65,25 @@ export const BasicExample = (): VNode => (
         </a>
       </p>
       <Banner
-        elements={[
-          {
-            icon: <SignalWifiOffIcon color="gray" />,
-            description: (
-              <Typography>
-                You have lost connection to the internet. This app is offline.
-              </Typography>
-            ),
-          },
-        ]}
+        // elements={[
+        //   {
+        //     icon: <SignalWifiOffIcon color="gray" />,
+        //     description: (
+        //       <Typography>
+        //         You have lost connection to the internet. This app is 
offline.
+        //       </Typography>
+        //     ),
+        //   },
+        // ]}
         confirm={{
           label: "turn on wifi",
           action: async () => {
             return;
           },
         }}
-      />
+      >
+        <div />
+      </Banner>
     </Wrapper>
   </Fragment>
 );
@@ -92,31 +94,33 @@ export const PendingOperation = (): VNode => (
       <Banner
         title="PENDING TRANSACTIONS"
         style={{ backgroundColor: "lightcyan", padding: 8 }}
-        elements={[
-          {
-            icon: (
-              <Avatar
-                style={{
-                  border: "solid blue 1px",
-                  color: "blue",
-                  boxSizing: "border-box",
-                }}
-              >
-                P
-              </Avatar>
-            ),
-            description: (
-              <Fragment>
-                <Typography inline bold>
-                  EUR 37.95
-                </Typography>
-                &nbsp;
-                <Typography inline>- 5 feb 2022</Typography>
-              </Fragment>
-            ),
-          },
-        ]}
-      />
+        // elements={[
+        //   {
+        //     icon: (
+        //       <Avatar
+        //         style={{
+        //           border: "solid blue 1px",
+        //           color: "blue",
+        //           boxSizing: "border-box",
+        //         }}
+        //       >
+        //         P
+        //       </Avatar>
+        //     ),
+        //     description: (
+        //       <Fragment>
+        //         <Typography inline bold>
+        //           EUR 37.95
+        //         </Typography>
+        //         &nbsp;
+        //         <Typography inline>- 5 feb 2022</Typography>
+        //       </Fragment>
+        //     ),
+        //   },
+        // ]}
+      >
+        asd
+      </Banner>
     </Wrapper>
   </Fragment>
 );
diff --git a/packages/taler-wallet-webextension/src/components/Banner.tsx 
b/packages/taler-wallet-webextension/src/components/Banner.tsx
index f95647d42..a91fd384f 100644
--- a/packages/taler-wallet-webextension/src/components/Banner.tsx
+++ b/packages/taler-wallet-webextension/src/components/Banner.tsx
@@ -13,21 +13,20 @@
  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, Fragment, VNode, JSX } from "preact";
-import { Divider } from "../mui/Divider.js";
+import { ComponentChildren, Fragment, h, JSX, VNode } from "preact";
 import { Button } from "../mui/Button.js";
-import { Typography } from "../mui/Typography.js";
-import { Avatar } from "../mui/Avatar.js";
+import { Divider } from "../mui/Divider.js";
 import { Grid } from "../mui/Grid.js";
 import { Paper } from "../mui/Paper.js";
 
 interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
   titleHead?: VNode;
-  elements: {
-    icon?: VNode;
-    description: VNode;
-    action?: () => void;
-  }[];
+  children: ComponentChildren;
+  // elements: {
+  //   icon?: VNode;
+  //   description: VNode;
+  //   action?: () => void;
+  // }[];
   confirm?: {
     label: string;
     action: () => Promise<void>;
@@ -36,8 +35,9 @@ interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
 
 export function Banner({
   titleHead,
-  elements,
+  children,
   confirm,
+  href,
   ...rest
 }: Props): VNode {
   return (
@@ -49,25 +49,7 @@ export function Banner({
           </Grid>
         )}
         <Grid container columns={1}>
-          {elements.map((e, i) => (
-            <Grid
-              container
-              item
-              xs={1}
-              key={i}
-              wrap="nowrap"
-              spacing={1}
-              alignItems="center"
-              onClick={e.action}
-            >
-              {e.icon && (
-                <Grid item xs={"auto"}>
-                  <Avatar>{e.icon}</Avatar>
-                </Grid>
-              )}
-              <Grid item>{e.description}</Grid>
-            </Grid>
-          ))}
+          {children}
         </Grid>
         {confirm && (
           <Grid container justifyContent="flex-end" spacing={8}>
diff --git 
a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx 
b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
new file mode 100644
index 000000000..def1e16eb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx
@@ -0,0 +1,153 @@
+/*
+ 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 {
+  AmountJson,
+  Amounts,
+  PreparePayResult,
+  PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Amount } from "./Amount.js";
+import { Part } from "./Part.js";
+import { QR } from "./QR.js";
+import { LinkSuccess, WarningBox } from "./styled/index.js";
+import { useTranslationContext } from "../context/translation.js";
+import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { assertUnreachable } from "../utils/index.js";
+
+interface Props {
+  payStatus: PreparePayResult;
+  payHandler: ButtonHandler | undefined;
+  balance: AmountJson | undefined;
+  uri: string;
+  amount: AmountJson;
+  goToWalletManualWithdraw: (currency: string) => Promise<void>;
+}
+
+export function PaymentButtons({
+  payStatus,
+  uri,
+  payHandler,
+  balance,
+  amount,
+  goToWalletManualWithdraw,
+}: Props): VNode {
+  const { i18n } = useTranslationContext();
+  if (payStatus.status === PreparePayResultType.PaymentPossible) {
+    const privateUri = `${uri}&n=${payStatus.noncePriv}`;
+
+    return (
+      <Fragment>
+        <section>
+          <Button
+            variant="contained"
+            color="success"
+            onClick={payHandler?.onClick}
+          >
+            <i18n.Translate>
+              Pay &nbsp;
+              {<Amount value={amount} />}
+            </i18n.Translate>
+          </Button>
+        </section>
+        <PayWithMobile uri={privateUri} />
+      </Fragment>
+    );
+  }
+
+  if (payStatus.status === PreparePayResultType.InsufficientBalance) {
+    let BalanceMessage = "";
+    if (!balance) {
+      BalanceMessage = i18n.str`You have no balance for this currency. 
Withdraw digital cash first.`;
+    } else {
+      const balanceShouldBeEnough = Amounts.cmp(balance, amount) !== -1;
+      if (balanceShouldBeEnough) {
+        BalanceMessage = i18n.str`Could not find enough coins to pay. Even if 
you have enough ${balance.currency} some restriction may apply.`;
+      } else {
+        BalanceMessage = i18n.str`Your current balance is not enough.`;
+      }
+    }
+    const uriPrivate = `${uri}&n=${payStatus.noncePriv}`;
+
+    return (
+      <Fragment>
+        <section>
+          <WarningBox>{BalanceMessage}</WarningBox>
+        </section>
+        <section>
+          <Button
+            variant="contained"
+            color="success"
+            onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))}
+          >
+            <i18n.Translate>Get digital cash</i18n.Translate>
+          </Button>
+        </section>
+        <PayWithMobile uri={uriPrivate} />
+      </Fragment>
+    );
+  }
+  if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+    return (
+      <Fragment>
+        <section>
+          {payStatus.paid && payStatus.contractTerms.fulfillment_message && (
+            <Part
+              title={<i18n.Translate>Merchant message</i18n.Translate>}
+              text={payStatus.contractTerms.fulfillment_message}
+              kind="neutral"
+            />
+          )}
+        </section>
+        {!payStatus.paid && <PayWithMobile uri={uri} />}
+      </Fragment>
+    );
+  }
+
+  assertUnreachable(payStatus);
+}
+
+function PayWithMobile({ uri }: { uri: string }): VNode {
+  const { i18n } = useTranslationContext();
+
+  const [showQR, setShowQR] = useState<boolean>(false);
+
+  return (
+    <section>
+      <LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}>
+        {!showQR ? (
+          <i18n.Translate>Pay with a mobile phone</i18n.Translate>
+        ) : (
+          <i18n.Translate>Hide QR</i18n.Translate>
+        )}
+      </LinkSuccess>
+      {showQR && (
+        <div>
+          <QR text={uri} />
+          <i18n.Translate>
+            Scan the QR code or &nbsp;
+            <a href={uri}>
+              <i18n.Translate>click here</i18n.Translate>
+            </a>
+          </i18n.Translate>
+        </div>
+      )}
+    </section>
+  );
+}
diff --git 
a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx 
b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
index 85b43fb4e..e41ff2836 100644
--- a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
+++ b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx
@@ -26,6 +26,7 @@ import { useBackendContext } from "../context/backend.js";
 import { useTranslationContext } from "../context/translation.js";
 import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
 import { Avatar } from "../mui/Avatar.js";
+import { Grid } from "../mui/Grid.js";
 import { Typography } from "../mui/Typography.js";
 import Banner from "./Banner.js";
 import { Time } from "./Time.js";
@@ -34,6 +35,11 @@ interface Props extends JSX.HTMLAttributes {
   goToTransaction: (id: string) => Promise<void>;
 }
 
+/**
+ * this cache will save the tx from the previous render
+ */
+const cache = { tx: [] as Transaction[] };
+
 export function PendingTransactions({ goToTransaction }: Props): VNode {
   const api = useBackendContext();
   const state = useAsyncAsHook(() =>
@@ -49,12 +55,13 @@ export function PendingTransactions({ goToTransaction }: 
Props): VNode {
 
   const transactions =
     !state || state.hasError
-      ? []
+      ? cache.tx
       : state.response.transactions.filter((t) => t.pending);
 
-  if (!state || state.hasError || !transactions.length) {
+  if (!transactions.length) {
     return <Fragment />;
   }
+  cache.tx = transactions;
   return (
     <PendingTransactionsView
       goToTransaction={goToTransaction}
@@ -72,46 +79,67 @@ export function PendingTransactionsView({
 }): VNode {
   const { i18n } = useTranslationContext();
   return (
-    <Banner
-      titleHead={<i18n.Translate>PENDING OPERATIONS</i18n.Translate>}
+    <div
       style={{
         backgroundColor: "lightcyan",
-        maxHeight: 150,
-        padding: 8,
-        flexGrow: 1,
-        maxWidth: 500,
-        overflowY: transactions.length > 3 ? "scroll" : "hidden",
+        display: "flex",
+        justifyContent: "center",
       }}
-      elements={transactions.map((t) => {
-        const amount = Amounts.parseOrThrow(t.amountEffective);
-        return {
-          icon: (
-            <Avatar
-              style={{
-                border: "solid blue 1px",
-                color: "blue",
-                boxSizing: "border-box",
+    >
+      <Banner
+        titleHead={<i18n.Translate>PENDING OPERATIONS</i18n.Translate>}
+        style={{
+          backgroundColor: "lightcyan",
+          maxHeight: 150,
+          padding: 8,
+          flexGrow: 1,
+          maxWidth: 500,
+          overflowY: transactions.length > 3 ? "scroll" : "hidden",
+        }}
+      >
+        {transactions.map((t, i) => {
+          const amount = Amounts.parseOrThrow(t.amountEffective);
+          return (
+            <Grid
+              container
+              item
+              xs={1}
+              key={i}
+              wrap="nowrap"
+              role="button"
+              spacing={1}
+              alignItems="center"
+              onClick={() => {
+                goToTransaction(t.transactionId);
               }}
             >
-              {t.type.substring(0, 1)}
-            </Avatar>
-          ),
-          action: () => goToTransaction(t.transactionId),
-          description: (
-            <Fragment>
-              <Typography inline bold>
-                {amount.currency} {Amounts.stringifyValue(amount)}
-              </Typography>
-              &nbsp;-&nbsp;
-              <Time
-                timestamp={AbsoluteTime.fromTimestamp(t.timestamp)}
-                format="dd MMMM yyyy"
-              />
-            </Fragment>
-          ),
-        };
-      })}
-    />
+              <Grid item xs={"auto"}>
+                <Avatar
+                  style={{
+                    border: "solid blue 1px",
+                    color: "blue",
+                    boxSizing: "border-box",
+                  }}
+                >
+                  {t.type.substring(0, 1)}
+                </Avatar>
+              </Grid>
+
+              <Grid item>
+                <Typography inline bold>
+                  {amount.currency} {Amounts.stringifyValue(amount)}
+                </Typography>
+                &nbsp;-&nbsp;
+                <Time
+                  timestamp={AbsoluteTime.fromTimestamp(t.timestamp)}
+                  format="dd MMMM yyyy"
+                />
+              </Grid>
+            </Grid>
+          );
+        })}
+      </Banner>
+    </div>
   );
 }
 
diff --git a/packages/taler-wallet-webextension/src/components/ProductList.tsx 
b/packages/taler-wallet-webextension/src/components/ProductList.tsx
new file mode 100644
index 000000000..a78733179
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/ProductList.tsx
@@ -0,0 +1,89 @@
+/*
+ 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, Product } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { SmallLightText } from "./styled/index.js";
+import { useTranslationContext } from "../context/translation.js";
+
+export function ProductList({ products }: { products: Product[] }): VNode {
+  const { i18n } = useTranslationContext();
+  return (
+    <Fragment>
+      <SmallLightText style={{ margin: ".5em" }}>
+        <i18n.Translate>List of products</i18n.Translate>
+      </SmallLightText>
+      <dl>
+        {products.map((p, i) => {
+          if (p.price) {
+            const pPrice = Amounts.parseOrThrow(p.price);
+            return (
+              <div key={i} style={{ display: "flex", textAlign: "left" }}>
+                <div>
+                  <img
+                    src={p.image ? p.image : undefined}
+                    style={{ width: 32, height: 32 }}
+                  />
+                </div>
+                <div>
+                  <dt>
+                    {p.quantity ?? 1} x {p.description}{" "}
+                    <span style={{ color: "gray" }}>
+                      {Amounts.stringify(pPrice)}
+                    </span>
+                  </dt>
+                  <dd>
+                    <b>
+                      {Amounts.stringify(
+                        Amounts.mult(pPrice, p.quantity ?? 1).amount,
+                      )}
+                    </b>
+                  </dd>
+                </div>
+              </div>
+            );
+          }
+          return (
+            <div key={i} style={{ display: "flex", textAlign: "left" }}>
+              <div>
+                <img src={p.image} style={{ width: 32, height: 32 }} />
+              </div>
+              <div>
+                <dt>
+                  {p.quantity ?? 1} x {p.description}
+                </dt>
+                <dd>
+                  <i18n.Translate>Total</i18n.Translate>
+                  {` `}
+                  {p.price ? (
+                    `${Amounts.stringifyValue(
+                      Amounts.mult(
+                        Amounts.parseOrThrow(p.price),
+                        p.quantity ?? 1,
+                      ).amount,
+                    )} ${p}`
+                  ) : (
+                    <i18n.Translate>free</i18n.Translate>
+                  )}
+                </dd>
+              </div>
+            </div>
+          );
+        })}
+      </dl>
+    </Fragment>
+  );
+}
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx 
b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 7a3c27c73..8e98f75eb 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -159,7 +159,7 @@ export const Middle = styled.div`
   height: 100%;
 `;
 
-export const PopupBox = styled.div<{ noPadding?: boolean; devMode?: boolean }>`
+export const PopupBox = styled.div<{ noPadding?: boolean }>`
   height: 290px;
   width: 500px;
   overflow-y: visible;
diff --git a/packages/taler-wallet-webextension/src/context/backend.ts 
b/packages/taler-wallet-webextension/src/context/backend.ts
index e00a70080..280fb266d 100644
--- a/packages/taler-wallet-webextension/src/context/backend.ts
+++ b/packages/taler-wallet-webextension/src/context/backend.ts
@@ -29,7 +29,7 @@ const initial = wxApi;
 
 const Context = createContext<Type>(initial);
 
-type Props = Partial<WxApiType> & {
+type Props = Partial<Type> & {
   children: ComponentChildren;
 };
 
diff --git a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx 
b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
index 8484680bf..a53fa881a 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoicePay/views.tsx
@@ -23,7 +23,7 @@ import { Part } from "../../components/Part.js";
 import { Link, SubTitle, WalletAction } from 
"../../components/styled/index.js";
 import { Time } from "../../components/Time.js";
 import { useTranslationContext } from "../../context/translation.js";
-import { ButtonsSection } from "../Payment/views.js";
+import { PaymentButtons } from "../../components/PaymentButtons";
 import { State } from "./index.js";
 
 export function LoadingUriView({ error }: State.LoadingUriError): VNode {
@@ -83,7 +83,7 @@ export function ReadyView(
           kind="neutral"
         />
       </section>
-      <ButtonsSection
+      <PaymentButtons
         amount={amount}
         balance={balance}
         payStatus={payStatus}
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx 
b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index 0f6cb5c28..efc8bcfc4 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -16,35 +16,17 @@
 
 import {
   AbsoluteTime,
-  AmountJson,
   Amounts,
   MerchantContractTerms as ContractTerms,
-  PreparePayResult,
   PreparePayResultType,
-  Product,
 } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { Amount } from "../../components/Amount.js";
-import { ErrorMessage } from "../../components/ErrorMessage.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,
-  LinkSuccess,
-  SmallLightText,
-  SubTitle,
-  SuccessBox,
-  WalletAction,
-  WarningBox,
-} from "../../components/styled/index.js";
+import { PaymentButtons } from "../../components/PaymentButtons.js";
+import { Link, SuccessBox, WarningBox } from 
"../../components/styled/index.js";
 import { Time } from "../../components/Time.js";
 import { useTranslationContext } from "../../context/translation.js";
-import { Button } from "../../mui/Button.js";
-import { ButtonHandler } from "../../mui/handlers.js";
-import { assertUnreachable } from "../../utils/index.js";
 import { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
 import { State } from "./index.js";
 
@@ -77,44 +59,12 @@ export function BaseView(state: SupportedStates): VNode {
         ? Amounts.parseOrThrow(state.payStatus.amountEffective)
         : state.amount,
   };
-  // const totalFees = Amounts.sub(price.effective, price.raw).amount;
 
   return (
-    <WalletAction>
-      <LogoHeader />
-
-      <SubTitle>
-        <i18n.Translate>Digital cash payment</i18n.Translate>
-      </SubTitle>
-
+    <Fragment>
       <ShowImportantMessage state={state} />
 
       <section style={{ textAlign: "left" }}>
-        {/* {state.payStatus.status !== 
PreparePayResultType.InsufficientBalance &&
-          Amounts.isNonZero(totalFees) && (
-            <Part
-              big
-              title={<i18n.Translate>Total to pay</i18n.Translate>}
-              text={<Amount value={state.payStatus.amountEffective} />}
-              kind="negative"
-            />
-          )}
-        <Part
-          big
-          title={<i18n.Translate>Purchase amount</i18n.Translate>}
-          text={<Amount value={state.payStatus.amountRaw} />}
-          kind="neutral"
-        />
-        {Amounts.isNonZero(totalFees) && (
-          <Fragment>
-            <Part
-              big
-              title={<i18n.Translate>Fee</i18n.Translate>}
-              text={<Amount value={totalFees} />}
-              kind="negative"
-            />
-          </Fragment>
-        )} */}
         <Part
           title={<i18n.Translate>Purchase</i18n.Translate>}
           text={contractTerms.summary}
@@ -125,9 +75,6 @@ export function BaseView(state: SupportedStates): VNode {
           text={<MerchantDetails merchant={contractTerms.merchant} />}
           kind="neutral"
         />
-        {/* <pre>{JSON.stringify(price)}</pre>
-        <hr />
-        <pre>{JSON.stringify(state.payStatus, undefined, 2)}</pre> */}
         <Part
           title={<i18n.Translate>Details</i18n.Translate>}
           text={
@@ -166,7 +113,7 @@ export function BaseView(state: SupportedStates): VNode {
           />
         )}
       </section>
-      <ButtonsSection
+      <PaymentButtons
         amount={state.amount}
         balance={state.balance}
         payStatus={state.payStatus}
@@ -179,75 +126,6 @@ export function BaseView(state: SupportedStates): VNode {
           <i18n.Translate>Cancel</i18n.Translate>
         </Link>
       </section>
-    </WalletAction>
-  );
-}
-
-export function ProductList({ products }: { products: Product[] }): VNode {
-  const { i18n } = useTranslationContext();
-  return (
-    <Fragment>
-      <SmallLightText style={{ margin: ".5em" }}>
-        <i18n.Translate>List of products</i18n.Translate>
-      </SmallLightText>
-      <dl>
-        {products.map((p, i) => {
-          if (p.price) {
-            const pPrice = Amounts.parseOrThrow(p.price);
-            return (
-              <div key={i} style={{ display: "flex", textAlign: "left" }}>
-                <div>
-                  <img
-                    src={p.image ? p.image : undefined}
-                    style={{ width: 32, height: 32 }}
-                  />
-                </div>
-                <div>
-                  <dt>
-                    {p.quantity ?? 1} x {p.description}{" "}
-                    <span style={{ color: "gray" }}>
-                      {Amounts.stringify(pPrice)}
-                    </span>
-                  </dt>
-                  <dd>
-                    <b>
-                      {Amounts.stringify(
-                        Amounts.mult(pPrice, p.quantity ?? 1).amount,
-                      )}
-                    </b>
-                  </dd>
-                </div>
-              </div>
-            );
-          }
-          return (
-            <div key={i} style={{ display: "flex", textAlign: "left" }}>
-              <div>
-                <img src={p.image} style={{ width: 32, height: 32 }} />
-              </div>
-              <div>
-                <dt>
-                  {p.quantity ?? 1} x {p.description}
-                </dt>
-                <dd>
-                  <i18n.Translate>Total</i18n.Translate>
-                  {` `}
-                  {p.price ? (
-                    `${Amounts.stringifyValue(
-                      Amounts.mult(
-                        Amounts.parseOrThrow(p.price),
-                        p.quantity ?? 1,
-                      ).amount,
-                    )} ${p}`
-                  ) : (
-                    <i18n.Translate>free</i18n.Translate>
-                  )}
-                </dd>
-              </div>
-            </div>
-          );
-        })}
-      </dl>
     </Fragment>
   );
 }
@@ -284,124 +162,3 @@ function ShowImportantMessage({ state }: { state: 
SupportedStates }): VNode {
 
   return <Fragment />;
 }
-
-export function PayWithMobile({ uri }: { uri: string }): VNode {
-  const { i18n } = useTranslationContext();
-
-  const [showQR, setShowQR] = useState<boolean>(false);
-
-  return (
-    <section>
-      <LinkSuccess upperCased onClick={() => setShowQR((qr) => !qr)}>
-        {!showQR ? (
-          <i18n.Translate>Pay with a mobile phone</i18n.Translate>
-        ) : (
-          <i18n.Translate>Hide QR</i18n.Translate>
-        )}
-      </LinkSuccess>
-      {showQR && (
-        <div>
-          <QR text={uri} />
-          <i18n.Translate>
-            Scan the QR code or &nbsp;
-            <a href={uri}>
-              <i18n.Translate>click here</i18n.Translate>
-            </a>
-          </i18n.Translate>
-        </div>
-      )}
-    </section>
-  );
-}
-
-interface ButtonSectionProps {
-  payStatus: PreparePayResult;
-  payHandler: ButtonHandler | undefined;
-  balance: AmountJson | undefined;
-  uri: string;
-  amount: AmountJson;
-  goToWalletManualWithdraw: (currency: string) => Promise<void>;
-}
-
-export function ButtonsSection({
-  payStatus,
-  uri,
-  payHandler,
-  balance,
-  amount,
-  goToWalletManualWithdraw,
-}: ButtonSectionProps): VNode {
-  const { i18n } = useTranslationContext();
-  if (payStatus.status === PreparePayResultType.PaymentPossible) {
-    const privateUri = `${uri}&n=${payStatus.noncePriv}`;
-
-    return (
-      <Fragment>
-        <section>
-          <Button
-            variant="contained"
-            color="success"
-            onClick={payHandler?.onClick}
-          >
-            <i18n.Translate>
-              Pay &nbsp;
-              {<Amount value={amount} />}
-            </i18n.Translate>
-          </Button>
-        </section>
-        <PayWithMobile uri={privateUri} />
-      </Fragment>
-    );
-  }
-
-  if (payStatus.status === PreparePayResultType.InsufficientBalance) {
-    let BalanceMessage = "";
-    if (!balance) {
-      BalanceMessage = i18n.str`You have no balance for this currency. 
Withdraw digital cash first.`;
-    } else {
-      const balanceShouldBeEnough = Amounts.cmp(balance, amount) !== -1;
-      if (balanceShouldBeEnough) {
-        BalanceMessage = i18n.str`Could not find enough coins to pay. Even if 
you have enough ${balance.currency} some restriction may apply.`;
-      } else {
-        BalanceMessage = i18n.str`Your current balance is not enough.`;
-      }
-    }
-    const uriPrivate = `${uri}&n=${payStatus.noncePriv}`;
-
-    return (
-      <Fragment>
-        <section>
-          <WarningBox>{BalanceMessage}</WarningBox>
-        </section>
-        <section>
-          <Button
-            variant="contained"
-            color="success"
-            onClick={() => goToWalletManualWithdraw(Amounts.stringify(amount))}
-          >
-            <i18n.Translate>Get digital cash</i18n.Translate>
-          </Button>
-        </section>
-        <PayWithMobile uri={uriPrivate} />
-      </Fragment>
-    );
-  }
-  if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
-    return (
-      <Fragment>
-        <section>
-          {payStatus.paid && payStatus.contractTerms.fulfillment_message && (
-            <Part
-              title={<i18n.Translate>Merchant message</i18n.Translate>}
-              text={payStatus.contractTerms.fulfillment_message}
-              kind="neutral"
-            />
-          )}
-        </section>
-        {!payStatus.paid && <PayWithMobile uri={uri} />}
-      </Fragment>
-    );
-  }
-
-  assertUnreachable(payStatus);
-}
diff --git a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx 
b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
index 4b5ff70dd..a55bc43dd 100644
--- a/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Refund/views.tsx
@@ -23,7 +23,7 @@ import { Part } from "../../components/Part.js";
 import { Link, SubTitle, WalletAction } from 
"../../components/styled/index.js";
 import { useTranslationContext } from "../../context/translation.js";
 import { Button } from "../../mui/Button.js";
-import { ProductList } from "../Payment/views.js";
+import { ProductList } from "../../components/ProductList.js";
 import { State } from "./index.js";
 
 export function LoadingUriView({ error }: State.LoadingUriError): VNode {
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
index 5c35151c8..9dbe24b7e 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -14,12 +14,12 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { ExchangeTosStatus } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 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 { SelectList } from "../../components/SelectList.js";
@@ -27,17 +27,14 @@ import {
   Input,
   Link,
   LinkSuccess,
-  SubTitle,
   SvgIcon,
-  WalletAction,
 } from "../../components/styled/index.js";
+import { TermsOfService } from "../../components/TermsOfService/index.js";
 import { useTranslationContext } from "../../context/translation.js";
 import { Button } from "../../mui/Button.js";
 import editIcon from "../../svg/edit_24px.svg";
 import { ExchangeDetails, WithdrawDetails } from "../../wallet/Transaction.js";
-import { TermsOfService } from "../../components/TermsOfService/index.js";
 import { State } from "./index.js";
-import { ExchangeTosStatus } from "@gnu-taler/taler-util";
 
 export function LoadingUriView({ error }: State.LoadingUriError): VNode {
   const { i18n } = useTranslationContext();
@@ -68,12 +65,7 @@ export function SuccessView(state: State.Success): VNode {
   const currentTosVersionIsAccepted =
     state.currentExchange.tosStatus === ExchangeTosStatus.Accepted;
   return (
-    <WalletAction>
-      <LogoHeader />
-      <SubTitle>
-        <i18n.Translate>Digital cash withdrawal</i18n.Translate>
-      </SubTitle>
-
+    <Fragment>
       {state.doWithdrawal.error && (
         <ErrorTalerOperation
           title={
@@ -161,7 +153,7 @@ export function SuccessView(state: State.Success): VNode {
           <i18n.Translate>Cancel</i18n.Translate>
         </Link>
       </section>
-    </WalletAction>
+    </Fragment>
   );
 }
 
diff --git a/packages/taler-wallet-webextension/src/popup/Application.tsx 
b/packages/taler-wallet-webextension/src/popup/Application.tsx
index 8186c6790..9cae0d048 100644
--- a/packages/taler-wallet-webextension/src/popup/Application.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Application.tsx
@@ -21,7 +21,7 @@
  */
 
 import { createHashHistory } from "history";
-import { Fragment, h, VNode } from "preact";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
 import Router, { route, Route } from "preact-router";
 import { Match } from "preact-router/match";
 import { useEffect, useState } from "preact/hooks";
@@ -34,15 +34,28 @@ import {
   useTranslationContext,
 } from "../context/translation.js";
 import { useTalerActionURL } from "../hooks/useTalerActionURL.js";
-import { Pages, PopupNavBar } from "../NavigationBar.js";
+import { PopupNavBarOptions, Pages, PopupNavBar } from "../NavigationBar.js";
 import { platform } from "../platform/api.js";
 import { BackupPage } from "../wallet/BackupPage.js";
 import { ProviderDetailPage } from "../wallet/ProviderDetailPage.js";
 import { BalancePage } from "./BalancePage.js";
 import { TalerActionFound } from "./TalerActionFound.js";
 
-function CheckTalerActionComponent(): VNode {
-  const [action] = useTalerActionURL();
+export function Application(): VNode {
+  return (
+    <TranslationProvider>
+      <DevContextProvider>
+        <IoCProviderForRuntime>
+          <ApplicationView />
+        </IoCProviderForRuntime>
+      </DevContextProvider>
+    </TranslationProvider>
+  );
+}
+function ApplicationView(): VNode {
+  const hash_history = createHashHistory();
+
+  const [action, setDismissed] = useTalerActionURL();
 
   const actionUri = action?.uri;
 
@@ -52,116 +65,110 @@ function CheckTalerActionComponent(): VNode {
     }
   }, [actionUri]);
 
-  return <Fragment />;
-}
+  async function redirectToTxInfo(tid: string): Promise<void> {
+    redirectTo(Pages.balanceTransaction({ tid }));
+  }
 
-export function Application(): VNode {
-  const hash_history = createHashHistory();
   return (
-    <TranslationProvider>
-      <DevContextProvider>
-        {({ devMode }: { devMode: boolean }) => (
-          <IoCProviderForRuntime>
-            <PendingTransactions
-              goToTransaction={(tid: string) =>
-                redirectTo(Pages.balanceTransaction({ tid }))
+    <Router history={hash_history}>
+      <Route
+        path={Pages.balance}
+        component={() => (
+          <PopupTemplate path="balance" goToTransaction={redirectToTxInfo}>
+            <BalancePage
+              goToWalletManualWithdraw={() => 
redirectTo(Pages.receiveCash({}))}
+              goToWalletDeposit={(currency: string) =>
+                redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+              }
+              goToWalletHistory={(currency: string) =>
+                redirectTo(Pages.balanceHistory({ currency }))
               }
             />
-            <Match>
-              {({ path }: { path: string }) => <PopupNavBar path={path} />}
-            </Match>
-            <CheckTalerActionComponent />
-            <PopupBox devMode={devMode}>
-              <Router history={hash_history}>
-                <Route
-                  path={Pages.balance}
-                  component={BalancePage}
-                  goToWalletManualWithdraw={() =>
-                    redirectTo(Pages.receiveCash({}))
-                  }
-                  goToWalletDeposit={(currency: string) =>
-                    redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
-                  }
-                  goToWalletHistory={(currency: string) =>
-                    redirectTo(Pages.balanceHistory({ currency }))
-                  }
-                />
-
-                <Route
-                  path={Pages.cta.pattern}
-                  component={function Action({ action }: { action: string }) {
-                    const [, setDismissed] = useTalerActionURL();
-
-                    return (
-                      <TalerActionFound
-                        url={decodeURIComponent(action)}
-                        onDismiss={() => {
-                          setDismissed(true);
-                          return redirectTo(Pages.balance);
-                        }}
-                      />
-                    );
-                  }}
-                />
-
-                <Route
-                  path={Pages.backup}
-                  component={BackupPage}
-                  onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
-                />
-                <Route
-                  path={Pages.backupProviderDetail.pattern}
-                  component={ProviderDetailPage}
-                  onBack={() => redirectTo(Pages.backup)}
-                />
-
-                <Route
-                  path={Pages.balanceTransaction.pattern}
-                  component={RedirectToWalletPage}
-                />
-                <Route
-                  path={Pages.ctaWithdrawManual.pattern}
-                  component={RedirectToWalletPage}
-                />
-                <Route
-                  path={Pages.balanceDeposit.pattern}
-                  component={RedirectToWalletPage}
-                />
-                <Route
-                  path={Pages.balanceHistory.pattern}
-                  component={RedirectToWalletPage}
-                />
-                <Route
-                  path={Pages.backupProviderAdd}
-                  component={RedirectToWalletPage}
-                />
-                <Route
-                  path={Pages.receiveCash.pattern}
-                  component={RedirectToWalletPage}
-                />
-                <Route
-                  path={Pages.sendCash.pattern}
-                  component={RedirectToWalletPage}
-                />
-                <Route path={Pages.qr} component={RedirectToWalletPage} />
-                <Route path={Pages.settings} component={RedirectToWalletPage} 
/>
-                <Route
-                  path={Pages.settingsExchangeAdd.pattern}
-                  component={RedirectToWalletPage}
-                />
-                <Route path={Pages.dev} component={RedirectToWalletPage} />
-                <Route
-                  path={Pages.notifications}
-                  component={RedirectToWalletPage}
-                />
-
-                <Route default component={Redirect} to={Pages.balance} />
-              </Router>
-            </PopupBox>
-          </IoCProviderForRuntime>
+          </PopupTemplate>
         )}
-      </DevContextProvider>
-    </TranslationProvider>
+      />
+
+      <Route
+        path={Pages.cta.pattern}
+        component={function Action({ action }: { action: string }) {
+          // const [, setDismissed] = useTalerActionURL();
+
+          return (
+            <PopupTemplate>
+              <TalerActionFound
+                url={decodeURIComponent(action)}
+                onDismiss={() => {
+                  setDismissed(true);
+                  return redirectTo(Pages.balance);
+                }}
+              />
+            </PopupTemplate>
+          );
+        }}
+      />
+
+      <Route
+        path={Pages.backup}
+        component={() => (
+          <PopupTemplate path="backup" goToTransaction={redirectToTxInfo}>
+            <BackupPage
+              onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
+            />
+          </PopupTemplate>
+        )}
+      />
+      <Route
+        path={Pages.backupProviderDetail.pattern}
+        component={({ pid }: { pid: string }) => (
+          <PopupTemplate path="backup">
+            <ProviderDetailPage
+              onPayProvider={(uri: string) =>
+                redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+              }
+              onWithdraw={(amount: string) =>
+                redirectTo(Pages.receiveCash({ amount }))
+              }
+              pid={pid}
+              onBack={() => redirectTo(Pages.backup)}
+            />
+          </PopupTemplate>
+        )}
+      />
+
+      <Route
+        path={Pages.balanceTransaction.pattern}
+        component={RedirectToWalletPage}
+      />
+      <Route
+        path={Pages.ctaWithdrawManual.pattern}
+        component={RedirectToWalletPage}
+      />
+      <Route
+        path={Pages.balanceDeposit.pattern}
+        component={RedirectToWalletPage}
+      />
+      <Route
+        path={Pages.balanceHistory.pattern}
+        component={RedirectToWalletPage}
+      />
+      <Route path={Pages.backupProviderAdd} component={RedirectToWalletPage} />
+      <Route
+        path={Pages.receiveCash.pattern}
+        component={RedirectToWalletPage}
+      />
+      <Route path={Pages.sendCash.pattern} component={RedirectToWalletPage} />
+      <Route path={Pages.ctaPay} component={RedirectToWalletPage} />
+      <Route path={Pages.qr} component={RedirectToWalletPage} />
+      <Route path={Pages.settings} component={RedirectToWalletPage} />
+      <Route
+        path={Pages.settingsExchangeAdd.pattern}
+        component={RedirectToWalletPage}
+      />
+      <Route path={Pages.dev} component={RedirectToWalletPage} />
+      <Route path={Pages.notifications} component={RedirectToWalletPage} />
+
+      <Route default component={Redirect} to={Pages.balance} />
+    </Router>
   );
 }
 
@@ -195,3 +202,24 @@ function Redirect({ to }: { to: string }): null {
   });
   return null;
 }
+
+function PopupTemplate({
+  path,
+  children,
+  goToTransaction,
+}: {
+  path?: PopupNavBarOptions;
+  children: ComponentChildren;
+  goToTransaction?: (id: string) => Promise<void>;
+}): VNode {
+  return (
+    <Fragment>
+      {/* <CheckTalerActionComponent /> */}
+      {goToTransaction ? (
+        <PendingTransactions goToTransaction={goToTransaction} />
+      ) : undefined}
+      <PopupNavBar path={path} />
+      <PopupBox>{children}</PopupBox>
+    </Fragment>
+  );
+}
diff --git a/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts 
b/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts
index c9327b8e6..82d11a15a 100644
--- a/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts
+++ b/packages/taler-wallet-webextension/src/serviceWorkerHttpLib.ts
@@ -69,7 +69,7 @@ export class ServiceWorkerHttpLib implements 
HttpRequestLibrary {
       } else if (ArrayBuffer.isView(requestBody)) {
         myBody = requestBody;
       } else if (typeof requestBody === "object") {
-        myBody = JSON.stringify(myBody);
+        myBody = JSON.stringify(requestBody);
       } else {
         throw Error("unsupported request body type");
       }
@@ -127,8 +127,6 @@ export class ServiceWorkerHttpLib implements 
HttpRequestLibrary {
     });
   }
 
-  // FIXME: "Content-Type: application/json" goes here,
-  // after Sebastian suggestion.
   postJson(
     url: string,
     body: any,
diff --git a/packages/taler-wallet-webextension/src/stories.tsx 
b/packages/taler-wallet-webextension/src/stories.tsx
index 8834b8084..a7b8a4d06 100644
--- a/packages/taler-wallet-webextension/src/stories.tsx
+++ b/packages/taler-wallet-webextension/src/stories.tsx
@@ -20,7 +20,11 @@
  */
 import { Fragment, FunctionComponent, h } from "preact";
 import { LogoHeader } from "./components/LogoHeader.js";
-import { PopupBox, WalletBox } from "./components/styled/index.js";
+import {
+  PopupBox,
+  WalletAction,
+  WalletBox,
+} from "./components/styled/index.js";
 import { strings } from "./i18n/strings.js";
 import { PopupNavBar, WalletNavBar } from "./NavigationBar.js";
 
@@ -72,7 +76,7 @@ function getWrapperForGroup(group: string): FunctionComponent 
{
       return function WalletWrapper({ children }: any) {
         return (
           <Fragment>
-            <WalletBox>{children}</WalletBox>
+            <WalletAction>{children}</WalletAction>
           </Fragment>
         );
       };
diff --git a/packages/taler-wallet-webextension/src/utils/index.ts 
b/packages/taler-wallet-webextension/src/utils/index.ts
index c2d7c10a8..ad4eabf15 100644
--- a/packages/taler-wallet-webextension/src/utils/index.ts
+++ b/packages/taler-wallet-webextension/src/utils/index.ts
@@ -74,7 +74,7 @@ export async function queryToSlashKeys<T>(url: string): 
Promise<T> {
   return timeout(3000, query);
 }
 
-export type StateFunc<S> = (p: S) => VNode;
+export type StateFunc<S> = (p: S) => VNode | null;
 
 export type StateViewMap<StateType extends { status: string }> = {
   [S in StateType as S["status"]]: StateFunc<S>;
diff --git 
a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts 
b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
index 94020069b..10fcd84ce 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/index.ts
@@ -32,7 +32,6 @@ import {
 } from "./views.js";
 
 export interface Props {
-  currency: string;
   onBack: () => Promise<void>;
   onComplete: (pid: string) => Promise<void>;
   onPaymentRequired: (uri: string) => Promise<void>;
diff --git 
a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts 
b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
index 32c48be91..1b30ed0cd 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
@@ -144,7 +144,6 @@ function useUrlState<T>(
 }
 
 export function useComponentState({
-  currency,
   onBack,
   onComplete,
   onPaymentRequired,
diff --git 
a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts 
b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
index 9abb672fa..3241a3ab0 100644
--- a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/test.ts
@@ -26,7 +26,6 @@ import { Props } from "./index.js";
 import { useComponentState } from "./state.js";
 
 const props: Props = {
-  currency: "KUDOS",
   onBack: nullFunction,
   onComplete: nullFunction,
   onPaymentRequired: nullFunction,
diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx 
b/packages/taler-wallet-webextension/src/wallet/Application.tsx
index d150ebfaf..8b77e152c 100644
--- a/packages/taler-wallet-webextension/src/wallet/Application.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx
@@ -20,352 +20,452 @@
  * @author sebasjm
  */
 
+import { TranslatedString } from "@gnu-taler/taler-util";
 import { createHashHistory } from "history";
-import { Fragment, h, VNode } from "preact";
+import { ComponentChildren, Fragment, h, VNode } from "preact";
 import Router, { route, Route } from "preact-router";
-import Match from "preact-router/match";
-import { useEffect, useState } from "preact/hooks";
+import { useEffect } from "preact/hooks";
 import { LogoHeader } from "../components/LogoHeader.js";
 import PendingTransactions from "../components/PendingTransactions.js";
-import { SuccessBox, WalletBox } from "../components/styled/index.js";
+import {
+  SubTitle,
+  WalletAction,
+  WalletBox,
+} from "../components/styled/index.js";
 import { DevContextProvider } from "../context/devContext.js";
 import { IoCProviderForRuntime } from "../context/iocContext.js";
 import {
   TranslationProvider,
   useTranslationContext,
 } from "../context/translation.js";
+import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js";
+import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js";
+import { InvoicePayPage } from "../cta/InvoicePay/index.js";
 import { PaymentPage } from "../cta/Payment/index.js";
+import { RecoveryPage } from "../cta/Recovery/index.js";
 import { RefundPage } from "../cta/Refund/index.js";
 import { TipPage } from "../cta/Tip/index.js";
+import { TransferCreatePage } from "../cta/TransferCreate/index.js";
+import { TransferPickupPage } from "../cta/TransferPickup/index.js";
 import {
   WithdrawPageFromParams,
   WithdrawPageFromURI,
 } from "../cta/Withdraw/index.js";
-import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js";
-import { Pages, WalletNavBar } from "../NavigationBar.js";
-import { DeveloperPage } from "./DeveloperPage.js";
+import { WalletNavBarOptions, Pages, WalletNavBar } from "../NavigationBar.js";
+import { platform } from "../platform/api.js";
+import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
 import { BackupPage } from "./BackupPage.js";
 import { DepositPage } from "./DepositPage/index.js";
+import { DestinationSelectionPage } from "./DestinationSelection/index.js";
+import { DeveloperPage } from "./DeveloperPage.js";
 import { ExchangeAddPage } from "./ExchangeAddPage.js";
 import { HistoryPage } from "./History.js";
+import { NotificationsPage } from "./Notifications/index.js";
 import { ProviderDetailPage } from "./ProviderDetailPage.js";
+import { QrReaderPage } from "./QrReader.js";
 import { SettingsPage } from "./Settings.js";
 import { TransactionPage } from "./Transaction.js";
 import { WelcomePage } from "./Welcome.js";
-import { QrReaderPage } from "./QrReader.js";
-import { platform } from "../platform/api.js";
-import { DestinationSelectionPage } from "./DestinationSelection/index.js";
-import { ExchangeSelectionPage } from "./ExchangeSelection/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";
-import { RecoveryPage } from "../cta/Recovery/index.js";
-import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
-import { NotificationsPage } from "./Notifications/index.js";
 
 export function Application(): VNode {
-  const [globalNotification, setGlobalNotification] = useState<
-    VNode | undefined
-  >(undefined);
-  const hash_history = createHashHistory();
-  function clearNotification(): void {
-    setGlobalNotification(undefined);
-  }
-  function clearNotificationWhenMovingOut(): void {
-    // const movingOutFromNotification =
-    //   globalNotification && e.url !== globalNotification.to;
-    if (globalNotification) {
-      //&& movingOutFromNotification) {
-      setGlobalNotification(undefined);
-    }
-  }
   const { i18n } = useTranslationContext();
+  const hash_history = createHashHistory();
 
+  async function redirectToTxInfo(tid: string): Promise<void> {
+    redirectTo(Pages.balanceTransaction({ tid }));
+  }
   return (
     <TranslationProvider>
       <DevContextProvider>
         <IoCProviderForRuntime>
-          {/* <Match/> won't work in the first render if <Router /> is not 
called first */}
-          {/* https://github.com/preactjs/preact-router/issues/415 */}
           <Router history={hash_history}>
-            <Match default>
-              {({ path }: { path: string }) => {
-                if (path && path.startsWith("/cta")) return;
-                return (
-                  <Fragment>
-                    <LogoHeader />
-                    <WalletNavBar path={path} />
-                    {shouldShowPendingOperations(path) && (
-                      <div
-                        style={{
-                          backgroundColor: "lightcyan",
-                          display: "flex",
-                          justifyContent: "center",
-                        }}
-                      >
-                        <PendingTransactions
-                          goToTransaction={(tid: string) =>
-                            redirectTo(Pages.balanceTransaction({ tid }))
-                          }
-                        />
-                      </div>
-                    )}
-                  </Fragment>
-                );
-              }}
-            </Match>
-          </Router>
-          <WalletBox>
-            {globalNotification && (
-              <SuccessBox onClick={clearNotification}>
-                <div>{globalNotification}</div>
-              </SuccessBox>
-            )}
-            <Router
-              history={hash_history}
-              onChange={clearNotificationWhenMovingOut}
-            >
-              <Route path={Pages.welcome} component={WelcomePage} />
-
-              {/**
-               * BALANCE
-               */}
+            <Route
+              path={Pages.welcome}
+              component={() => (
+                <WalletTemplate>
+                  <WelcomePage />
+                </WalletTemplate>
+              )}
+            />
 
-              <Route
-                path={Pages.balanceHistory.pattern}
-                component={HistoryPage}
-                goToWalletDeposit={(currency: string) =>
-                  redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
-                }
-                goToWalletManualWithdraw={(currency?: string) =>
-                  redirectTo(
-                    Pages.receiveCash({
-                      amount: !currency ? undefined : `${currency}:0`,
-                    }),
-                  )
-                }
-              />
-              <Route path={Pages.exchanges} component={ExchangeSelectionPage} 
/>
-              <Route
-                path={Pages.sendCash.pattern}
-                type="send"
-                component={DestinationSelectionPage}
-                goToWalletBankDeposit={(amount: string) =>
-                  redirectTo(Pages.balanceDeposit({ amount }))
-                }
-                goToWalletWalletSend={(amount: string) =>
-                  redirectTo(Pages.ctaTransferCreate({ amount }))
-                }
-              />
-              <Route
-                path={Pages.receiveCash.pattern}
-                type="get"
-                component={DestinationSelectionPage}
-                goToWalletManualWithdraw={(amount?: string) =>
-                  redirectTo(Pages.ctaWithdrawManual({ amount }))
-                }
-                goToWalletWalletInvoice={(amount?: string) =>
-                  redirectTo(Pages.ctaInvoiceCreate({ amount }))
-                }
-              />
+            <Route
+              path={Pages.qr}
+              component={() => (
+                <WalletTemplate goToTransaction={redirectToTxInfo}>
+                  <QrReaderPage
+                    onDetected={(talerActionUrl: string) => {
+                      platform.openWalletURIFromPopup(talerActionUrl);
+                    }}
+                  />
+                </WalletTemplate>
+              )}
+            />
 
-              <Route
-                path={Pages.balanceTransaction.pattern}
-                component={TransactionPage}
-                goToWalletHistory={(currency?: string) =>
-                  redirectTo(Pages.balanceHistory({ currency }))
-                }
-              />
+            <Route
+              path={Pages.settings}
+              component={() => (
+                <WalletTemplate goToTransaction={redirectToTxInfo}>
+                  <SettingsPage />
+                </WalletTemplate>
+              )}
+            />
+            <Route
+              path={Pages.notifications}
+              component={() => (
+                <WalletTemplate>
+                  <NotificationsPage />
+                </WalletTemplate>
+              )}
+            />
+            {/**
+             * SETTINGS
+             */}
+            <Route
+              path={Pages.settingsExchangeAdd.pattern}
+              component={() => (
+                <WalletTemplate>
+                  <ExchangeAddPage onBack={() => redirectTo(Pages.balance)} />
+                </WalletTemplate>
+              )}
+            />
 
-              <Route
-                path={Pages.balanceDeposit.pattern}
-                component={DepositPage}
-                onCancel={(currency: string) => {
-                  redirectTo(Pages.balanceHistory({ currency }));
-                }}
-                onSuccess={(currency: string) => {
-                  redirectTo(Pages.balanceHistory({ currency }));
-                  setGlobalNotification(
-                    <i18n.Translate>
-                      All done, your transaction is in progress
-                    </i18n.Translate>,
-                  );
-                }}
-              />
-              {/**
-               * PENDING
-               */}
-              <Route
-                path={Pages.qr}
-                component={QrReaderPage}
-                onDetected={(talerActionUrl: string) => {
-                  platform.openWalletURIFromPopup(talerActionUrl);
-                }}
-              />
+            <Route
+              path={Pages.balanceHistory.pattern}
+              component={() => (
+                <WalletTemplate
+                  path="balance"
+                  goToTransaction={redirectToTxInfo}
+                >
+                  <HistoryPage
+                    goToWalletDeposit={(currency: string) =>
+                      redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
+                    }
+                    goToWalletManualWithdraw={(currency?: string) =>
+                      redirectTo(
+                        Pages.receiveCash({
+                          amount: !currency ? undefined : `${currency}:0`,
+                        }),
+                      )
+                    }
+                  />
+                </WalletTemplate>
+              )}
+            />
+            <Route
+              path={Pages.sendCash.pattern}
+              component={({ amount }: { amount?: string }) => (
+                <WalletTemplate path="balance">
+                  <DestinationSelectionPage
+                    type="send"
+                    amount={amount}
+                    goToWalletBankDeposit={(amount: string) =>
+                      redirectTo(Pages.balanceDeposit({ amount }))
+                    }
+                    goToWalletWalletSend={(amount: string) =>
+                      redirectTo(Pages.ctaTransferCreate({ amount }))
+                    }
+                  />
+                </WalletTemplate>
+              )}
+            />
+            <Route
+              path={Pages.receiveCash.pattern}
+              component={({ amount }: { amount?: string }) => (
+                <WalletTemplate path="balance">
+                  <DestinationSelectionPage
+                    type="get"
+                    amount={amount}
+                    goToWalletManualWithdraw={(amount?: string) =>
+                      redirectTo(Pages.ctaWithdrawManual({ amount }))
+                    }
+                    goToWalletWalletInvoice={(amount?: string) =>
+                      redirectTo(Pages.ctaInvoiceCreate({ amount }))
+                    }
+                  />
+                </WalletTemplate>
+              )}
+            />
 
-              <Route path={Pages.settings} component={SettingsPage} />
-              <Route path={Pages.notifications} component={NotificationsPage} 
/>
+            <Route
+              path={Pages.balanceTransaction.pattern}
+              component={({ tid }: { tid: string }) => (
+                <WalletTemplate path="balance">
+                  <TransactionPage
+                    tid={tid}
+                    goToWalletHistory={(currency?: string) =>
+                      redirectTo(Pages.balanceHistory({ currency }))
+                    }
+                  />
+                </WalletTemplate>
+              )}
+            />
 
-              {/**
-               * BACKUP
-               */}
-              <Route
-                path={Pages.backup}
-                component={BackupPage}
-                onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
-              />
-              <Route
-                path={Pages.backupProviderDetail.pattern}
-                component={ProviderDetailPage}
-                onPayProvider={(uri: string) =>
-                  redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
-                }
-                onWithdraw={(amount: string) =>
-                  redirectTo(Pages.receiveCash({ amount }))
-                }
-                onBack={() => redirectTo(Pages.backup)}
-              />
-              <Route
-                path={Pages.backupProviderAdd}
-                component={AddBackupProviderPage}
-                onPaymentRequired={(uri: string) =>
-                  redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
-                }
-                onComplete={(pid: string) =>
-                  redirectTo(Pages.backupProviderDetail({ pid }))
-                }
-                onBack={() => redirectTo(Pages.backup)}
-              />
+            <Route
+              path={Pages.balanceDeposit.pattern}
+              component={() => (
+                <WalletTemplate path="balance">
+                  <DepositPage
+                    onCancel={(currency: string) => {
+                      redirectTo(Pages.balanceHistory({ currency }));
+                    }}
+                    onSuccess={(currency: string) => {
+                      redirectTo(Pages.balanceHistory({ currency }));
+                    }}
+                  />
+                </WalletTemplate>
+              )}
+            />
 
-              {/**
-               * SETTINGS
-               */}
-              <Route
-                path={Pages.settingsExchangeAdd.pattern}
-                component={ExchangeAddPage}
-                onBack={() => redirectTo(Pages.balance)}
-              />
+            <Route
+              path={Pages.backup}
+              component={() => (
+                <WalletTemplate
+                  path="backup"
+                  goToTransaction={redirectToTxInfo}
+                >
+                  <BackupPage
+                    onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
+                  />
+                </WalletTemplate>
+              )}
+            />
+            <Route
+              path={Pages.backupProviderDetail.pattern}
+              component={({ pid }: { pid: string }) => (
+                <WalletTemplate>
+                  <ProviderDetailPage
+                    pid={pid}
+                    onPayProvider={(uri: string) =>
+                      redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+                    }
+                    onWithdraw={(amount: string) =>
+                      redirectTo(Pages.receiveCash({ amount }))
+                    }
+                    onBack={() => redirectTo(Pages.backup)}
+                  />
+                </WalletTemplate>
+              )}
+            />
+            <Route
+              path={Pages.backupProviderAdd}
+              component={() => (
+                <WalletTemplate>
+                  <AddBackupProviderPage
+                    onPaymentRequired={(uri: string) =>
+                      redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
+                    }
+                    onComplete={(pid: string) =>
+                      redirectTo(Pages.backupProviderDetail({ pid }))
+                    }
+                    onBack={() => redirectTo(Pages.backup)}
+                  />
+                </WalletTemplate>
+              )}
+            />
 
-              {/**
-               * DEV
-               */}
+            {/**
+             * DEV
+             */}
+            <Route
+              path={Pages.dev}
+              component={() => (
+                <WalletTemplate path="dev" goToTransaction={redirectToTxInfo}>
+                  <DeveloperPage />
+                </WalletTemplate>
+              )}
+            />
 
-              <Route path={Pages.dev} component={DeveloperPage} />
+            {/**
+             * CALL TO ACTION
+             */}
+            <Route
+              path={Pages.ctaPay}
+              component={({ talerPayUri }: { talerPayUri: string }) => (
+                <CallToActionTemplate title={i18n.str`Digital cash payment`}>
+                  <PaymentPage
+                    talerPayUri={talerPayUri}
+                    goToWalletManualWithdraw={(amount?: string) =>
+                      redirectTo(Pages.receiveCash({ amount }))
+                    }
+                    cancel={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaRefund}
+              component={({ talerRefundUri }: { talerRefundUri: string }) => (
+                <CallToActionTemplate title={i18n.str`Digital cash refund`}>
+                  <RefundPage
+                    talerRefundUri={talerRefundUri}
+                    cancel={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaTips}
+              component={({ talerTipUri }: { talerTipUri: string }) => (
+                <CallToActionTemplate title={i18n.str`Digital cash tip`}>
+                  <TipPage
+                    talerTipUri={talerTipUri}
+                    onCancel={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaWithdraw}
+              component={({
+                talerWithdrawUri,
+              }: {
+                talerWithdrawUri: string;
+              }) => (
+                <CallToActionTemplate title={i18n.str`Digital cash 
withdrawal`}>
+                  <WithdrawPageFromURI
+                    talerWithdrawUri={talerWithdrawUri}
+                    cancel={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaWithdrawManual.pattern}
+              component={({ amount }: { amount: string }) => (
+                <CallToActionTemplate title={i18n.str`Digital cash 
withdrawal`}>
+                  <WithdrawPageFromParams
+                    amount={amount}
+                    cancel={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaDeposit}
+              component={({
+                amountStr,
+                talerDepositUri,
+              }: {
+                amountStr: string;
+                talerDepositUri: string;
+              }) => (
+                <CallToActionTemplate title={i18n.str`Digital cash deposit`}>
+                  <DepositPageCTA
+                    amountStr={amountStr}
+                    talerDepositUri={talerDepositUri}
+                    cancel={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaInvoiceCreate.pattern}
+              component={({ amount }: { amount: string }) => (
+                <CallToActionTemplate title={i18n.str`Digital cash invoice`}>
+                  <InvoiceCreatePage
+                    amount={amount}
+                    onClose={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaTransferCreate.pattern}
+              component={({ amount }: { amount: string }) => (
+                <CallToActionTemplate title={i18n.str`Digital cash transfer`}>
+                  <TransferCreatePage
+                    amount={amount}
+                    onClose={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaInvoicePay}
+              component={({ talerPayPullUri }: { talerPayPullUri: string }) => 
(
+                <CallToActionTemplate title={i18n.str`Digital cash invoice`}>
+                  <InvoicePayPage
+                    talerPayPullUri={talerPayPullUri}
+                    goToWalletManualWithdraw={(amount?: string) =>
+                      redirectTo(Pages.receiveCash({ amount }))
+                    }
+                    onClose={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaTransferPickup}
+              component={({ talerPayPushUri }: { talerPayPushUri: string }) => 
(
+                <CallToActionTemplate title={i18n.str`Digital cash transfer`}>
+                  <TransferPickupPage
+                    talerPayPushUri={talerPayPushUri}
+                    onClose={() => redirectTo(Pages.balance)}
+                    onSuccess={(tid: string) =>
+                      redirectTo(Pages.balanceTransaction({ tid }))
+                    }
+                  />
+                </CallToActionTemplate>
+              )}
+            />
+            <Route
+              path={Pages.ctaRecovery}
+              component={({
+                talerRecoveryUri,
+              }: {
+                talerRecoveryUri: string;
+              }) => (
+                <CallToActionTemplate title={i18n.str`Digital cash recovery`}>
+                  <RecoveryPage
+                    talerRecoveryUri={talerRecoveryUri}
+                    onCancel={() => redirectTo(Pages.balance)}
+                    onSuccess={() => redirectTo(Pages.backup)}
+                  />
+                </CallToActionTemplate>
+              )}
+            />
 
-              {/**
-               * CALL TO ACTION
-               */}
-              <Route
-                path={Pages.ctaPay}
-                component={PaymentPage}
-                goToWalletManualWithdraw={(amount?: string) =>
-                  redirectTo(Pages.receiveCash({ amount }))
-                }
-                cancel={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaRefund}
-                component={RefundPage}
-                cancel={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaTips}
-                component={TipPage}
-                onCancel={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaWithdraw}
-                component={WithdrawPageFromURI}
-                cancel={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaWithdrawManual.pattern}
-                component={WithdrawPageFromParams}
-                cancel={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaDeposit}
-                component={DepositPageCTA}
-                cancel={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaInvoiceCreate.pattern}
-                component={InvoiceCreatePage}
-                onClose={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaTransferCreate.pattern}
-                component={TransferCreatePage}
-                onClose={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaInvoicePay}
-                component={InvoicePayPage}
-                goToWalletManualWithdraw={(amount?: string) =>
-                  redirectTo(Pages.receiveCash({ amount }))
-                }
-                onClose={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaTransferPickup}
-                component={TransferPickupPage}
-                onClose={() => redirectTo(Pages.balance)}
-                onSuccess={(tid: string) =>
-                  redirectTo(Pages.balanceTransaction({ tid }))
-                }
-              />
-              <Route
-                path={Pages.ctaRecovery}
-                component={RecoveryPage}
-                onCancel={() => redirectTo(Pages.balance)}
-                onSuccess={() => redirectTo(Pages.backup)}
-              />
+            {/**
+             * NOT FOUND
+             * all redirects should be at the end
+             */}
+            <Route
+              path={Pages.balance}
+              component={() => <Redirect to={Pages.balanceHistory({})} />}
+            />
 
-              {/**
-               * NOT FOUND
-               * all redirects should be at the end
-               */}
-              <Route
-                path={Pages.balance}
-                component={Redirect}
-                to={Pages.balanceHistory({})}
-              />
-
-              <Route
-                default
-                component={Redirect}
-                to={Pages.balanceHistory({})}
-              />
-            </Router>
-          </WalletBox>
+            <Route
+              default
+              component={() => <Redirect to={Pages.balanceHistory({})} />}
+            />
+          </Router>
         </IoCProviderForRuntime>
       </DevContextProvider>
     </TranslationProvider>
@@ -403,3 +503,40 @@ function shouldShowPendingOperations(url: string): boolean 
{
     Pages.backup,
   ].some((p) => matchesRoute(url, p));
 }
+
+function CallToActionTemplate({
+  title,
+  children,
+}: {
+  title: TranslatedString;
+  children: ComponentChildren;
+}): VNode {
+  return (
+    <WalletAction>
+      <LogoHeader />
+      <SubTitle>{title}</SubTitle>
+      {children}
+    </WalletAction>
+  );
+}
+
+function WalletTemplate({
+  path,
+  children,
+  goToTransaction,
+}: {
+  path?: WalletNavBarOptions;
+  children: ComponentChildren;
+  goToTransaction?: (id: string) => Promise<void>;
+}): VNode {
+  return (
+    <Fragment>
+      <LogoHeader />
+      <WalletNavBar path={path} />
+      {goToTransaction ? (
+        <PendingTransactions goToTransaction={goToTransaction} />
+      ) : undefined}
+      <WalletBox>{children}</WalletBox>
+    </Fragment>
+  );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx 
b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
index 4805c03ca..74e7ce611 100644
--- a/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DeveloperPage.tsx
@@ -92,6 +92,7 @@ type CoinsInfo = CoinDumpJson["coins"];
 type CalculatedCoinfInfo = {
   ageKeysCount: number | undefined;
   denom_value: number;
+  denom_fraction: number;
   //remain_value: number;
   status: string;
   from_refresh: boolean;
@@ -151,7 +152,8 @@ export function View({
       }
       prev[cur.exchange_base_url].push({
         ageKeysCount: cur.ageCommitmentProof?.proof.privateKeys.length,
-        denom_value: parseFloat(Amounts.stringifyValue(denom)),
+        denom_value: denom.value,
+        denom_fraction: denom.fraction,
         // remain_value: parseFloat(
         //   Amounts.stringifyValue(Amounts.parseOrThrow(cur.remaining_value)),
         // ),
@@ -340,7 +342,10 @@ export function View({
       {Object.keys(money_by_exchange).map((ex, idx) => {
         const allcoins = money_by_exchange[ex];
         allcoins.sort((a, b) => {
-          return b.denom_value - a.denom_value;
+          if (b.denom_value !== a.denom_value) {
+            return b.denom_value - a.denom_value;
+          }
+          return b.denom_fraction - a.denom_fraction;
         });
 
         const coins = allcoins.reduce(
@@ -407,11 +412,31 @@ function ShowAllCoins({
   const { i18n } = useTranslationContext();
   const [collapsedSpent, setCollapsedSpent] = useState(true);
   const [collapsedUnspent, setCollapsedUnspent] = useState(false);
-  const total = coins.usable.reduce((prev, cur) => prev + cur.denom_value, 0);
+  const totalUsable = coins.usable.reduce(
+    (prev, cur) =>
+      Amounts.add(prev, {
+        currency: "NONE",
+        fraction: cur.denom_fraction,
+        value: cur.denom_value,
+      }).amount,
+    Amounts.zeroOfCurrency("NONE"),
+  );
+  const totalSpent = coins.spent.reduce(
+    (prev, cur) =>
+      Amounts.add(prev, {
+        currency: "NONE",
+        fraction: cur.denom_fraction,
+        value: cur.denom_value,
+      }).amount,
+    Amounts.zeroOfCurrency("NONE"),
+  );
   return (
     <Fragment>
       <p>
-        <b>{ex}</b>: {total} {currencies[ex]}
+        <b>{ex}</b>: {Amounts.stringifyValue(totalUsable)} {currencies[ex]}
+      </p>
+      <p>
+        spent: {Amounts.stringifyValue(totalSpent)} {currencies[ex]}
       </p>
       <p onClick={() => setCollapsedUnspent(true)}>
         <b>

-- 
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]