gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: fixing #6096


From: gnunet
Subject: [taler-wallet-core] branch master updated: fixing #6096
Date: Mon, 08 Aug 2022 19:09:46 +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 7a600514 fixing #6096
7a600514 is described below

commit 7a600514c6d43bbaeba6b962533415e59fc46057
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Aug 8 14:09:28 2022 -0300

    fixing #6096
    
    merchant details and contract terms details factored out, to be used by 
other components
    tests and stories updated
    payment completed != confirmed (confirmed if paid by someone else)
---
 .../src/components/Modal.tsx                       |  91 +++++
 .../ShowFullContractTermPopup.stories.tsx          | 116 +++++++
 .../src/components/ShowFullContractTermPopup.tsx   | 385 +++++++++++++++++++++
 .../src/components/index.stories.tsx               |   5 +-
 .../src/components/styled/index.tsx                |  10 +
 .../src/cta/Payment/index.ts                       |  16 +-
 .../src/cta/Payment/state.ts                       |  71 ++--
 .../src/cta/Payment/stories.tsx                    | 175 ++++++----
 .../src/cta/Payment/test.ts                        |  24 +-
 .../src/cta/Payment/views.tsx                      | 178 ++++++----
 .../src/popup/Balance.stories.tsx                  |   9 +
 .../src/popup/BalancePage.tsx                      | 152 +++++---
 .../src/popup/NoBalanceHelp.tsx                    |   5 +-
 .../src/wallet/History.tsx                         |   6 +-
 .../src/wallet/Transaction.stories.tsx             |   8 +-
 .../src/wallet/Transaction.tsx                     | 174 +++++-----
 packages/taler-wallet-webextension/src/wxApi.ts    |   6 +
 17 files changed, 1126 insertions(+), 305 deletions(-)

diff --git a/packages/taler-wallet-webextension/src/components/Modal.tsx 
b/packages/taler-wallet-webextension/src/components/Modal.tsx
new file mode 100644
index 00000000..3fea063d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Modal.tsx
@@ -0,0 +1,91 @@
+/*
+ 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 { styled } from "@linaria/react";
+import { ComponentChildren, h, VNode } from "preact";
+import { ButtonHandler } from "../mui/handlers.js";
+import closeIcon from "../svg/close_24px.svg";
+import { Link, LinkPrimary, LinkWarning } from "./styled/index.js";
+
+interface Props {
+  children: ComponentChildren;
+  onClose: ButtonHandler;
+  title: string;
+}
+
+const FullSize = styled.div`
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  justify-content: center;
+  z-index: 10;
+`;
+
+const Header = styled.div`
+  display: flex;
+  justify-content: space-between;
+  height: 5%;
+  vertical-align: center;
+  align-items: center;
+`;
+
+const Body = styled.div`
+  height: 95%;
+`;
+
+export function Modal({ title, children, onClose }: Props): VNode {
+  return (
+    <FullSize onClick={onClose?.onClick}>
+      <div
+        onClick={(e) => e.stopPropagation()}
+        style={{
+          background: "white",
+          width: 600,
+          height: "80%",
+          margin: "auto",
+          borderRadius: 8,
+          padding: 8,
+          // overflow: "scroll",
+        }}
+      >
+        <Header>
+          <div>
+            <h2>{title}</h2>
+          </div>
+          <Link onClick={onClose?.onClick}>
+            <div
+              style={{
+                height: 24,
+                width: 24,
+                marginLeft: 4,
+                marginRight: 4,
+                // fill: "white",
+              }}
+              dangerouslySetInnerHTML={{ __html: closeIcon }}
+            />
+          </Link>
+        </Header>
+        <hr />
+
+        <Body onClick={(e: any) => e.stopPropagation()}>{children}</Body>
+      </div>
+    </FullSize>
+  );
+}
diff --git 
a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
 
b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
new file mode 100644
index 00000000..6f71b9d2
--- /dev/null
+++ 
b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
@@ -0,0 +1,116 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { WalletContractData } from "@gnu-taler/taler-wallet-core";
+import { createExample } from "../test-utils.js";
+import {
+  ErrorView,
+  HiddenView,
+  LoadingView,
+  ShowView,
+} from "./ShowFullContractTermPopup.js";
+
+export default {
+  title: "component/ShowFullContractTermPopup",
+};
+
+const cd: WalletContractData = {
+  amount: {
+    currency: "ARS",
+    fraction: 0,
+    value: 2,
+  },
+  contractTermsHash:
+    
"92X0KSJPZ8XS2XECCGFWTCGW8XMFCXTT2S6WHZDP6H9Y3TSKMTHY94WXEWDERTNN5XWCYGW4VN5CF2D4846HXTW7P06J4CZMHCWKC9G",
+  fulfillmentUrl: "",
+  merchantBaseUrl: "https://merchant-backend.taler.ar/";,
+  merchantPub: "JZYHJ13M91GMSQMT75J8Q6ZN0QP8XF8CRHR7K5MMWYE8JQB6AAPG",
+  merchantSig:
+    
"0YA1WETV15R6K8QKS79QA3QMT16010F42Q49VSKYQ71HVQKAG0A4ZJCA4YTKHE9EA5SP156TJSKZEJJJ87305N6PS80PC48RNKYZE08",
+  orderId: "2022.220-0281XKKB8W7YE",
+  summary: "w",
+  maxWireFee: {
+    currency: "ARS",
+    fraction: 0,
+    value: 1,
+  },
+  payDeadline: {
+    t_s: 1660002673,
+  },
+  refundDeadline: {
+    t_s: 1660002673,
+  },
+  wireFeeAmortization: 1,
+  allowedAuditors: [
+    {
+      auditorBaseUrl: "https://auditor.taler.ar/";,
+      auditorPub: "0000000000000000000000000000000000000000000000000000",
+    },
+  ],
+  allowedExchanges: [
+    {
+      exchangeBaseUrl: "https://exchange.taler.ar/";,
+      exchangePub: "1C2EYE90PYDNVRTQ25A3PA0KW5W4WPAJNNQHVHV49PT6W5CERFV0",
+    },
+  ],
+  timestamp: {
+    t_s: 1659972710,
+  },
+  wireMethod: "x-taler-bank",
+  wireInfoHash:
+    
"QDT28374ZHYJ59WQFZ3TW1D5WKJVDYHQT86VHED3TNMB15ANJSKXDYPPNX01348KDYCX6T4WXA5A8FJJ8YWNEB1JW726C1JPKHM89DR",
+  maxDepositFee: {
+    currency: "ARS",
+    fraction: 0,
+    value: 1,
+  },
+  merchant: {
+    name: "Default",
+    address: {
+      country: "ar",
+    },
+    jurisdiction: {
+      country: "ar",
+    },
+  },
+  products: [],
+  autoRefund: undefined,
+  summaryI18n: undefined,
+  deliveryDate: undefined,
+  deliveryLocation: undefined,
+};
+
+export const ShowingSimpleOrder = createExample(ShowView, {
+  contractTerms: cd,
+});
+export const Error = createExample(ErrorView, {
+  proposalId: "asd",
+  error: {
+    hasError: true,
+    message: "message",
+    operational: false,
+    // details: {
+    //   code: 123,
+    // },
+  },
+});
+export const Loading = createExample(LoadingView, {});
+export const Hidden = createExample(HiddenView, {});
diff --git 
a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
 
b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
new file mode 100644
index 00000000..b7d8376b
--- /dev/null
+++ 
b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
@@ -0,0 +1,385 @@
+/*
+ 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 { AbsoluteTime, Duration, Location } from "@gnu-taler/taler-util";
+import { WalletContractData } from "@gnu-taler/taler-wallet-core";
+import { styled } from "@linaria/react";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../components/Loading.js";
+import { LoadingError } from "../components/LoadingError.js";
+import { Modal } from "../components/Modal.js";
+import { Time } from "../components/Time.js";
+import { useTranslationContext } from "../context/translation.js";
+import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { compose, StateViewMap } from "../utils/index.js";
+import * as wxApi from "../wxApi.js";
+import { Amount } from "./Amount.js";
+import { Link, LinkPrimary } from "./styled/index.js";
+
+const ContractTermsTable = styled.table`
+  width: 100%;
+  border-spacing: 0px;
+  & > tr > td {
+    padding: 5px;
+  }
+  & > tr > td:nth-child(2n) {
+    text-align: right;
+  }
+  & > tr:nth-child(2n) {
+    background: #ebebeb;
+  }
+`;
+
+function locationAsText(l: Location | undefined): VNode {
+  if (!l) return <span />;
+  const lines = [
+    ...(l.address_lines || []).map((e) => [e]),
+    [l.town_location, l.town, l.street],
+    [l.building_name, l.building_number],
+    [l.country, l.country_subdivision],
+    [l.district, l.post_code],
+  ];
+  //remove all missing value
+  //then remove all empty lines
+  const curated = lines
+    .map((l) => l.filter((v) => !!v))
+    .filter((l) => l.length > 0);
+  return (
+    <span>
+      {curated.map((c, i) => (
+        <div key={i}>{c.join(",")}</div>
+      ))}
+    </span>
+  );
+}
+
+type State = States.Loading | States.Error | States.Hidden | States.Show;
+
+namespace States {
+  export interface Loading {
+    status: "loading";
+    hideHandler: ButtonHandler;
+  }
+  export interface Error {
+    status: "error";
+    proposalId: string;
+    error: HookError;
+    hideHandler: ButtonHandler;
+  }
+  export interface Hidden {
+    status: "hidden";
+    showHandler: ButtonHandler;
+  }
+  export interface Show {
+    status: "show";
+    hideHandler: ButtonHandler;
+    contractTerms: WalletContractData;
+  }
+}
+
+interface Props {
+  proposalId: string;
+}
+
+function useComponentState({ proposalId }: Props, api: typeof wxApi): State {
+  const [show, setShow] = useState(false);
+  const hook = useAsyncAsHook(async () => {
+    if (!show) return undefined;
+    return await api.getContractTermsDetails(proposalId);
+  }, [show]);
+
+  const hideHandler = {
+    onClick: async () => setShow(false),
+  };
+  const showHandler = {
+    onClick: async () => setShow(true),
+  };
+  if (!show) {
+    return {
+      status: "hidden",
+      showHandler,
+    };
+  }
+  if (!hook) return { status: "loading", hideHandler };
+  if (hook.hasError)
+    return { status: "error", proposalId, error: hook, hideHandler };
+  if (!hook.response) return { status: "loading", hideHandler };
+  return {
+    status: "show",
+    contractTerms: hook.response,
+    hideHandler,
+  };
+}
+
+const viewMapping: StateViewMap<State> = {
+  loading: LoadingView,
+  error: ErrorView,
+  show: ShowView,
+  hidden: HiddenView,
+};
+
+export const ShowFullContractTermPopup = compose(
+  "ShowFullContractTermPopup",
+  (p: Props) => useComponentState(p, wxApi),
+  viewMapping,
+);
+
+export function LoadingView({ hideHandler }: States.Loading): VNode {
+  return (
+    <Modal title="Full detail" onClose={hideHandler}>
+      <Loading />
+    </Modal>
+  );
+}
+
+export function ErrorView({
+  hideHandler,
+  error,
+  proposalId,
+}: States.Error): VNode {
+  const { i18n } = useTranslationContext();
+  return (
+    <Modal title="Full detail" onClose={hideHandler}>
+      <LoadingError
+        title={
+          <i18n.Translate>
+            Could not load purchase proposal details
+          </i18n.Translate>
+        }
+        error={error}
+      />
+    </Modal>
+  );
+}
+
+export function HiddenView({ showHandler }: States.Hidden): VNode {
+  return <Link onClick={showHandler?.onClick}>Show full details</Link>;
+}
+
+export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
+  const createdAt = AbsoluteTime.fromTimestamp(contractTerms.timestamp);
+
+  return (
+    <Modal title="Full detail" onClose={hideHandler}>
+      <div style={{ overflowY: "auto", height: "95%", padding: 5 }}>
+        <ContractTermsTable>
+          <tr>
+            <td>Order Id</td>
+            <td>{contractTerms.orderId}</td>
+          </tr>
+          <tr>
+            <td>Summary</td>
+            <td>{contractTerms.summary}</td>
+          </tr>
+          <tr>
+            <td>Amount</td>
+            <td>
+              <Amount value={contractTerms.amount} />
+            </td>
+          </tr>
+          <tr>
+            <td>Merchant name</td>
+            <td>{contractTerms.merchant.name}</td>
+          </tr>
+          <tr>
+            <td>Merchant jurisdiction</td>
+            <td>{locationAsText(contractTerms.merchant.jurisdiction)}</td>
+          </tr>
+          <tr>
+            <td>Merchant address</td>
+            <td>{locationAsText(contractTerms.merchant.address)}</td>
+          </tr>
+          <tr>
+            <td>Merchant logo</td>
+            <td>
+              <div>
+                <img
+                  src={contractTerms.merchant.logo}
+                  style={{ width: 64, height: 64, margin: 4 }}
+                />
+              </div>
+            </td>
+          </tr>
+          <tr>
+            <td>Merchant website</td>
+            <td>{contractTerms.merchant.website}</td>
+          </tr>
+          <tr>
+            <td>Merchant email</td>
+            <td>{contractTerms.merchant.email}</td>
+          </tr>
+          <tr>
+            <td>Merchant public key</td>
+            <td>
+              <span title={contractTerms.merchantPub}>
+                {contractTerms.merchantPub.substring(0, 6)}...
+              </span>
+            </td>
+          </tr>
+          <tr>
+            <td>Delivery date</td>
+            <td>
+              {contractTerms.deliveryDate && (
+                <Time
+                  timestamp={AbsoluteTime.fromTimestamp(
+                    contractTerms.deliveryDate,
+                  )}
+                  format="dd MMMM yyyy, HH:mm"
+                />
+              )}
+            </td>
+          </tr>
+          <tr>
+            <td>Delivery location</td>
+            <td>{locationAsText(contractTerms.deliveryLocation)}</td>
+          </tr>
+          <tr>
+            <td>Products</td>
+            <td>
+              {!contractTerms.products || contractTerms.products.length === 0
+                ? "none"
+                : contractTerms.products
+                    .map((p) => `${p.description} x ${p.quantity}`)
+                    .join(", ")}
+            </td>
+          </tr>
+          <tr>
+            <td>Created at</td>
+            <td>
+              {contractTerms.timestamp && (
+                <Time
+                  timestamp={AbsoluteTime.fromTimestamp(
+                    contractTerms.timestamp,
+                  )}
+                  format="dd MMMM yyyy, HH:mm"
+                />
+              )}
+            </td>
+          </tr>
+          <tr>
+            <td>Refund deadline</td>
+            <td>
+              {
+                <Time
+                  timestamp={AbsoluteTime.fromTimestamp(
+                    contractTerms.refundDeadline,
+                  )}
+                  format="dd MMMM yyyy, HH:mm"
+                />
+              }
+            </td>
+          </tr>
+          <tr>
+            <td>Auto refund</td>
+            <td>
+              {
+                <Time
+                  timestamp={AbsoluteTime.addDuration(
+                    createdAt,
+                    !contractTerms.autoRefund
+                      ? Duration.getZero()
+                      : Duration.fromTalerProtocolDuration(
+                          contractTerms.autoRefund,
+                        ),
+                  )}
+                  format="dd MMMM yyyy, HH:mm"
+                />
+              }
+            </td>
+          </tr>
+          <tr>
+            <td>Pay deadline</td>
+            <td>
+              {
+                <Time
+                  timestamp={AbsoluteTime.fromTimestamp(
+                    contractTerms.payDeadline,
+                  )}
+                  format="dd MMMM yyyy, HH:mm"
+                />
+              }
+            </td>
+          </tr>
+          <tr>
+            <td>Fulfillment URL</td>
+            <td>{contractTerms.fulfillmentUrl}</td>
+          </tr>
+          <tr>
+            <td>Fulfillment message</td>
+            <td>{contractTerms.fulfillmentMessage}</td>
+          </tr>
+          {/* <tr>
+          <td>Public reorder URL</td>
+          <td>{contractTerms.public_reorder_url}</td>
+        </tr> */}
+          <tr>
+            <td>Max deposit fee</td>
+            <td>
+              <Amount value={contractTerms.maxDepositFee} />
+            </td>
+          </tr>
+          <tr>
+            <td>Max fee</td>
+            <td>
+              <Amount value={contractTerms.maxWireFee} />
+            </td>
+          </tr>
+          <tr>
+            <td>Minimum age</td>
+            <td>{contractTerms.minimumAge}</td>
+          </tr>
+          {/* <tr>
+          <td>Extra</td>
+          <td>
+            <pre>{contractTerms.}</pre>
+          </td>
+        </tr> */}
+          <tr>
+            <td>Wire fee amortization</td>
+            <td>{contractTerms.wireFeeAmortization}</td>
+          </tr>
+          <tr>
+            <td>Auditors</td>
+            <td>
+              {(contractTerms.allowedAuditors || []).map((e) => (
+                <Fragment key={e.auditorPub}>
+                  <a href={e.auditorBaseUrl} title={e.auditorPub}>
+                    {e.auditorPub.substring(0, 6)}...
+                  </a>
+                  &nbsp;
+                </Fragment>
+              ))}
+            </td>
+          </tr>
+          <tr>
+            <td>Exchanges</td>
+            <td>
+              {(contractTerms.allowedExchanges || []).map((e) => (
+                <Fragment key={e.exchangePub}>
+                  <a href={e.exchangeBaseUrl} title={e.exchangePub}>
+                    {e.exchangePub.substring(0, 6)}...
+                  </a>
+                  &nbsp;
+                </Fragment>
+              ))}
+            </td>
+          </tr>
+        </ContractTermsTable>
+      </div>
+    </Modal>
+  );
+}
diff --git 
a/packages/taler-wallet-webextension/src/components/index.stories.tsx 
b/packages/taler-wallet-webextension/src/components/index.stories.tsx
index 053b27f7..901347e4 100644
--- a/packages/taler-wallet-webextension/src/components/index.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/index.stories.tsx
@@ -19,8 +19,9 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
- import * as a1 from "./Banner.stories.js";
+import * as a1 from "./Banner.stories.js";
 import * as a2 from "./PendingTransactions.stories.js";
 import * as a3 from "./Amount.stories.js";
+import * as a4 from "./ShowFullContractTermPopup.stories.js";
 
-export default [a1, a2, a3];
+export default [a1, a2, a3, a4];
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx 
b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 928562fb..ff4a5b4d 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -40,8 +40,18 @@ export const WalletAction = styled.div`
   & h1:first-child {
     margin-top: 0;
   }
+  & > * {
+    width: 600px;
+  }
   section {
     margin-bottom: 2em;
+    table td {
+      padding: 5px 5px;
+    }
+    table tr {
+      border-bottom: 1px solid black;
+      border-top: 1px solid black;
+    }
     button {
       margin-right: 8px;
       margin-left: 8px;
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/index.ts 
b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
index 0e67a499..5c0f6f0d 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/index.ts
@@ -14,7 +14,7 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { AmountJson, ConfirmPayResult, PreparePayResult } from 
"@gnu-taler/taler-util";
+import { AmountJson, ConfirmPayResult, PreparePayResult, 
PreparePayResultAlreadyConfirmed, PreparePayResultInsufficientBalance, 
PreparePayResultPaymentPossible } from "@gnu-taler/taler-util";
 import { Loading } from "../../components/Loading.js";
 import { HookError } from "../../hooks/useAsyncAsHook.js";
 import { ButtonHandler } from "../../mui/handlers.js";
@@ -37,6 +37,7 @@ export type State =
   | State.Ready
   | State.NoEnoughBalance
   | State.NoBalanceForCurrency
+  | State.Completed
   | State.Confirmed;
 
 export namespace State {
@@ -52,8 +53,6 @@ export namespace State {
 
   interface BaseInfo {
     amount: AmountJson;
-    totalFees: AmountJson;
-    payStatus: PreparePayResult;
     uri: string;
     error: undefined;
     goToWalletManualWithdraw: (currency?: string) => Promise<void>;
@@ -61,20 +60,30 @@ export namespace State {
   }
   export interface NoBalanceForCurrency extends BaseInfo {
     status: "no-balance-for-currency"
+    payStatus: PreparePayResult;
     balance: undefined;
   }
   export interface NoEnoughBalance extends BaseInfo {
     status: "no-enough-balance"
+    payStatus: PreparePayResult;
     balance: AmountJson;
   }
   export interface Ready extends BaseInfo {
     status: "ready";
+    payStatus: PreparePayResultPaymentPossible;
     payHandler: ButtonHandler;
     balance: AmountJson;
   }
 
   export interface Confirmed extends BaseInfo {
     status: "confirmed";
+    payStatus: PreparePayResultAlreadyConfirmed;
+    balance: AmountJson;
+  }
+
+  export interface Completed extends BaseInfo {
+    status: "completed";
+    payStatus: PreparePayResult;
     payResult: ConfirmPayResult;
     payHandler: ButtonHandler;
     balance: AmountJson;
@@ -87,6 +96,7 @@ const viewMapping: StateViewMap<State> = {
   "no-balance-for-currency": BaseView,
   "no-enough-balance": BaseView,
   confirmed: BaseView,
+  completed: BaseView,
   ready: BaseView,
 };
 
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/state.ts 
b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
index 3c819ec8..f75cef06 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
@@ -78,20 +78,9 @@ export function useComponentState(
     (b) => Amounts.parseOrThrow(b.available).currency === amount.currency,
   );
 
-
-  let totalFees = Amounts.getZero(amount.currency);
-  if (payStatus.status === PreparePayResultType.PaymentPossible) {
-    const amountEffective: AmountJson = Amounts.parseOrThrow(
-      payStatus.amountEffective,
-    );
-    totalFees = Amounts.sub(amountEffective, amount).amount;
-  }
-
   const baseResult = {
     uri: hook.response.uri,
     amount,
-    totalFees,
-    payStatus,
     error: undefined,
     goBack, goToWalletManualWithdraw
   }
@@ -100,12 +89,45 @@ export function useComponentState(
     return {
       status: "no-balance-for-currency",
       balance: undefined,
+      payStatus,
       ...baseResult,
     }
   }
 
   const foundAmount = Amounts.parseOrThrow(foundBalance.available);
 
+  if (payResult) {
+    return {
+      status: "completed",
+      balance: foundAmount,
+      payStatus,
+      payHandler: {
+        error: payErrMsg,
+      },
+      payResult,
+      ...baseResult,
+    };
+  }
+
+  if (payStatus.status === PreparePayResultType.InsufficientBalance) {
+    return {
+      status: 'no-enough-balance',
+      balance: foundAmount,
+      payStatus,
+      ...baseResult,
+    }
+  }
+
+  if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+    return {
+      status: "confirmed",
+      balance: foundAmount,
+      payStatus,
+      ...baseResult,
+    };
+  }
+
+
   async function doPayment(): Promise<void> {
     try {
       if (payStatus.status !== "payment-possible") {
@@ -138,34 +160,19 @@ export function useComponentState(
     }
   }
 
-  if (payStatus.status === PreparePayResultType.InsufficientBalance) {
-    return {
-      status: 'no-enough-balance',
-      balance: foundAmount,
-      ...baseResult,
-    }
-  }
-
   const payHandler: ButtonHandler = {
     onClick: payErrMsg ? undefined : doPayment,
     error: payErrMsg,
   };
 
-  if (!payResult) {
-    return {
-      status: "ready",
-      payHandler,
-      ...baseResult,
-      balance: foundAmount
-    };
-  }
-
+  // (payStatus.status === PreparePayResultType.PaymentPossible)
   return {
-    status: "confirmed",
-    balance: foundAmount,
-    payResult,
-    payHandler: {},
+    status: "ready",
+    payHandler,
+    payStatus,
     ...baseResult,
+    balance: foundAmount
   };
+
 }
 
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
index 603a9cb3..877c1996 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/stories.tsx
@@ -21,11 +21,14 @@
 
 import {
   Amounts,
+  ConfirmPayResultType,
   ContractTerms,
   PreparePayResultType,
 } from "@gnu-taler/taler-util";
+import merchantIcon from "../../../static-dev/merchant-icon.jpeg";
 import { createExample } from "../../test-utils.js";
 import { BaseView } from "./views.js";
+import beer from "../../../static-dev/beer.png";
 
 export default {
   title: "cta/payment",
@@ -34,25 +37,22 @@ export default {
 };
 
 export const NoBalance = createExample(BaseView, {
-  status: "ready",
+  status: "no-balance-for-currency",
   error: undefined,
   amount: Amounts.parseOrThrow("USD:10"),
   balance: undefined,
-  payHandler: {
-    onClick: async () => {
-      null;
-    },
-  },
-  totalFees: Amounts.parseOrThrow("USD:0"),
 
   uri: "",
   payStatus: {
     status: PreparePayResultType.InsufficientBalance,
     noncePriv: "",
-    proposalId: "proposal1234",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
     contractTerms: {
       merchant: {
-        name: "someone",
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
       },
       summary: "some beers",
       amount: "USD:10",
@@ -62,7 +62,7 @@ export const NoBalance = createExample(BaseView, {
 });
 
 export const NoEnoughBalance = createExample(BaseView, {
-  status: "ready",
+  status: "no-enough-balance",
   error: undefined,
   amount: Amounts.parseOrThrow("USD:10"),
   balance: {
@@ -70,21 +70,18 @@ export const NoEnoughBalance = createExample(BaseView, {
     fraction: 40000000,
     value: 9,
   },
-  payHandler: {
-    onClick: async () => {
-      null;
-    },
-  },
-  totalFees: Amounts.parseOrThrow("USD:0"),
 
   uri: "",
   payStatus: {
     status: PreparePayResultType.InsufficientBalance,
     noncePriv: "",
-    proposalId: "proposal1234",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
     contractTerms: {
       merchant: {
-        name: "someone",
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
       },
       summary: "some beers",
       amount: "USD:10",
@@ -94,7 +91,7 @@ export const NoEnoughBalance = createExample(BaseView, {
 });
 
 export const EnoughBalanceButRestricted = createExample(BaseView, {
-  status: "ready",
+  status: "no-enough-balance",
   error: undefined,
   amount: Amounts.parseOrThrow("USD:10"),
   balance: {
@@ -102,21 +99,18 @@ export const EnoughBalanceButRestricted = 
createExample(BaseView, {
     fraction: 40000000,
     value: 19,
   },
-  payHandler: {
-    onClick: async () => {
-      null;
-    },
-  },
-  totalFees: Amounts.parseOrThrow("USD:0"),
 
   uri: "",
   payStatus: {
     status: PreparePayResultType.InsufficientBalance,
     noncePriv: "",
-    proposalId: "proposal1234",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
     contractTerms: {
       merchant: {
-        name: "someone",
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
       },
       summary: "some beers",
       amount: "USD:10",
@@ -139,7 +133,6 @@ export const PaymentPossible = createExample(BaseView, {
       null;
     },
   },
-  totalFees: Amounts.parseOrThrow("USD:0"),
 
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
   payStatus: {
@@ -150,13 +143,19 @@ export const PaymentPossible = createExample(BaseView, {
     contractTerms: {
       nonce: "123213123",
       merchant: {
-        name: "someone",
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
+      },
+      pay_deadline: {
+        t_s: new Date().getTime() / 1000 + 60 * 60 * 3,
       },
       amount: "USD:10",
       summary: "some beers",
     } as Partial<ContractTerms> as any,
     contractTermsHash: "123456",
-    proposalId: "proposal1234",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
   },
 });
 
@@ -174,7 +173,6 @@ export const PaymentPossibleWithFee = 
createExample(BaseView, {
       null;
     },
   },
-  totalFees: Amounts.parseOrThrow("USD:0.20"),
 
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
   payStatus: {
@@ -185,18 +183,19 @@ export const PaymentPossibleWithFee = 
createExample(BaseView, {
     contractTerms: {
       nonce: "123213123",
       merchant: {
-        name: "someone",
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
       },
       amount: "USD:10",
       summary: "some beers",
     } as Partial<ContractTerms> as any,
     contractTermsHash: "123456",
-    proposalId: "proposal1234",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
   },
 });
 
-import beer from "../../../static-dev/beer.png";
-
 export const TicketWithAProductList = createExample(BaseView, {
   status: "ready",
   error: undefined,
@@ -211,7 +210,6 @@ export const TicketWithAProductList = 
createExample(BaseView, {
       null;
     },
   },
-  totalFees: Amounts.parseOrThrow("USD:0.20"),
 
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
   payStatus: {
@@ -222,7 +220,10 @@ export const TicketWithAProductList = 
createExample(BaseView, {
     contractTerms: {
       nonce: "123213123",
       merchant: {
-        name: "someone",
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
       },
       amount: "USD:10",
       summary: "some beers",
@@ -247,11 +248,11 @@ export const TicketWithAProductList = 
createExample(BaseView, {
       ],
     } as Partial<ContractTerms> as any,
     contractTermsHash: "123456",
-    proposalId: "proposal1234",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
   },
 });
 
-export const AlreadyConfirmedByOther = createExample(BaseView, {
+export const TicketWithShipping = createExample(BaseView, {
   status: "ready",
   error: undefined,
   amount: Amounts.parseOrThrow("USD:10"),
@@ -265,7 +266,52 @@ export const AlreadyConfirmedByOther = 
createExample(BaseView, {
       null;
     },
   },
-  totalFees: Amounts.parseOrThrow("USD:0.20"),
+
+  uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
+  payStatus: {
+    status: PreparePayResultType.PaymentPossible,
+    amountEffective: "USD:10.20",
+    amountRaw: "USD:10",
+    noncePriv: "",
+    contractTerms: {
+      nonce: "123213123",
+      merchant: {
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
+      },
+      amount: "USD:10",
+      summary: "banana pi set",
+      products: [
+        {
+          description: "banana pi",
+          price: "USD:2",
+          quantity: 1,
+        },
+      ],
+      delivery_date: {
+        t_s: new Date().getTime() / 1000 + 30 * 24 * 60 * 60,
+      },
+      delivery_location: {
+        town: "Liverpool",
+        street: "Down st 1234",
+      },
+    } as Partial<ContractTerms> as any,
+    contractTermsHash: "123456",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
+  },
+});
+
+export const AlreadyConfirmedByOther = createExample(BaseView, {
+  status: "confirmed",
+  error: undefined,
+  amount: Amounts.parseOrThrow("USD:10"),
+  balance: {
+    currency: "USD",
+    fraction: 40000000,
+    value: 11,
+  },
 
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
   payStatus: {
@@ -274,19 +320,22 @@ export const AlreadyConfirmedByOther = 
createExample(BaseView, {
     amountRaw: "USD:10",
     contractTerms: {
       merchant: {
-        name: "someone",
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
       },
       summary: "some beers",
       amount: "USD:10",
     } as Partial<ContractTerms> as any,
     contractTermsHash: "123456",
-    proposalId: "proposal1234",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
     paid: false,
   },
 });
 
 export const AlreadyPaidWithoutFulfillment = createExample(BaseView, {
-  status: "ready",
+  status: "completed",
   error: undefined,
   amount: Amounts.parseOrThrow("USD:10"),
   balance: {
@@ -294,33 +343,34 @@ export const AlreadyPaidWithoutFulfillment = 
createExample(BaseView, {
     fraction: 40000000,
     value: 11,
   },
-  payHandler: {
-    onClick: async () => {
-      null;
-    },
-  },
-  totalFees: Amounts.parseOrThrow("USD:0.20"),
 
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
+  payResult: {
+    type: ConfirmPayResultType.Done,
+    contractTerms: {} as any,
+  },
   payStatus: {
     status: PreparePayResultType.AlreadyConfirmed,
     amountEffective: "USD:10",
     amountRaw: "USD:10",
     contractTerms: {
       merchant: {
-        name: "someone",
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
       },
       summary: "some beers",
       amount: "USD:10",
     } as Partial<ContractTerms> as any,
     contractTermsHash: "123456",
-    proposalId: "proposal1234",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
     paid: true,
   },
 });
 
 export const AlreadyPaidWithFulfillment = createExample(BaseView, {
-  status: "ready",
+  status: "completed",
   error: undefined,
   amount: Amounts.parseOrThrow("USD:10"),
   balance: {
@@ -328,29 +378,34 @@ export const AlreadyPaidWithFulfillment = 
createExample(BaseView, {
     fraction: 40000000,
     value: 11,
   },
-  payHandler: {
-    onClick: async () => {
-      null;
-    },
-  },
-  totalFees: Amounts.parseOrThrow("USD:0.20"),
 
   uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
+  payResult: {
+    type: ConfirmPayResultType.Done,
+    contractTerms: {
+      fulfillment_message: "thanks for buying!",
+      fulfillment_url: "https://demo.taler.net";,
+    } as Partial<ContractTerms> as any,
+  },
   payStatus: {
     status: PreparePayResultType.AlreadyConfirmed,
     amountEffective: "USD:10",
     amountRaw: "USD:10",
     contractTerms: {
       merchant: {
-        name: "someone",
+        name: "the merchant",
+        logo: merchantIcon,
+        website: "https://www.themerchant.taler";,
+        email: "contact@merchant.taler",
       },
+      fulfillment_url: "https://demo.taler.net";,
       fulfillment_message:
         "congratulations! you are looking at the fulfillment message! ",
       summary: "some beers",
       amount: "USD:10",
     } as Partial<ContractTerms> as any,
     contractTermsHash: "123456",
-    proposalId: "proposal1234",
+    proposalId: "96YY92RQZGF3V7TJSPN4SF9549QX7BRF88Q5PYFCSBNQ0YK4RPK0",
     paid: true,
   },
 });
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/test.ts 
b/packages/taler-wallet-webextension/src/cta/Payment/test.ts
index aea70b7c..afd881a7 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/test.ts
@@ -204,7 +204,7 @@ describe("Payment CTA states", () => {
       if (r.status !== "ready") expect.fail();
       expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
       expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
-      expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:0"));
+      // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:0"));
       expect(r.payHandler.onClick).not.undefined;
     }
 
@@ -246,7 +246,7 @@ describe("Payment CTA states", () => {
       if (r.status !== "ready") expect.fail();
       expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
       expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
-      expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+      // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
       expect(r.payHandler.onClick).not.undefined;
     }
 
@@ -293,7 +293,7 @@ describe("Payment CTA states", () => {
       if (r.status !== "ready") expect.fail();
       expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
       expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
-      expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+      // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
       if (r.payHandler.onClick === undefined) expect.fail();
       r.payHandler.onClick();
     }
@@ -302,13 +302,13 @@ describe("Payment CTA states", () => {
 
     {
       const r = getLastResultOrThrow();
-      if (r.status !== "confirmed") expect.fail();
+      if (r.status !== "completed") expect.fail();
       expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
       expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
-      expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
-      if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail();
-      expect(r.payResult.contractTerms).not.undefined;
-      expect(r.payHandler.onClick).undefined;
+      // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+      // if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail();
+      // expect(r.payResult.contractTerms).not.undefined;
+      // expect(r.payHandler.onClick).undefined;
     }
 
     await assertNoPendingUpdate();
@@ -354,7 +354,7 @@ describe("Payment CTA states", () => {
       if (r.status !== "ready") expect.fail();
       expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
       expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
-      expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+      // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
       if (r.payHandler.onClick === undefined) expect.fail();
       r.payHandler.onClick();
     }
@@ -366,7 +366,7 @@ describe("Payment CTA states", () => {
       if (r.status !== "ready") expect.fail();
       expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
       expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
-      expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+      // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
       expect(r.payHandler.onClick).undefined;
       if (r.payHandler.error === undefined) expect.fail();
       //FIXME: error message here is bad
@@ -425,7 +425,7 @@ describe("Payment CTA states", () => {
       if (r.status !== "ready") expect.fail();
       expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
       expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
-      expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+      // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
       expect(r.payHandler.onClick).not.undefined;
 
       notifyCoinWithdrawn(Amounts.parseOrThrow("USD:5"));
@@ -438,7 +438,7 @@ describe("Payment CTA states", () => {
       if (r.status !== "ready") expect.fail();
       expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
       expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
-      expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
+      // expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
       expect(r.payHandler.onClick).not.undefined;
     }
 
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx 
b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
index a8c9a640..4c2ddc0f 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Payment/views.tsx
@@ -15,6 +15,7 @@
  */
 
 import {
+  AbsoluteTime,
   Amounts,
   ConfirmPayResultType,
   ContractTerms,
@@ -38,8 +39,10 @@ import {
   WalletAction,
   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 { MerchantDetails, PurchaseDetails } from "../../wallet/Transaction.js";
 import { State } from "./index.js";
 
 export function LoadingUriView({ error }: State.LoadingUriError): VNode {
@@ -56,6 +59,7 @@ export function LoadingUriView({ error }: 
State.LoadingUriError): VNode {
 type SupportedStates =
   | State.Ready
   | State.Confirmed
+  | State.Completed
   | State.NoBalanceForCurrency
   | State.NoEnoughBalance;
 
@@ -63,6 +67,15 @@ export function BaseView(state: SupportedStates): VNode {
   const { i18n } = useTranslationContext();
   const contractTerms: ContractTerms = state.payStatus.contractTerms;
 
+  const price = {
+    raw: state.amount,
+    effective:
+      "amountEffective" in state.payStatus
+        ? Amounts.parseOrThrow(state.payStatus.amountEffective)
+        : state.amount,
+  };
+  const totalFees = Amounts.sub(price.effective, price.raw).amount;
+
   return (
     <WalletAction>
       <LogoHeader />
@@ -73,9 +86,9 @@ export function BaseView(state: SupportedStates): VNode {
 
       <ShowImportantMessage state={state} />
 
-      <section>
-        {state.payStatus.status !== PreparePayResultType.InsufficientBalance &&
-          Amounts.isNonZero(state.totalFees) && (
+      <section style={{ textAlign: "left" }}>
+        {/* {state.payStatus.status !== 
PreparePayResultType.InsufficientBalance &&
+          Amounts.isNonZero(totalFees) && (
             <Part
               big
               title={<i18n.Translate>Total to pay</i18n.Translate>}
@@ -89,24 +102,43 @@ export function BaseView(state: SupportedStates): VNode {
           text={<Amount value={state.payStatus.amountRaw} />}
           kind="neutral"
         />
-        {Amounts.isNonZero(state.totalFees) && (
+        {Amounts.isNonZero(totalFees) && (
           <Fragment>
             <Part
               big
               title={<i18n.Translate>Fee</i18n.Translate>}
-              text={<Amount value={state.totalFees} />}
+              text={<Amount value={totalFees} />}
               kind="negative"
             />
           </Fragment>
-        )}
+        )} */}
+        <Part
+          title={<i18n.Translate>Purchase</i18n.Translate>}
+          text={contractTerms.summary}
+          kind="neutral"
+        />
         <Part
           title={<i18n.Translate>Merchant</i18n.Translate>}
-          text={contractTerms.merchant.name}
+          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>Purchase</i18n.Translate>}
-          text={contractTerms.summary}
+          title={<i18n.Translate>Details</i18n.Translate>}
+          text={
+            <PurchaseDetails
+              price={price}
+              info={{
+                ...contractTerms,
+                orderId: contractTerms.order_id,
+                contractTermsHash: "",
+                products: contractTerms.products!,
+              }}
+              proposalId={state.payStatus.proposalId}
+            />
+          }
           kind="neutral"
         />
         {contractTerms.order_id && (
@@ -116,8 +148,19 @@ export function BaseView(state: SupportedStates): VNode {
             kind="neutral"
           />
         )}
-        {contractTerms.products && contractTerms.products.length > 0 && (
-          <ProductList products={contractTerms.products} />
+        {contractTerms.pay_deadline && (
+          <Part
+            title={<i18n.Translate>Valid until</i18n.Translate>}
+            text={
+              <Time
+                timestamp={AbsoluteTime.fromTimestamp(
+                  contractTerms.pay_deadline,
+                )}
+                format="dd MMMM yyyy, HH:mm"
+              />
+            }
+            kind="neutral"
+          />
         )}
       </section>
       <ButtonsSection
@@ -232,7 +275,7 @@ function ShowImportantMessage({ state }: { state: 
SupportedStates }): VNode {
     );
   }
 
-  if (state.status == "confirmed") {
+  if (state.status == "completed") {
     const { payResult, payHandler } = state;
     if (payHandler.error) {
       return <ErrorTalerOperation error={payHandler.error.errorDetail} />;
@@ -264,7 +307,7 @@ function ShowImportantMessage({ state }: { state: 
SupportedStates }): VNode {
   return <Fragment />;
 }
 
-function PayWithMobile({ state }: { state: State.Ready }): VNode {
+function PayWithMobile({ state }: { state: SupportedStates }): VNode {
   const { i18n } = useTranslationContext();
 
   const [showQR, setShowQR] = useState<boolean>(false);
@@ -286,7 +329,7 @@ function PayWithMobile({ state }: { state: State.Ready }): 
VNode {
         <div>
           <QR text={privateUri} />
           <i18n.Translate>
-            Scan the QR code or
+            Scan the QR code or &nbsp;
             <a href={privateUri}>
               <i18n.Translate>click here</i18n.Translate>
             </a>
@@ -306,61 +349,66 @@ function ButtonsSection({
 }): VNode {
   const { i18n } = useTranslationContext();
   if (state.status === "ready") {
-    const { payStatus } = state;
-    if (payStatus.status === PreparePayResultType.PaymentPossible) {
-      return (
-        <Fragment>
-          <section>
-            <Button
-              variant="contained"
-              color="success"
-              onClick={state.payHandler.onClick}
-            >
-              <i18n.Translate>
-                Pay {<Amount value={payStatus.amountEffective} />}
-              </i18n.Translate>
-            </Button>
-          </section>
-          <PayWithMobile state={state} />
-        </Fragment>
-      );
-    }
-    if (payStatus.status === PreparePayResultType.InsufficientBalance) {
-      let BalanceMessage = "";
-      if (!state.balance) {
-        BalanceMessage = i18n.str`You have no balance for this currency. 
Withdraw digital cash first.`;
+    return (
+      <Fragment>
+        <section>
+          <Button
+            variant="contained"
+            color="success"
+            onClick={state.payHandler.onClick}
+          >
+            <i18n.Translate>
+              Pay &nbsp;
+              {<Amount value={state.payStatus.amountEffective} />}
+            </i18n.Translate>
+          </Button>
+        </section>
+        <PayWithMobile state={state} />
+      </Fragment>
+    );
+  }
+  if (
+    state.status === "no-enough-balance" ||
+    state.status === "no-balance-for-currency"
+  ) {
+    // if (state.payStatus.status === 
PreparePayResultType.InsufficientBalance) {
+    let BalanceMessage = "";
+    if (!state.balance) {
+      BalanceMessage = i18n.str`You have no balance for this currency. 
Withdraw digital cash first.`;
+    } else {
+      const balanceShouldBeEnough =
+        Amounts.cmp(state.balance, state.amount) !== -1;
+      if (balanceShouldBeEnough) {
+        BalanceMessage = i18n.str`Could not find enough coins to pay this 
order. Even if you have enough ${state.balance.currency} some restriction may 
apply.`;
       } else {
-        const balanceShouldBeEnough =
-          Amounts.cmp(state.balance, state.amount) !== -1;
-        if (balanceShouldBeEnough) {
-          BalanceMessage = i18n.str`Could not find enough coins to pay this 
order. Even if you have enough ${state.balance.currency} some restriction may 
apply.`;
-        } else {
-          BalanceMessage = i18n.str`Your current balance is not enough for 
this order.`;
-        }
+        BalanceMessage = i18n.str`Your current balance is not enough for this 
order.`;
       }
-      return (
-        <Fragment>
-          <section>
-            <WarningBox>{BalanceMessage}</WarningBox>
-          </section>
-          <section>
-            <Button
-              variant="contained"
-              color="success"
-              onClick={() => goToWalletManualWithdraw(state.amount.currency)}
-            >
-              <i18n.Translate>Withdraw digital cash</i18n.Translate>
-            </Button>
-          </section>
-          <PayWithMobile state={state} />
-        </Fragment>
-      );
     }
-    if (payStatus.status === PreparePayResultType.AlreadyConfirmed) {
+    return (
+      <Fragment>
+        <section>
+          <WarningBox>{BalanceMessage}</WarningBox>
+        </section>
+        <section>
+          <Button
+            variant="contained"
+            color="success"
+            onClick={() => goToWalletManualWithdraw(state.amount.currency)}
+          >
+            <i18n.Translate>Withdraw digital cash</i18n.Translate>
+          </Button>
+        </section>
+        <PayWithMobile state={state} />
+      </Fragment>
+    );
+    // }
+  }
+  if (state.status === "confirmed") {
+    if (state.payStatus.status === PreparePayResultType.AlreadyConfirmed) {
       return (
         <Fragment>
           <section>
-            {payStatus.paid &&
+            {state.payStatus.paid &&
               state.payStatus.contractTerms.fulfillment_message && (
                 <Part
                   title={<i18n.Translate>Merchant message</i18n.Translate>}
@@ -369,13 +417,13 @@ function ButtonsSection({
                 />
               )}
           </section>
-          {!payStatus.paid && <PayWithMobile state={state} />}
+          {!state.payStatus.paid && <PayWithMobile state={state} />}
         </Fragment>
       );
     }
   }
 
-  if (state.status === "confirmed") {
+  if (state.status === "completed") {
     if (state.payResult.type === ConfirmPayResultType.Pending) {
       return (
         <section>
diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx 
b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
index 89a7cace..87cc98ea 100644
--- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx
@@ -30,6 +30,7 @@ export default {
 
 export const EmptyBalance = createExample(TestedComponent, {
   balances: [],
+  goToWalletManualWithdraw: {},
 });
 
 export const SomeCoins = createExample(TestedComponent, {
@@ -42,6 +43,8 @@ export const SomeCoins = createExample(TestedComponent, {
       requiresUserInput: false,
     },
   ],
+  addAction: {},
+  goToWalletManualWithdraw: {},
 });
 
 export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, {
@@ -68,6 +71,8 @@ export const SomeCoinsInTreeCurrencies = 
createExample(TestedComponent, {
       requiresUserInput: false,
     },
   ],
+  goToWalletManualWithdraw: {},
+  addAction: {},
 });
 
 export const NoCoinsInTreeCurrencies = createExample(TestedComponent, {
@@ -94,6 +99,8 @@ export const NoCoinsInTreeCurrencies = 
createExample(TestedComponent, {
       requiresUserInput: false,
     },
   ],
+  goToWalletManualWithdraw: {},
+  addAction: {},
 });
 
 export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, {
@@ -148,4 +155,6 @@ export const SomeCoinsInFiveCurrencies = 
createExample(TestedComponent, {
       requiresUserInput: false,
     },
   ],
+  goToWalletManualWithdraw: {},
+  addAction: {},
 });
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx 
b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index cdf507cb..3275a0a0 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -23,8 +23,10 @@ import { Loading } from "../components/Loading.js";
 import { LoadingError } from "../components/LoadingError.js";
 import { MultiActionButton } from "../components/MultiActionButton.js";
 import { useTranslationContext } from "../context/translation.js";
-import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
 import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
+import { compose, StateViewMap } from "../utils/index.js";
 import { AddNewActionView } from "../wallet/AddNewActionView.js";
 import * as wxApi from "../wxApi.js";
 import { NoBalanceHelp } from "./NoBalanceHelp.js";
@@ -34,17 +36,46 @@ export interface Props {
   goToWalletHistory: (currency: string) => Promise<void>;
   goToWalletManualWithdraw: () => Promise<void>;
 }
-export function BalancePage({
-  goToWalletManualWithdraw,
-  goToWalletDeposit,
-  goToWalletHistory,
-}: Props): VNode {
-  const { i18n } = useTranslationContext();
+
+export type State = State.Loading | State.Error | State.Action | 
State.Balances;
+
+export namespace State {
+  export interface Loading {
+    status: "loading";
+    error: undefined;
+  }
+
+  export interface Error {
+    status: "error";
+    error: HookError;
+  }
+
+  export interface Action {
+    status: "action";
+    error: undefined;
+    cancel: ButtonHandler;
+  }
+
+  export interface Balances {
+    status: "balance";
+    error: undefined;
+    balances: Balance[];
+    addAction: ButtonHandler;
+    goToWalletDeposit: (currency: string) => Promise<void>;
+    goToWalletHistory: (currency: string) => Promise<void>;
+    goToWalletManualWithdraw: ButtonHandler;
+  }
+}
+
+function useComponentState(
+  { goToWalletDeposit, goToWalletHistory, goToWalletManualWithdraw }: Props,
+  api: typeof wxApi,
+): State {
   const [addingAction, setAddingAction] = useState(false);
-  const state = useAsyncAsHook(wxApi.getBalance);
+  const state = useAsyncAsHook(api.getBalance);
 
   useEffect(() => {
-    return wxApi.onUpdateNotification(
+    return api.onUpdateNotification(
       [NotificationType.WithdrawGroupFinished],
       () => {
         state?.retry();
@@ -52,58 +83,80 @@ export function BalancePage({
     );
   });
 
-  const balances = !state || state.hasError ? [] : state.response.balances;
-
   if (!state) {
-    return <Loading />;
+    return {
+      status: "loading",
+      error: undefined,
+    };
   }
-
   if (state.hasError) {
-    return (
-      <LoadingError
-        title={<i18n.Translate>Could not load balance page</i18n.Translate>}
-        error={state}
-      />
-    );
+    return {
+      status: "error",
+      error: state,
+    };
   }
-
   if (addingAction) {
-    return <AddNewActionView onCancel={async () => setAddingAction(false)} />;
+    return {
+      status: "action",
+      error: undefined,
+      cancel: {
+        onClick: async () => setAddingAction(false),
+      },
+    };
   }
+  return {
+    status: "balance",
+    error: undefined,
+    balances: state.response.balances,
+    addAction: {
+      onClick: async () => setAddingAction(true),
+    },
+    goToWalletManualWithdraw: {
+      onClick: goToWalletManualWithdraw,
+    },
+    goToWalletDeposit,
+    goToWalletHistory,
+  };
+}
+
+const viewMapping: StateViewMap<State> = {
+  loading: Loading,
+  error: ErrorView,
+  action: ActionView,
+  balance: BalanceView,
+};
 
+export const BalancePage = compose(
+  "BalancePage",
+  (p: Props) => useComponentState(p, wxApi),
+  viewMapping,
+);
+
+function ErrorView({ error }: State.Error): VNode {
+  const { i18n } = useTranslationContext();
   return (
-    <BalanceView
-      balances={balances}
-      goToWalletManualWithdraw={goToWalletManualWithdraw}
-      goToWalletDeposit={goToWalletDeposit}
-      goToWalletHistory={goToWalletHistory}
-      goToAddAction={async () => setAddingAction(true)}
+    <LoadingError
+      title={<i18n.Translate>Could not load balance page</i18n.Translate>}
+      error={error}
     />
   );
 }
-export interface BalanceViewProps {
-  balances: Balance[];
-  goToWalletManualWithdraw: () => Promise<void>;
-  goToAddAction: () => Promise<void>;
-  goToWalletDeposit: (currency: string) => Promise<void>;
-  goToWalletHistory: (currency: string) => Promise<void>;
+
+function ActionView({ cancel }: State.Action): VNode {
+  return <AddNewActionView onCancel={cancel.onClick!} />;
 }
 
-export function BalanceView({
-  balances,
-  goToWalletManualWithdraw,
-  goToWalletDeposit,
-  goToWalletHistory,
-  goToAddAction,
-}: BalanceViewProps): VNode {
+export function BalanceView(state: State.Balances): VNode {
   const { i18n } = useTranslationContext();
-  const currencyWithNonZeroAmount = balances
+  const currencyWithNonZeroAmount = state.balances
     .filter((b) => !Amounts.isZero(b.available))
     .map((b) => b.available.split(":")[0]);
 
-  if (balances.length === 0) {
+  if (state.balances.length === 0) {
     return (
-      <NoBalanceHelp goToWalletManualWithdraw={goToWalletManualWithdraw} />
+      <NoBalanceHelp
+        goToWalletManualWithdraw={state.goToWalletManualWithdraw}
+      />
     );
   }
 
@@ -111,23 +164,26 @@ export function BalanceView({
     <Fragment>
       <section>
         <BalanceTable
-          balances={balances}
-          goToWalletHistory={goToWalletHistory}
+          balances={state.balances}
+          goToWalletHistory={state.goToWalletHistory}
         />
       </section>
       <footer style={{ justifyContent: "space-between" }}>
-        <Button variant="contained" onClick={goToWalletManualWithdraw}>
+        <Button
+          variant="contained"
+          onClick={state.goToWalletManualWithdraw.onClick}
+        >
           <i18n.Translate>Withdraw</i18n.Translate>
         </Button>
         {currencyWithNonZeroAmount.length > 0 && (
           <MultiActionButton
             label={(s) => <i18n.Translate>Deposit {s}</i18n.Translate>}
             actions={currencyWithNonZeroAmount}
-            onClick={(c) => goToWalletDeposit(c)}
+            onClick={(c) => state.goToWalletDeposit(c)}
           />
         )}
         <JustInDevMode>
-          <Button onClick={goToAddAction}>
+          <Button onClick={state.addAction.onClick}>
             <i18n.Translate>Enter URI</i18n.Translate>
           </Button>
         </JustInDevMode>
diff --git a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx 
b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
index 2fe1f4ff..d9b96074 100644
--- a/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
+++ b/packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx
@@ -17,13 +17,14 @@ import { css } from "@linaria/core";
 import { Fragment, h, VNode } from "preact";
 import { Alert } from "../mui/Alert.js";
 import { Button } from "../mui/Button.js";
+import { ButtonHandler } from "../mui/handlers.js";
 import { Paper } from "../mui/Paper.js";
 import { Typography } from "../mui/Typography.js";
 
 export function NoBalanceHelp({
   goToWalletManualWithdraw,
 }: {
-  goToWalletManualWithdraw: () => Promise<void>;
+  goToWalletManualWithdraw: ButtonHandler;
 }): VNode {
   return (
     <Paper
@@ -37,7 +38,7 @@ export function NoBalanceHelp({
           fullWidth
           color="warning"
           variant="outlined"
-          onClick={goToWalletManualWithdraw}
+          onClick={goToWalletManualWithdraw.onClick}
         >
           <Typography>Withdraw</Typography>
         </Button>
diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx 
b/packages/taler-wallet-webextension/src/wallet/History.tsx
index c192b2ba..e40c1ac5 100644
--- a/packages/taler-wallet-webextension/src/wallet/History.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/History.tsx
@@ -143,7 +143,11 @@ export function HistoryView({
 
   if (balances.length === 0 || !selectedCurrency) {
     return (
-      <NoBalanceHelp goToWalletManualWithdraw={goToWalletManualWithdraw} />
+      <NoBalanceHelp
+        goToWalletManualWithdraw={{
+          onClick: goToWalletManualWithdraw,
+        }}
+      />
     );
   }
   return (
diff --git 
a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index ae43a7b0..ba61e35f 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -114,6 +114,12 @@ const exampleData = {
   tip: {
     ...commonTransaction,
     type: TransactionType.Tip,
+    // merchant: {
+    //   name: "the merchant",
+    //   logo: merchantIcon,
+    //   website: "https://www.themerchant.taler";,
+    //   email: "contact@merchant.taler",
+    // },
     merchantBaseUrl: "http://merchant.taler";,
   } as TransactionTip,
   refund: {
@@ -429,7 +435,7 @@ export const DepositBitcoin = 
createExample(TestedComponent, {
   transaction: {
     ...exampleData.deposit,
     amountRaw: "BITCOINBTC:0.0000011",
-    amountEffective: "BITCOINBTC:0.00000092",  
+    amountEffective: "BITCOINBTC:0.00000092",
     targetPaytoUri:
       
"payto://bitcoin/bcrt1q6ps8qs6v8tkqrnru4xqqqa6rfwcx5ufpdfqht4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
   },
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx 
b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index c42bf706..e643fef1 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -16,18 +16,18 @@
 
 import {
   AbsoluteTime,
-  amountFractionalLength,
   AmountJson,
   Amounts,
   Location,
+  MerchantInfo,
   NotificationType,
+  OrderShortInfo,
   parsePaytoUri,
   PaytoUri,
   stringifyPaytoUri,
   TalerProtocolTimestamp,
   Transaction,
   TransactionDeposit,
-  TransactionPayment,
   TransactionRefresh,
   TransactionRefund,
   TransactionTip,
@@ -46,6 +46,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 { ShowFullContractTermPopup } from 
"../components/ShowFullContractTermPopup.js";
 import {
   CenteredDialog,
   InfoBox,
@@ -319,10 +320,15 @@ export function TransactionView({
         ? undefined
         : Amounts.parseOrThrow(transaction.refundPending);
 
-    const total = Amounts.sub(
-      Amounts.parseOrThrow(transaction.amountEffective),
-      Amounts.parseOrThrow(transaction.totalRefundEffective),
-    ).amount;
+    const price = {
+      raw: Amounts.parseOrThrow(transaction.amountRaw),
+      effective: Amounts.parseOrThrow(transaction.amountEffective),
+    };
+    const refund = {
+      raw: Amounts.parseOrThrow(transaction.totalRefundRaw),
+      effective: Amounts.parseOrThrow(transaction.totalRefundEffective),
+    };
+    const total = Amounts.sub(price.effective, refund.effective).amount;
 
     return (
       <TransactionTemplate>
@@ -404,45 +410,7 @@ export function TransactionView({
         )}
         <Part
           title={<i18n.Translate>Merchant</i18n.Translate>}
-          text={
-            <Fragment>
-              <div style={{ display: "flex", flexDirection: "row" }}>
-                {transaction.info.merchant.logo && (
-                  <div>
-                    <img
-                      src={transaction.info.merchant.logo}
-                      style={{ width: 64, height: 64, margin: 4 }}
-                    />
-                  </div>
-                )}
-                <div>
-                  <p>{transaction.info.merchant.name}</p>
-                  {transaction.info.merchant.website && (
-                    <a
-                      href={transaction.info.merchant.website}
-                      target="_blank"
-                      style={{ textDecorationColor: "gray" }}
-                      rel="noreferrer"
-                    >
-                      <SmallLightText>
-                        {transaction.info.merchant.website}
-                      </SmallLightText>
-                    </a>
-                  )}
-                  {transaction.info.merchant.email && (
-                    <a
-                      href={`mailto:${transaction.info.merchant.email}`}
-                      style={{ textDecorationColor: "gray" }}
-                    >
-                      <SmallLightText>
-                        {transaction.info.merchant.email}
-                      </SmallLightText>
-                    </a>
-                  )}
-                </div>
-              </div>
-            </Fragment>
-          }
+          text={<MerchantDetails merchant={transaction.info.merchant} />}
           kind="neutral"
         />
         <Part
@@ -452,7 +420,14 @@ export function TransactionView({
         />
         <Part
           title={<i18n.Translate>Details</i18n.Translate>}
-          text={<PurchaseDetails transaction={transaction} />}
+          text={
+            <PurchaseDetails
+              price={price}
+              refund={refund}
+              info={transaction.info}
+              proposalId={transaction.proposalId}
+            />
+          }
           kind="neutral"
         />
       </TransactionTemplate>
@@ -521,12 +496,7 @@ export function TransactionView({
         </Header>
         {/* <Part
           title={<i18n.Translate>Merchant</i18n.Translate>}
-          text={transaction.info.merchant.name}
-          kind="neutral"
-        />
-        <Part
-          title={<i18n.Translate>Invoice ID</i18n.Translate>}
-          text={transaction.info.orderId}
+          text={<MerchantDetails merchant={transaction.merchant} />}
           kind="neutral"
         /> */}
         <Part
@@ -584,6 +554,46 @@ export function TransactionView({
   return <div />;
 }
 
+export function MerchantDetails({
+  merchant,
+}: {
+  merchant: MerchantInfo;
+}): VNode {
+  return (
+    <div style={{ display: "flex", flexDirection: "row" }}>
+      {merchant.logo && (
+        <div>
+          <img
+            src={merchant.logo}
+            style={{ width: 64, height: 64, margin: 4 }}
+          />
+        </div>
+      )}
+      <div>
+        <p style={{ marginTop: 0 }}>{merchant.name}</p>
+        {merchant.website && (
+          <a
+            href={merchant.website}
+            target="_blank"
+            style={{ textDecorationColor: "gray" }}
+            rel="noreferrer"
+          >
+            <SmallLightText>{merchant.website}</SmallLightText>
+          </a>
+        )}
+        {merchant.email && (
+          <a
+            href={`mailto:${merchant.email}`}
+            style={{ textDecorationColor: "gray" }}
+          >
+            <SmallLightText>{merchant.email}</SmallLightText>
+          </a>
+        )}
+      </div>
+    </div>
+  );
+}
+
 function DeliveryDetails({
   date,
   location,
@@ -703,57 +713,58 @@ function DeliveryDetails({
   );
 }
 
-function PurchaseDetails({
-  transaction,
+export interface AmountWithFee {
+  effective: AmountJson;
+  raw: AmountJson;
+}
+export function PurchaseDetails({
+  price,
+  refund,
+  info,
+  proposalId,
 }: {
-  transaction: TransactionPayment;
+  price: AmountWithFee;
+  refund?: AmountWithFee;
+  info: OrderShortInfo;
+  proposalId: string;
 }): VNode {
   const { i18n } = useTranslationContext();
 
-  const partialFee = Amounts.sub(
-    Amounts.parseOrThrow(transaction.amountEffective),
-    Amounts.parseOrThrow(transaction.amountRaw),
-  ).amount;
+  const partialFee = Amounts.sub(price.effective, price.raw).amount;
 
-  const refundRaw = Amounts.parseOrThrow(transaction.totalRefundRaw);
-
-  const refundFee = Amounts.sub(
-    refundRaw,
-    Amounts.parseOrThrow(transaction.totalRefundEffective),
-  ).amount;
+  const refundFee = !refund
+    ? Amounts.getZero(price.effective.currency)
+    : Amounts.sub(refund.raw, refund.effective).amount;
 
   const fee = Amounts.sum([partialFee, refundFee]).amount;
 
-  const hasProducts =
-    transaction.info.products && transaction.info.products.length > 0;
+  const hasProducts = info.products && info.products.length > 0;
 
   const hasShipping =
-    transaction.info.delivery_date !== undefined ||
-    transaction.info.delivery_location !== undefined;
+    info.delivery_date !== undefined || info.delivery_location !== undefined;
 
   const showLargePic = (): void => {
     return;
   };
 
-  const total = Amounts.sub(
-    Amounts.parseOrThrow(transaction.amountEffective),
-    Amounts.parseOrThrow(transaction.totalRefundEffective),
-  ).amount;
+  const total = !refund
+    ? price.effective
+    : Amounts.sub(price.effective, refund.effective).amount;
 
   return (
     <PurchaseDetailsTable>
       <tr>
         <td>Price</td>
         <td>
-          <Amount value={transaction.amountRaw} />
+          <Amount value={price.raw} />
         </td>
       </tr>
 
-      {Amounts.isNonZero(refundRaw) && (
+      {refund && Amounts.isNonZero(refund.raw) && (
         <tr>
           <td>Refunded</td>
           <td>
-            <Amount value={transaction.totalRefundRaw} negative />
+            <Amount value={refund.raw} negative />
           </td>
         </tr>
       )}
@@ -784,7 +795,7 @@ function PurchaseDetails({
               title={<i18n.Translate>Products</i18n.Translate>}
               text={
                 <ListOfProducts>
-                  {transaction.info.products?.map((p, k) => (
+                  {info.products?.map((p, k) => (
                     <Row key={k}>
                       <a href="#" onClick={showLargePic}>
                         <img src={p.image ? p.image : emptyImg} />
@@ -813,14 +824,19 @@ function PurchaseDetails({
               title={<i18n.Translate>Delivery</i18n.Translate>}
               text={
                 <DeliveryDetails
-                  date={transaction.info.delivery_date}
-                  location={transaction.info.delivery_location}
+                  date={info.delivery_date}
+                  location={info.delivery_location}
                 />
               }
             />
           </td>
         </tr>
       )}
+      <tr>
+        <td>
+          <ShowFullContractTermPopup proposalId={proposalId} />
+        </td>
+      </tr>
     </PurchaseDetailsTable>
   );
 }
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts 
b/packages/taler-wallet-webextension/src/wxApi.ts
index f9506695..9700c475 100644
--- a/packages/taler-wallet-webextension/src/wxApi.ts
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -63,6 +63,7 @@ import {
   PendingOperationsResponse,
   RemoveBackupProviderRequest,
   TalerError,
+  WalletContractData,
 } from "@gnu-taler/taler-wallet-core";
 import type { DepositGroupFees } from 
"@gnu-taler/taler-wallet-core/src/operations/deposits";
 import type { ExchangeWithdrawDetails } from 
"@gnu-taler/taler-wallet-core/src/operations/withdraw";
@@ -190,6 +191,11 @@ export function getBalance(): Promise<BalancesResponse> {
   return callBackend("getBalances", {});
 }
 
+
+export function getContractTermsDetails(proposalId: string): 
Promise<WalletContractData> {
+  return callBackend("getContractTermsDetails", { proposalId });
+}
+
 /**
  * Retrieve the full event history for this wallet.
  */

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