aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-08-08 14:09:28 -0300
committerSebastian <sebasjm@gmail.com>2022-08-08 14:09:36 -0300
commit7a600514c6d43bbaeba6b962533415e59fc46057 (patch)
treed96c02537cda29f1637787a8fb8e659a37ea8c1f /packages/taler-wallet-webextension
parent4409d8384b77401489c2a92d3de20f79959ae34a (diff)
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)
Diffstat (limited to 'packages/taler-wallet-webextension')
-rw-r--r--packages/taler-wallet-webextension/src/components/Modal.tsx91
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx116
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx385
-rw-r--r--packages/taler-wallet-webextension/src/components/index.stories.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx10
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/index.ts16
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/state.ts71
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/stories.tsx175
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/test.ts24
-rw-r--r--packages/taler-wallet-webextension/src/cta/Payment/views.tsx178
-rw-r--r--packages/taler-wallet-webextension/src/popup/Balance.stories.tsx9
-rw-r--r--packages/taler-wallet-webextension/src/popup/BalancePage.tsx152
-rw-r--r--packages/taler-wallet-webextension/src/popup/NoBalanceHelp.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/wallet/History.tsx6
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx174
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts6
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 000000000..3fea063d3
--- /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 000000000..6f71b9d2e
--- /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 000000000..b7d8376bd
--- /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 053b27f79..901347e4f 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 928562fb6..ff4a5b4d5 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 0e67a4991..5c0f6f0d6 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 3c819ec8f..f75cef06f 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 603a9cb33..877c1996a 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 aea70b7ca..afd881a72 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 a8c9a640a..4c2ddc0f2 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 89a7cace8..87cc98ea0 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 cdf507cb5..3275a0a07 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 2fe1f4ff7..d9b960748 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 c192b2ba7..e40c1ac5b 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 ae43a7b09..ba61e35f3 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 c42bf7066..e643fef18 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 f95066954..9700c475e 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.
*/