aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-05-26 15:55:14 -0300
committerSebastian <sebasjm@gmail.com>2022-05-26 15:57:12 -0300
commit24162c1086c017305253c78280a82bfa9a572b1e (patch)
tree6842f44dad3fc029d44349527df8d0b09b92852d /packages/taler-wallet-webextension
parent72d936eaf99ad1d5ee156ba8f156a983f4ec613c (diff)
transaction details template
mayor change in the template of the transaction details for every transaction more work needs to be done in wallet core for tip and refund to show more information about the merchant like logo and website
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