summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension')
-rwxr-xr-xpackages/taler-wallet-webextension/build-fast-with-linaria.mjs1
-rw-r--r--packages/taler-wallet-webextension/src/components/Amount.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/components/BalanceTable.tsx2
-rw-r--r--packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx75
-rw-r--r--packages/taler-wallet-webextension/src/components/Part.tsx99
-rw-r--r--packages/taler-wallet-webextension/src/components/styled/index.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/custom.d.ts6
-rw-r--r--packages/taler-wallet-webextension/src/stories.tsx8
-rw-r--r--packages/taler-wallet-webextension/src/test-utils.ts13
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx161
-rw-r--r--packages/taler-wallet-webextension/src/wallet/Transaction.tsx1044
-rw-r--r--packages/taler-wallet-webextension/static-dev/merchant-icon-11.jpegbin0 -> 60184 bytes
12 files changed, 960 insertions, 459 deletions
diff --git a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs b/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
index f6de67885..41747a745 100755
--- a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
+++ b/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
@@ -54,6 +54,7 @@ export const buildConfig = {
loader: {
'.svg': 'text',
'.png': 'dataurl',
+ '.jpeg': 'dataurl',
},
target: [
'es6'
diff --git a/packages/taler-wallet-webextension/src/components/Amount.tsx b/packages/taler-wallet-webextension/src/components/Amount.tsx
index c41f7faf6..b415a30cd 100644
--- a/packages/taler-wallet-webextension/src/components/Amount.tsx
+++ b/packages/taler-wallet-webextension/src/components/Amount.tsx
@@ -6,7 +6,7 @@ export function Amount({ value }: { value: AmountJson | AmountString }): VNode {
const amount = Amounts.stringifyValue(aj, 2);
return (
<Fragment>
- {amount} {aj.currency}
+ {amount}&nbsp;{aj.currency}
</Fragment>
);
}
diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
index e67fb6b4d..a2c91f4a1 100644
--- a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
+++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
@@ -44,7 +44,7 @@ export function BalanceTable({
width: "100%",
}}
>
- {Amounts.stringifyValue(av)}
+ {Amounts.stringifyValue(av, 2)}
</td>
</tr>
);
diff --git a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
index 185021bc0..3a2a12c72 100644
--- a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
+++ b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
@@ -46,43 +46,47 @@ export function BankDetailsByPaytoType({
if (payto.isKnown && payto.targetType === "bitcoin") {
const min = segwitMinAmount(amount.currency);
return (
- <section style={{ textAlign: "left" }}>
+ <section
+ style={{
+ textAlign: "left",
+ border: "solid 1px black",
+ padding: 8,
+ borderRadius: 4,
+ }}
+ >
+ <p style={{ marginTop: 0 }}>Bitcoin transfer details</p>
<p>
<i18n.Translate>
- Bitcoin exchange need a transaction with 3 output, one output is the
+ The exchange need a transaction with 3 output, one output is the
exchange account and the other two are segwit fake address for
- metadata with an minimum amount. Reserve pub : {subject}
+ metadata with an minimum amount.
</i18n.Translate>
</p>
+ <Row
+ literal
+ name={<i18n.Translate>Reserve</i18n.Translate>}
+ value={subject}
+ />
+
<p>
<i18n.Translate>
In bitcoincore wallet use &apos;Add Recipient&apos; button to add
two additional recipient and copy addresses and amounts
</i18n.Translate>
- <ul>
- <li>
- {payto.targetPath} {Amounts.stringifyValue(amount)} BTC
- </li>
- {payto.segwitAddrs.map((addr, i) => (
- <li key={i}>
- {addr} {Amounts.stringifyValue(min)} BTC
- </li>
- ))}
- </ul>
- <i18n.Translate>
- In Electrum wallet paste the following three lines in &apos;Pay
- to&apos; field :
- </i18n.Translate>
- <ul>
- <li>
- {payto.targetPath},{Amounts.stringifyValue(amount)}
- </li>
- {payto.segwitAddrs.map((addr, i) => (
- <li key={i}>
- {addr} {Amounts.stringifyValue(min)} BTC
- </li>
- ))}
- </ul>
+ </p>
+ <table>
+ <tr>
+ <td>{payto.targetPath}</td>
+ <td>{Amounts.stringifyValue(amount)} BTC</td>
+ </tr>
+ {payto.segwitAddrs.map((addr, i) => (
+ <tr key={i}>
+ <td>{addr}</td>
+ <td>{Amounts.stringifyValue(min)} BTC</td>
+ </tr>
+ ))}
+ </table>
+ <p>
<i18n.Translate>
Make sure the amount show{" "}
{Amounts.stringifyValue(Amounts.sum([amount, min, min]).amount)}{" "}
@@ -93,7 +97,7 @@ export function BankDetailsByPaytoType({
);
}
- const firstPart = !payto.isKnown ? (
+ const accountPart = !payto.isKnown ? (
<Row
name={<i18n.Translate>Account</i18n.Translate>}
value={payto.targetPath}
@@ -113,10 +117,17 @@ export function BankDetailsByPaytoType({
<Row name={<i18n.Translate>IBAN</i18n.Translate>} value={payto.iban} />
) : undefined;
return (
- <div style={{ textAlign: "left" }}>
- <p>Bank transfer details</p>
+ <div
+ style={{
+ textAlign: "left",
+ border: "solid 1px black",
+ padding: 8,
+ borderRadius: 4,
+ }}
+ >
+ <p style={{ marginTop: 0 }}>Bank transfer details</p>
<table>
- {firstPart}
+ {accountPart}
<Row
name={<i18n.Translate>Exchange</i18n.Translate>}
value={exchangeBaseUrl}
@@ -176,7 +187,7 @@ function Row({
</TooltipRight>
)}
</td>
- <td>
+ <td style={{ paddingRight: 8 }}>
<b>{name}</b>
</td>
{literal ? (
diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx
index 21c0f65dc..58165a349 100644
--- a/packages/taler-wallet-webextension/src/components/Part.tsx
+++ b/packages/taler-wallet-webextension/src/components/Part.tsx
@@ -14,33 +14,122 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { PaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
-import { ExtraLargeText, LargeText, SmallLightText } from "./styled/index.js";
+import { useState } from "preact/hooks";
+import {
+ ExtraLargeText,
+ LargeText,
+ SmallBoldText,
+ SmallLightText,
+} from "./styled/index.js";
export type Kind = "positive" | "negative" | "neutral";
interface Props {
- title: VNode;
+ title: VNode | string;
text: VNode | string;
- kind: Kind;
+ kind?: Kind;
big?: boolean;
+ showSign?: boolean;
}
-export function Part({ text, title, kind, big }: Props): VNode {
+export function Part({
+ text,
+ title,
+ kind = "neutral",
+ big,
+ showSign,
+}: Props): VNode {
const Text = big ? ExtraLargeText : LargeText;
return (
<div style={{ margin: "1em" }}>
- <SmallLightText style={{ margin: ".5em" }}>{title}</SmallLightText>
+ <SmallBoldText style={{ marginBottom: "1em" }}>{title}</SmallBoldText>
<Text
style={{
color:
kind == "positive" ? "green" : kind == "negative" ? "red" : "black",
+ fontWeight: "lighten",
}}
>
+ {!showSign || kind === "neutral"
+ ? undefined
+ : kind === "positive"
+ ? "+"
+ : "-"}
{text}
</Text>
</div>
);
}
+const CollasibleBox = styled.div`
+ border: 1px solid black;
+ border-radius: 0.25em;
+ display: flex;
+ vertical-align: middle;
+ justify-content: space-between;
+ flex-direction: column;
+ /* margin: 0.5em; */
+ padding: 0.5em;
+ /* margin: 1em; */
+ /* width: 100%; */
+ /* color: #721c24; */
+ /* background: #f8d7da; */
+
+ & > div {
+ display: flex;
+ justify-content: space-between;
+ div {
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+ & > button {
+ align-self: center;
+ font-size: 100%;
+ padding: 0;
+ height: 28px;
+ width: 28px;
+ }
+ }
+`;
+import arrowDown from "../svg/chevron-down.svg";
+
+export function PartCollapsible({ text, title, big, showSign }: Props): VNode {
+ const Text = big ? ExtraLargeText : LargeText;
+ const [collapsed, setCollapsed] = useState(true);
+
+ return (
+ <CollasibleBox>
+ <div>
+ <SmallBoldText>{title}</SmallBoldText>
+ <button
+ onClick={() => {
+ setCollapsed((v) => !v);
+ }}
+ >
+ <div
+ style={{
+ transform: !collapsed ? "scaleY(-1)" : undefined,
+ height: 24,
+ }}
+ dangerouslySetInnerHTML={{ __html: arrowDown }}
+ />
+ </button>
+ </div>
+ {/* <SmallBoldText
+ style={{
+ paddingBottom: "1em",
+ paddingTop: "1em",
+ paddingLeft: "1em",
+ border: "black solid 1px",
+ }}
+ >
+
+ </SmallBoldText> */}
+ {!collapsed && <div style={{ display: "block" }}>{text}</div>}
+ </CollasibleBox>
+ );
+}
+
interface PropsPayto {
payto: PaytoUri;
kind: Kind;
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index 7517a1388..a531a15dc 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -87,7 +87,7 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>`
justify-content: space-between;
align-items: center;
& > * {
- width: 500px;
+ width: 600px;
}
& > section {
padding: ${({ noPadding }) => (noPadding ? "0px" : "8px")};
@@ -660,6 +660,12 @@ export const WarningText = styled.div`
export const SmallText = styled.div`
font-size: small;
`;
+
+export const SmallBoldText = styled.div`
+ font-size: small;
+ font-weight: bold;
+`;
+
export const LargeText = styled.div`
font-size: large;
`;
diff --git a/packages/taler-wallet-webextension/src/custom.d.ts b/packages/taler-wallet-webextension/src/custom.d.ts
index 521b824c7..711112ad8 100644
--- a/packages/taler-wallet-webextension/src/custom.d.ts
+++ b/packages/taler-wallet-webextension/src/custom.d.ts
@@ -13,7 +13,11 @@
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/>
*/
-declare module "*.jpeg" {
+ declare module "*.jpeg" {
+ const content: any;
+ export default content;
+}
+declare module "*.jpg" {
const content: any;
export default content;
}
diff --git a/packages/taler-wallet-webextension/src/stories.tsx b/packages/taler-wallet-webextension/src/stories.tsx
index 9c0f69ec4..fd5d3c590 100644
--- a/packages/taler-wallet-webextension/src/stories.tsx
+++ b/packages/taler-wallet-webextension/src/stories.tsx
@@ -330,9 +330,11 @@ function Application(): VNode {
const hash = location.hash.substring(1);
const found = document.getElementById(hash);
if (found) {
- found.scrollIntoView({
- block: "center",
- });
+ setTimeout(() => {
+ found.scrollIntoView({
+ block: "center",
+ });
+ }, 10);
}
}
}, []);
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts b/packages/taler-wallet-webextension/src/test-utils.ts
index eceda616f..9e219daa6 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -26,22 +26,27 @@ options.requestAnimationFrame = (fn: () => void) => {
export function createExample<Props>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>,
+ props: Partial<Props> | (() => Partial<Props>),
): ComponentChildren {
+ //FIXME: props are evaluated on build time
+ // in some cases we want to evaluated the props on render time so we can get some relative timestamp
+ // check how we can build evaluatedProps in render time
+ const evaluatedProps = typeof props === "function" ? props() : props
const Render = (args: any): VNode => create(Component, args);
- Render.args = props;
+ Render.args = evaluatedProps;
return Render;
}
export function createExampleWithCustomContext<Props, ContextProps>(
Component: FunctionalComponent<Props>,
- props: Partial<Props>,
+ props: Partial<Props> | (() => Partial<Props>),
ContextProvider: FunctionalComponent<ContextProps>,
contextProps: Partial<ContextProps>,
): ComponentChildren {
+ const evaluatedProps = typeof props === "function" ? props() : props
const Render = (args: any): VNode => create(Component, args);
const WithContext = (args: any): VNode => create(ContextProvider, { ...contextProps, children: [Render(args)] } as any);
- WithContext.args = props
+ WithContext.args = evaluatedProps
return WithContext
}
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
index f162543ae..493cdd1d7 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx
@@ -30,6 +30,7 @@ import {
TransactionTip,
TransactionType,
TransactionWithdrawal,
+ WithdrawalDetails,
WithdrawalType,
} from "@gnu-taler/taler-util";
import { DevContextProviderForTesting } from "../context/devContext.js";
@@ -57,6 +58,8 @@ const commonTransaction = {
transactionId: "12",
} as TransactionCommon;
+import merchantIcon from "../../static-dev/merchant-icon-11.jpeg";
+
const exampleData = {
withdraw: {
...commonTransaction,
@@ -65,27 +68,34 @@ const exampleData = {
withdrawalDetails: {
confirmed: false,
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- exchangePaytoUris: ["payto://x-taler-bank/bank/account"],
+ exchangePaytoUris: ["payto://x-taler-bank/bank.demo.taler.net/Exchange"],
type: WithdrawalType.ManualTransfer,
},
} as TransactionWithdrawal,
payment: {
...commonTransaction,
- amountEffective: "KUDOS:11",
+ amountEffective: "KUDOS:12",
type: TransactionType.Payment,
info: {
contractTermsHash: "ASDZXCASD",
merchant: {
name: "the merchant",
+ logo: merchantIcon,
+ website: "https://www.themerchant.taler",
+ email: "contact@merchant.taler",
},
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
fulfillmentMessage: "",
+ // delivery_date: { t_s: 1 },
+ // delivery_location: {
+ // address_lines: [""],
+ // },
},
refundPending: undefined,
- totalRefundEffective: "USD:0",
- totalRefundRaw: "USD:0",
+ totalRefundEffective: "KUDOS:0",
+ totalRefundRaw: "KUDOS:0",
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
status: PaymentStatus.Accepted,
} as TransactionPayment,
@@ -93,7 +103,7 @@ const exampleData = {
...commonTransaction,
type: TransactionType.Deposit,
depositGroupId: "#groupId",
- targetPaytoUri: "payto://x-taler-bank/bank/account",
+ targetPaytoUri: "payto://x-taler-bank/bank.demo.taler.net/Exchange",
} as TransactionDeposit,
refresh: {
...commonTransaction,
@@ -117,7 +127,7 @@ const exampleData = {
},
orderId: "2021.167-03NPY6MCYMVGT",
products: [],
- summary: "the summary",
+ summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
fulfillmentMessage: "",
},
refundPending: undefined,
@@ -143,20 +153,27 @@ export const Withdraw = createExample(TestedComponent, {
transaction: exampleData.withdraw,
});
-export const WithdrawOneMinuteAgo = createExample(TestedComponent, {
+export const WithdrawFiveMinutesAgo = createExample(TestedComponent, () => ({
transaction: {
...exampleData.withdraw,
- timestamp: TalerProtocolTimestamp.fromSeconds(new Date().getTime() - 60),
+ timestamp: TalerProtocolTimestamp.fromSeconds(
+ new Date().getTime() / 1000 - 60 * 5,
+ ),
},
-});
+}));
-export const WithdrawOneMinuteAgoAndPending = createExample(TestedComponent, {
- transaction: {
- ...exampleData.withdraw,
- timestamp: TalerProtocolTimestamp.fromSeconds(new Date().getTime() - 60),
- pending: true,
- },
-});
+export const WithdrawFiveMinutesAgoAndPending = createExample(
+ TestedComponent,
+ () => ({
+ transaction: {
+ ...exampleData.withdraw,
+ timestamp: TalerProtocolTimestamp.fromSeconds(
+ new Date().getTime() / 1000 - 60 * 5,
+ ),
+ pending: true,
+ },
+ }),
+);
export const WithdrawError = createExample(TestedComponent, {
transaction: {
@@ -177,17 +194,17 @@ export const WithdrawErrorInDevMode = createExampleInCustomContext(
{ value: true },
);
-export const WithdrawPendingManual = createExample(TestedComponent, {
+export const WithdrawPendingManual = createExample(TestedComponent, () => ({
transaction: {
...exampleData.withdraw,
withdrawalDetails: {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris: ["payto://iban/asdasdasd"],
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
- },
+ } as WithdrawalDetails,
pending: true,
},
-});
+}));
export const WithdrawPendingTalerBankUnconfirmed = createExample(
TestedComponent,
@@ -231,10 +248,95 @@ export const PaymentError = createExample(TestedComponent, {
},
});
-export const PaymentWithoutFee = createExample(TestedComponent, {
+export const PaymentWithRefund = createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12",
+ totalRefundEffective: "KUDOS:1",
+ totalRefundRaw: "KUDOS:1",
+ },
+});
+
+export const PaymentWithDeliveryDate = createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12",
+ info: {
+ ...exampleData.payment.info,
+ delivery_date: {
+ t_s: new Date().getTime() / 1000,
+ },
+ },
+ },
+});
+
+export const PaymentWithDeliveryAddr = createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12",
+ info: {
+ ...exampleData.payment.info,
+ delivery_location: {
+ country: "Argentina",
+ street: "Elm Street",
+ district: "CABA",
+ post_code: "1101",
+ },
+ },
+ },
+});
+
+export const PaymentWithDeliveryFull = createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12",
+ info: {
+ ...exampleData.payment.info,
+ delivery_date: {
+ t_s: new Date().getTime() / 1000,
+ },
+ delivery_location: {
+ country: "Argentina",
+ street: "Elm Street",
+ district: "CABA",
+ post_code: "1101",
+ },
+ },
+ },
+});
+
+export const PaymentWithRefundPending = createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12",
+ refundPending: "KUDOS:3",
+ totalRefundEffective: "KUDOS:1",
+ totalRefundRaw: "KUDOS:1",
+ },
+});
+
+export const PaymentWithFeeAndRefund = createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:11",
+ totalRefundEffective: "KUDOS:1",
+ totalRefundRaw: "KUDOS:1",
+ },
+});
+
+export const PaymentWithFeeAndRefundFee = createExample(TestedComponent, {
transaction: {
...exampleData.payment,
amountRaw: "KUDOS:11",
+ totalRefundEffective: "KUDOS:1",
+ totalRefundRaw: "KUDOS:2",
+ },
+});
+
+export const PaymentWithoutFee = createExample(TestedComponent, {
+ transaction: {
+ ...exampleData.payment,
+ amountRaw: "KUDOS:12",
},
});
@@ -249,7 +351,7 @@ export const PaymentWithProducts = createExample(TestedComponent, {
...exampleData.payment,
info: {
...exampleData.payment.info,
- summary: "this order has 5 products",
+ summary: "summary of 5 products",
products: [
{
description: "t-shirt",
@@ -360,20 +462,3 @@ export const RefundError = createExample(TestedComponent, {
export const RefundPending = createExample(TestedComponent, {
transaction: { ...exampleData.refund, pending: true },
});
-
-export const RefundWithProducts = createExample(TestedComponent, {
- transaction: {
- ...exampleData.refund,
- info: {
- ...exampleData.refund.info,
- products: [
- {
- description: "t-shirt",
- },
- {
- description: "beer",
- },
- ],
- },
- } as TransactionRefund,
-});
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 3377f98c7..9ccb353a9 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -16,14 +16,24 @@
import {
AbsoluteTime,
+ AmountJson,
Amounts,
+ Location,
NotificationType,
parsePaytoUri,
parsePayUri,
+ TalerProtocolTimestamp,
Transaction,
+ TransactionDeposit,
+ TransactionPayment,
+ TransactionRefresh,
+ TransactionRefund,
+ TransactionTip,
TransactionType,
+ TransactionWithdrawal,
WithdrawalType,
} from "@gnu-taler/taler-util";
+import { styled } from "@linaria/react";
import { differenceInSeconds } from "date-fns";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
@@ -33,15 +43,17 @@ import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType.js"
import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
import { Loading } from "../components/Loading.js";
import { LoadingError } from "../components/LoadingError.js";
-import { Part, PartPayto } from "../components/Part.js";
+import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js";
import {
Button,
+ ButtonBox,
ButtonDestructive,
ButtonPrimary,
CenteredDialog,
InfoBox,
ListOfProducts,
Overlay,
+ Row,
RowBorderGray,
SmallLightText,
SubTitle,
@@ -119,6 +131,14 @@ export interface WalletTransactionProps {
onBack: () => void;
}
+const PurchaseDetailsTable = styled.table`
+ width: 100%;
+
+ & > tr > td:nth-child(2n) {
+ text-align: right;
+ }
+`;
+
export function TransactionView({
transaction,
onDelete,
@@ -168,9 +188,7 @@ export function TransactionView({
</WarningBox>
)}
</section>
- <section>
- <div style={{ textAlign: "center" }}>{children}</div>
- </section>
+ <section>{children}</section>
<footer>
<div />
<div>
@@ -189,10 +207,8 @@ export function TransactionView({
}
if (transaction.type === TransactionType.Withdrawal) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount;
+ const total = Amounts.parseOrThrow(transaction.amountEffective);
+ const chosen = Amounts.parseOrThrow(transaction.amountRaw);
return (
<TransactionTemplate>
{confirmBeforeForget ? (
@@ -219,205 +235,125 @@ export function TransactionView({
</CenteredDialog>
</Overlay>
) : undefined}
- <SubTitle>
- <i18n.Translate>Withdrawal</i18n.Translate>
- </SubTitle>
- <Time
- timestamp={AbsoluteTime.fromTimestamp(transaction.timestamp)}
- format="dd MMMM yyyy, HH:mm"
- />
- {transaction.pending ? (
- transaction.withdrawalDetails.type ===
- WithdrawalType.ManualTransfer ? (
- <Fragment>
- <BankDetailsByPaytoType
- amount={Amounts.parseOrThrow(transaction.amountRaw)}
- exchangeBaseUrl={transaction.exchangeBaseUrl}
- payto={parsePaytoUri(
- transaction.withdrawalDetails.exchangePaytoUris[0],
- )}
- subject={transaction.withdrawalDetails.reservePub}
- />
- <p>
- <WarningBox>
- <i18n.Translate>
- Make sure to use the correct subject, otherwise the money
- will not arrive in this wallet.
- </i18n.Translate>
- </WarningBox>
- </p>
- <Part
- big
- title={<i18n.Translate>Total withdrawn</i18n.Translate>}
- text={<Amount value={transaction.amountEffective} />}
- kind="positive"
- />
- <Part
- big
- title={<i18n.Translate>Exchange fee</i18n.Translate>}
- text={<Amount value={fee} />}
- kind="negative"
- />
- </Fragment>
- ) : (
- <Fragment>
- {!transaction.withdrawalDetails.confirmed &&
- transaction.withdrawalDetails.bankConfirmationUrl ? (
- <InfoBox>
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Withdrawal`}
+ total={total}
+ kind="positive"
+ >
+ {transaction.exchangeBaseUrl}
+ </Header>
+
+ {!transaction.pending ? undefined : transaction.withdrawalDetails
+ .type === WithdrawalType.ManualTransfer ? (
+ <Fragment>
+ <BankDetailsByPaytoType
+ amount={chosen}
+ exchangeBaseUrl={transaction.exchangeBaseUrl}
+ payto={parsePaytoUri(
+ transaction.withdrawalDetails.exchangePaytoUris[0],
+ )}
+ subject={transaction.withdrawalDetails.reservePub}
+ />
+ <WarningBox>
+ <i18n.Translate>
+ Make sure to use the correct subject, otherwise the money will
+ not arrive in this wallet.
+ </i18n.Translate>
+ </WarningBox>
+ </Fragment>
+ ) : (
+ <Fragment>
+ {!transaction.withdrawalDetails.confirmed &&
+ transaction.withdrawalDetails.bankConfirmationUrl ? (
+ <InfoBox>
+ <div style={{ display: "block" }}>
<i18n.Translate>
- The bank is waiting for confirmation. Go to the
+ The bank did not yet confirmed the wire transfer. Go to the
+ {` `}
<a
href={transaction.withdrawalDetails.bankConfirmationUrl}
target="_blank"
rel="noreferrer"
+ style={{ display: "inline" }}
>
<i18n.Translate>bank site</i18n.Translate>
- </a>
- </i18n.Translate>
- </InfoBox>
- ) : undefined}
- {transaction.withdrawalDetails.confirmed && (
- <InfoBox>
- <i18n.Translate>
- Waiting for the coins to arrive
+ </a>{" "}
+ and check there is no pending step.
</i18n.Translate>
- </InfoBox>
- )}
- <Part
- big
- title={<i18n.Translate>Total withdrawn</i18n.Translate>}
- text={<Amount value={transaction.amountEffective} />}
- kind="positive"
- />
- <Part
- big
- title={<i18n.Translate>Chosen amount</i18n.Translate>}
- text={<Amount value={transaction.amountRaw} />}
- kind="neutral"
- />
- <Part
- big
- title={<i18n.Translate>Exchange fee</i18n.Translate>}
- text={<Amount value={fee} />}
- kind="negative"
- />
- </Fragment>
- )
- ) : (
- <Fragment>
- <Part
- big
- title={<i18n.Translate>Total withdrawn</i18n.Translate>}
- text={<Amount value={transaction.amountEffective} />}
- kind="positive"
- />
- <Part
- big
- title={<i18n.Translate>Chosen amount</i18n.Translate>}
- text={<Amount value={transaction.amountRaw} />}
- kind="neutral"
- />
- <Part
- big
- title={<i18n.Translate>Exchange fee</i18n.Translate>}
- text={<Amount value={fee} />}
- kind="negative"
- />
+ </div>
+ </InfoBox>
+ ) : undefined}
+ {transaction.withdrawalDetails.confirmed && (
+ <InfoBox>
+ <i18n.Translate>
+ Bank has confirmed the wire transfer. Waiting for the exchange
+ to send the coins
+ </i18n.Translate>
+ </InfoBox>
+ )}
</Fragment>
)}
<Part
- title={<i18n.Translate>Exchange</i18n.Translate>}
- text={new URL(transaction.exchangeBaseUrl).hostname}
- kind="neutral"
+ title={<i18n.Translate>Details</i18n.Translate>}
+ text={<WithdrawDetails transaction={transaction} />}
/>
</TransactionTemplate>
);
}
- const showLargePic = (): void => {
- return;
- };
-
if (transaction.type === TransactionType.Payment) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountEffective),
- Amounts.parseOrThrow(transaction.amountRaw),
- ).amount;
-
- const refundFee = Amounts.sub(
- Amounts.parseOrThrow(transaction.totalRefundRaw),
- Amounts.parseOrThrow(transaction.totalRefundEffective),
- ).amount;
- const refunded = Amounts.isNonZero(
- Amounts.parseOrThrow(transaction.totalRefundRaw),
- );
const pendingRefund =
transaction.refundPending === undefined
? undefined
: Amounts.parseOrThrow(transaction.refundPending);
+
+ const total = Amounts.sub(
+ Amounts.parseOrThrow(transaction.amountEffective),
+ Amounts.parseOrThrow(transaction.totalRefundEffective),
+ ).amount;
+
return (
<TransactionTemplate>
- <SubTitle>
- <i18n.Translate>Payment</i18n.Translate>
- </SubTitle>
- <Time
- timestamp={AbsoluteTime.fromTimestamp(transaction.timestamp)}
- format="dd MMMM yyyy, HH:mm"
- />
- <br />
- <Part
- big
- title={<i18n.Translate>Total paid</i18n.Translate>}
- text={<Amount value={transaction.amountEffective} />}
+ <Header
+ timestamp={transaction.timestamp}
+ total={total}
+ type={i18n.str`Payment`}
kind="negative"
- />
- {Amounts.isNonZero(fee) && (
- <Fragment>
- <Part
- big
- title={<i18n.Translate>Purchase amount</i18n.Translate>}
- text={<Amount value={transaction.amountRaw} />}
- kind="neutral"
- />
- <Part
- title={<i18n.Translate>Purchase Fee</i18n.Translate>}
- text={<Amount value={fee} />}
- kind="negative"
- />
- </Fragment>
- )}
- {refunded && (
- <Fragment>
+ >
+ {transaction.info.fulfillmentUrl ? (
+ <a
+ href={transaction.info.fulfillmentUrl}
+ target="_bank"
+ rel="noreferrer"
+ >
+ {transaction.info.summary}
+ </a>
+ ) : (
+ transaction.info.summary
+ )}
+ </Header>
+ <br />
+ {pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
+ <InfoBox>
+ <i18n.Translate>
+ Merchant created a refund for this order but was not automatically
+ picked up.
+ </i18n.Translate>
<Part
- big
- title={<i18n.Translate>Total refunded</i18n.Translate>}
- text={<Amount value={transaction.totalRefundEffective} />}
+ title={<i18n.Translate>Offer</i18n.Translate>}
+ text={<Amount value={pendingRefund} />}
kind="positive"
/>
- {Amounts.isNonZero(refundFee) && (
- <Fragment>
- <Part
- big
- title={<i18n.Translate>Refund amount</i18n.Translate>}
- text={<Amount value={transaction.totalRefundRaw} />}
- kind="neutral"
- />
- <Part
- title={<i18n.Translate>Refund fee</i18n.Translate>}
- text={<Amount value={refundFee} />}
- kind="negative"
- />
- </Fragment>
- )}
- </Fragment>
- )}
- {pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
- <Part
- big
- title={<i18n.Translate>Refund pending</i18n.Translate>}
- text={<Amount value={pendingRefund} />}
- kind="positive"
- />
+ <div>
+ <div />
+ <div>
+ <ButtonPrimary>
+ <i18n.Translate>Accept</i18n.Translate>
+ </ButtonPrimary>
+ </div>
+ </div>
+ </InfoBox>
)}
<Part
title={<i18n.Translate>Merchant</i18n.Translate>}
@@ -425,268 +361,630 @@ export function TransactionView({
kind="neutral"
/>
<Part
- title={<i18n.Translate>Purchase</i18n.Translate>}
- text={
- transaction.info.fulfillmentUrl ? (
- <a
- href={transaction.info.fulfillmentUrl}
- target="_bank"
- rel="noreferrer"
- >
- {transaction.info.summary}
- </a>
- ) : (
- transaction.info.summary
- )
- }
+ title={<i18n.Translate>Invoice ID</i18n.Translate>}
+ text={transaction.info.orderId}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Receipt</i18n.Translate>}
- text={`#${transaction.info.orderId}`}
+ title={<i18n.Translate>Details</i18n.Translate>}
+ text={<PurchaseDetails transaction={transaction} />}
kind="neutral"
/>
-
- <div>
- {transaction.info.products && transaction.info.products.length > 0 && (
- <ListOfProducts>
- {transaction.info.products.map((p, k) => (
- <RowBorderGray key={k}>
- <a href="#" onClick={showLargePic}>
- <img src={p.image ? p.image : emptyImg} />
- </a>
- <div>
- {p.quantity && p.quantity > 0 && (
- <SmallLightText>
- x {p.quantity} {p.unit}
- </SmallLightText>
- )}
- <div>{p.description}</div>
- </div>
- </RowBorderGray>
- ))}
- </ListOfProducts>
- )}
- </div>
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Deposit) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountEffective),
- Amounts.parseOrThrow(transaction.amountRaw),
- ).amount;
+ const total = Amounts.parseOrThrow(transaction.amountRaw);
const payto = parsePaytoUri(transaction.targetPaytoUri);
return (
<TransactionTemplate>
- <SubTitle>
- <i18n.Translate>Deposit</i18n.Translate>
- </SubTitle>
- <Time
- timestamp={AbsoluteTime.fromTimestamp(transaction.timestamp)}
- format="dd MMMM yyyy, HH:mm"
- />
- <br />
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Deposit`}
+ total={total}
+ kind="negative"
+ >
+ {transaction.targetPaytoUri}
+ </Header>
+ {payto && <PartPayto big payto={payto} kind="neutral" />}
<Part
- big
- title={<i18n.Translate>Total send</i18n.Translate>}
- text={<Amount value={transaction.amountEffective} />}
+ title={<i18n.Translate>Details</i18n.Translate>}
+ text={<DepositDetails transaction={transaction} />}
kind="neutral"
/>
- {Amounts.isNonZero(fee) && (
- <Fragment>
- <Part
- big
- title={<i18n.Translate>Deposit amount</i18n.Translate>}
- text={<Amount value={transaction.amountRaw} />}
- kind="positive"
- />
- <Part
- big
- title={<i18n.Translate>Fee</i18n.Translate>}
- text={<Amount value={fee} />}
- kind="negative"
- />
- </Fragment>
- )}
- {payto && <PartPayto big payto={payto} kind="neutral" />}
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refresh) {
- const fee = Amounts.sub(
+ const total = Amounts.sub(
Amounts.parseOrThrow(transaction.amountRaw),
Amounts.parseOrThrow(transaction.amountEffective),
).amount;
+
return (
<TransactionTemplate>
- <SubTitle>
- <i18n.Translate>Refresh</i18n.Translate>
- </SubTitle>
- <Time
- timestamp={AbsoluteTime.fromTimestamp(transaction.timestamp)}
- format="dd MMMM yyyy, HH:mm"
- />
- <br />
- <Part
- big
- title={<i18n.Translate>Total refresh</i18n.Translate>}
- text={<Amount value={transaction.amountEffective} />}
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Refresh`}
+ total={total}
kind="negative"
+ >
+ {transaction.exchangeBaseUrl}
+ </Header>
+ <Part
+ title={<i18n.Translate>Details</i18n.Translate>}
+ text={<RefreshDetails transaction={transaction} />}
/>
- {Amounts.isNonZero(fee) && (
- <Fragment>
- <Part
- big
- title={<i18n.Translate>Refresh amount</i18n.Translate>}
- text={<Amount value={transaction.amountRaw} />}
- kind="neutral"
- />
- <Part
- big
- title={<i18n.Translate>Fee</i18n.Translate>}
- text={<Amount value={fee} />}
- kind="negative"
- />
- </Fragment>
- )}
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Tip) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount;
+ const total = Amounts.parseOrThrow(transaction.amountEffective);
+
return (
<TransactionTemplate>
- <SubTitle>
- <i18n.Translate>Tip</i18n.Translate>
- </SubTitle>
- <Time
- timestamp={AbsoluteTime.fromTimestamp(transaction.timestamp)}
- format="dd MMMM yyyy, HH:mm"
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Tip`}
+ total={total}
+ kind="positive"
+ >
+ {transaction.merchantBaseUrl}
+ </Header>
+ {/* <Part
+ title={<i18n.Translate>Merchant</i18n.Translate>}
+ text={transaction.info.merchant.name}
+ kind="neutral"
/>
- <br />
<Part
- big
- title={<i18n.Translate>Total tip</i18n.Translate>}
- text={<Amount value={transaction.amountRaw} />}
- kind="positive"
+ title={<i18n.Translate>Invoice ID</i18n.Translate>}
+ text={transaction.info.orderId}
+ kind="neutral"
+ /> */}
+ <Part
+ title={<i18n.Translate>Details</i18n.Translate>}
+ text={<TipDetails transaction={transaction} />}
/>
- {Amounts.isNonZero(fee) && (
- <Fragment>
- <Part
- big
- title={<i18n.Translate>Received amount</i18n.Translate>}
- text={<Amount value={transaction.amountEffective} />}
- kind="neutral"
- />
- <Part
- big
- title={<i18n.Translate>Fee</i18n.Translate>}
- text={<Amount value={fee} />}
- kind="negative"
- />
- </Fragment>
- )}
</TransactionTemplate>
);
}
if (transaction.type === TransactionType.Refund) {
- const fee = Amounts.sub(
- Amounts.parseOrThrow(transaction.amountRaw),
- Amounts.parseOrThrow(transaction.amountEffective),
- ).amount;
+ const total = Amounts.parseOrThrow(transaction.amountEffective);
return (
<TransactionTemplate>
- <SubTitle>
- <i18n.Translate>Refund</i18n.Translate>
- </SubTitle>
- <Time
- timestamp={AbsoluteTime.fromTimestamp(transaction.timestamp)}
- format="dd MMMM yyyy, HH:mm"
- />
- <br />
- <Part
- big
- title={<i18n.Translate>Total refund</i18n.Translate>}
- text={<Amount value={transaction.amountEffective} />}
+ <Header
+ timestamp={transaction.timestamp}
+ type={i18n.str`Refund`}
+ total={total}
kind="positive"
- />
- {Amounts.isNonZero(fee) && (
- <Fragment>
- <Part
- big
- title={<i18n.Translate>Refund amount</i18n.Translate>}
- text={<Amount value={transaction.amountRaw} />}
- kind="neutral"
- />
- <Part
- big
- title={<i18n.Translate>Fee</i18n.Translate>}
- text={<Amount value={fee} />}
- kind="negative"
- />
- </Fragment>
- )}
+ >
+ {transaction.info.summary}
+ </Header>
+
<Part
title={<i18n.Translate>Merchant</i18n.Translate>}
text={transaction.info.merchant.name}
kind="neutral"
/>
-
<Part
- title={<i18n.Translate>Purchase</i18n.Translate>}
+ title={<i18n.Translate>Original order ID</i18n.Translate>}
text={
<a
href={Pages.balance_transaction.replace(
":tid",
transaction.refundedTransactionId,
)}
- // href={transaction.info.fulfillmentUrl}
- // target="_bank"
- // rel="noreferrer"
>
- {transaction.info.summary}
+ {transaction.info.orderId}
</a>
}
kind="neutral"
/>
<Part
- title={<i18n.Translate>Receipt</i18n.Translate>}
- text={`#${transaction.info.orderId}`}
+ title={<i18n.Translate>Purchase summary</i18n.Translate>}
+ text={transaction.info.summary}
kind="neutral"
/>
-
- <div>
- {transaction.info.products && transaction.info.products.length > 0 && (
- <ListOfProducts>
- {transaction.info.products.map((p, k) => (
- <RowBorderGray key={k}>
- <a href="#" onClick={showLargePic}>
- <img src={p.image ? p.image : emptyImg} />
- </a>
- <div>
- {p.quantity && p.quantity > 0 && (
- <SmallLightText>
- x {p.quantity} {p.unit}
- </SmallLightText>
- )}
- <div>{p.description}</div>
- </div>
- </RowBorderGray>
- ))}
- </ListOfProducts>
- )}
- </div>
+ <Part
+ title={<i18n.Translate>Details</i18n.Translate>}
+ text={<RefundDetails transaction={transaction} />}
+ />
</TransactionTemplate>
);
}
return <div />;
}
+
+function DeliveryDetails({
+ date,
+ location,
+}: {
+ date: TalerProtocolTimestamp | undefined;
+ location: Location | undefined;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <PurchaseDetailsTable>
+ {location && (
+ <Fragment>
+ {location.country && (
+ <tr>
+ <td>
+ <i18n.Translate>Country</i18n.Translate>
+ </td>
+ <td>{location.country}</td>
+ </tr>
+ )}
+ {location.address_lines && (
+ <tr>
+ <td>
+ <i18n.Translate>Address lines</i18n.Translate>
+ </td>
+ <td>{location.address_lines}</td>
+ </tr>
+ )}
+ {location.building_number && (
+ <tr>
+ <td>
+ <i18n.Translate>Building number</i18n.Translate>
+ </td>
+ <td>{location.building_number}</td>
+ </tr>
+ )}
+ {location.building_name && (
+ <tr>
+ <td>
+ <i18n.Translate>Building name</i18n.Translate>
+ </td>
+ <td>{location.building_name}</td>
+ </tr>
+ )}
+ {location.street && (
+ <tr>
+ <td>
+ <i18n.Translate>Street</i18n.Translate>
+ </td>
+ <td>{location.street}</td>
+ </tr>
+ )}
+ {location.post_code && (
+ <tr>
+ <td>
+ <i18n.Translate>Post code</i18n.Translate>
+ </td>
+ <td>{location.post_code}</td>
+ </tr>
+ )}
+ {location.town_location && (
+ <tr>
+ <td>
+ <i18n.Translate>Town location</i18n.Translate>
+ </td>
+ <td>{location.town_location}</td>
+ </tr>
+ )}
+ {location.town && (
+ <tr>
+ <td>
+ <i18n.Translate>Town</i18n.Translate>
+ </td>
+ <td>{location.town}</td>
+ </tr>
+ )}
+ {location.district && (
+ <tr>
+ <td>
+ <i18n.Translate>District</i18n.Translate>
+ </td>
+ <td>{location.district}</td>
+ </tr>
+ )}
+ {location.country_subdivision && (
+ <tr>
+ <td>
+ <i18n.Translate>Country subdivision</i18n.Translate>
+ </td>
+ <td>{location.country_subdivision}</td>
+ </tr>
+ )}
+ </Fragment>
+ )}
+
+ {!location || !date ? undefined : (
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ )}
+ {date && (
+ <Fragment>
+ <tr>
+ <td>Date</td>
+ <td>
+ <Time
+ timestamp={AbsoluteTime.fromTimestamp(date)}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ </td>
+ </tr>
+ </Fragment>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+function PurchaseDetails({
+ transaction,
+}: {
+ transaction: TransactionPayment;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const partialFee = Amounts.sub(
+ Amounts.parseOrThrow(transaction.amountEffective),
+ Amounts.parseOrThrow(transaction.amountRaw),
+ ).amount;
+
+ const refundRaw = Amounts.parseOrThrow(transaction.totalRefundRaw);
+
+ const refundFee = Amounts.sub(
+ refundRaw,
+ Amounts.parseOrThrow(transaction.totalRefundEffective),
+ ).amount;
+
+ const fee = Amounts.sum([partialFee, refundFee]).amount;
+
+ const hasProducts =
+ transaction.info.products && transaction.info.products.length > 0;
+
+ const hasShipping =
+ transaction.info.delivery_date !== undefined ||
+ transaction.info.delivery_location !== undefined;
+
+ const showLargePic = (): void => {
+ return;
+ };
+
+ const total = Amounts.sub(
+ Amounts.parseOrThrow(transaction.amountEffective),
+ Amounts.parseOrThrow(transaction.totalRefundEffective),
+ ).amount;
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>Price</td>
+ <td>
+ <Amount value={transaction.amountRaw} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(refundRaw) && (
+ <tr>
+ <td>Refunded</td>
+ <td>
+ <Amount value={transaction.totalRefundEffective} />
+ </td>
+ </tr>
+ )}
+ {Amounts.isNonZero(fee) && (
+ <tr>
+ <td>Transaction fees</td>
+ <td>
+ <Amount value={fee} />
+ </td>
+ </tr>
+ )}
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>Total</td>
+ <td>
+ <Amount value={total} />
+ </td>
+ </tr>
+ {hasProducts && (
+ <tr>
+ <td colSpan={2}>
+ <PartCollapsible
+ big
+ title={<i18n.Translate>Products</i18n.Translate>}
+ text={
+ <ListOfProducts>
+ {transaction.info.products?.map((p, k) => (
+ <Row key={k}>
+ <a href="#" onClick={showLargePic}>
+ <img src={p.image ? p.image : emptyImg} />
+ </a>
+ <div>
+ {p.quantity && p.quantity > 0 && (
+ <SmallLightText>
+ x {p.quantity} {p.unit}
+ </SmallLightText>
+ )}
+ <div>{p.description}</div>
+ </div>
+ </Row>
+ ))}
+ </ListOfProducts>
+ }
+ />
+ </td>
+ </tr>
+ )}
+ {hasShipping && (
+ <tr>
+ <td colSpan={2}>
+ <PartCollapsible
+ big
+ title={<i18n.Translate>Delivery</i18n.Translate>}
+ text={
+ <DeliveryDetails
+ date={transaction.info.delivery_date}
+ location={transaction.info.delivery_location}
+ />
+ }
+ />
+ </td>
+ </tr>
+ )}
+ </PurchaseDetailsTable>
+ );
+}
+
+function RefundDetails({
+ transaction,
+}: {
+ transaction: TransactionRefund;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const fee = Amounts.sub(
+ Amounts.parseOrThrow(transaction.amountRaw),
+ Amounts.parseOrThrow(transaction.amountEffective),
+ ).amount;
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>Amount</td>
+ <td>
+ <Amount value={transaction.amountRaw} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(fee) && (
+ <tr>
+ <td>Transaction fees</td>
+ <td>
+ <Amount value={fee} />
+ </td>
+ </tr>
+ )}
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>Total</td>
+ <td>
+ <Amount value={transaction.amountEffective} />
+ </td>
+ </tr>
+ </PurchaseDetailsTable>
+ );
+}
+
+function DepositDetails({
+ transaction,
+}: {
+ transaction: TransactionDeposit;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const fee = Amounts.sub(
+ Amounts.parseOrThrow(transaction.amountRaw),
+ Amounts.parseOrThrow(transaction.amountEffective),
+ ).amount;
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>Amount</td>
+ <td>
+ <Amount value={transaction.amountRaw} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(fee) && (
+ <tr>
+ <td>Transaction fees</td>
+ <td>
+ <Amount value={fee} />
+ </td>
+ </tr>
+ )}
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>Total transfer</td>
+ <td>
+ <Amount value={transaction.amountEffective} />
+ </td>
+ </tr>
+ </PurchaseDetailsTable>
+ );
+}
+function RefreshDetails({
+ transaction,
+}: {
+ transaction: TransactionRefresh;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const fee = Amounts.sub(
+ Amounts.parseOrThrow(transaction.amountRaw),
+ Amounts.parseOrThrow(transaction.amountEffective),
+ ).amount;
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>Amount</td>
+ <td>
+ <Amount value={transaction.amountRaw} />
+ </td>
+ </tr>
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>Transaction fees</td>
+ <td>
+ <Amount value={fee} />
+ </td>
+ </tr>
+ </PurchaseDetailsTable>
+ );
+}
+
+function TipDetails({ transaction }: { transaction: TransactionTip }): VNode {
+ const { i18n } = useTranslationContext();
+
+ const fee = Amounts.sub(
+ Amounts.parseOrThrow(transaction.amountRaw),
+ Amounts.parseOrThrow(transaction.amountEffective),
+ ).amount;
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>Amount</td>
+ <td>
+ <Amount value={transaction.amountRaw} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(fee) && (
+ <tr>
+ <td>Transaction fees</td>
+ <td>
+ <Amount value={fee} />
+ </td>
+ </tr>
+ )}
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>Total</td>
+ <td>
+ <Amount value={transaction.amountEffective} />
+ </td>
+ </tr>
+ </PurchaseDetailsTable>
+ );
+}
+
+function WithdrawDetails({
+ transaction,
+}: {
+ transaction: TransactionWithdrawal;
+}): VNode {
+ const { i18n } = useTranslationContext();
+
+ const fee = Amounts.sub(
+ Amounts.parseOrThrow(transaction.amountRaw),
+ Amounts.parseOrThrow(transaction.amountEffective),
+ ).amount;
+
+ return (
+ <PurchaseDetailsTable>
+ <tr>
+ <td>Withdraw</td>
+ <td>
+ <Amount value={transaction.amountRaw} />
+ </td>
+ </tr>
+
+ {Amounts.isNonZero(fee) && (
+ <tr>
+ <td>Transaction fees</td>
+ <td>
+ <Amount value={fee} />
+ </td>
+ </tr>
+ )}
+ <tr>
+ <td colSpan={2}>
+ <hr />
+ </td>
+ </tr>
+ <tr>
+ <td>Total</td>
+ <td>
+ <Amount value={transaction.amountEffective} />
+ </td>
+ </tr>
+ </PurchaseDetailsTable>
+ );
+}
+
+function Header({
+ timestamp,
+ total,
+ children,
+ kind,
+ type,
+}: {
+ timestamp: TalerProtocolTimestamp;
+ total: AmountJson;
+ children: ComponentChildren;
+ kind: Kind;
+ type: string;
+}): VNode {
+ return (
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-between",
+ flexDirection: "row",
+ }}
+ >
+ <div>
+ <SubTitle>{children}</SubTitle>
+ <Time
+ timestamp={AbsoluteTime.fromTimestamp(timestamp)}
+ format="dd MMMM yyyy, HH:mm"
+ />
+ </div>
+ <div>
+ <SubTitle>
+ <Part
+ title={type}
+ text={<Amount value={total} />}
+ kind={kind}
+ showSign
+ />
+ </SubTitle>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/taler-wallet-webextension/static-dev/merchant-icon-11.jpeg b/packages/taler-wallet-webextension/static-dev/merchant-icon-11.jpeg
new file mode 100644
index 000000000..1777936c8
--- /dev/null
+++ b/packages/taler-wallet-webextension/static-dev/merchant-icon-11.jpeg
Binary files differ