diff options
14 files changed, 975 insertions, 459 deletions
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts index d9213ef5d..7fc3fcba0 100644 --- a/packages/taler-util/src/talerTypes.ts +++ b/packages/taler-util/src/talerTypes.ts @@ -362,6 +362,9 @@ export interface MerchantInfo { name: string; jurisdiction?: Location; address?: Location; + logo?: string; + website?: string; + email?: string; } export interface Tax { diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts index 37c1c7ef1..dcaa56675 100644 --- a/packages/taler-util/src/transactionsTypes.ts +++ b/packages/taler-util/src/transactionsTypes.ts @@ -33,6 +33,7 @@ import { codecForInternationalizedString, codecForMerchantInfo, codecForProduct, + Location, } from "./talerTypes.js"; import { Codec, @@ -277,6 +278,17 @@ export interface OrderShortInfo { products: Product[] | undefined; /** + * Time indicating when the order should be delivered. + * May be overwritten by individual products. + */ + delivery_date?: TalerProtocolTimestamp; + + /** + * Delivery location for (all!) products. + */ + delivery_location?: Location; + + /** * URL of the fulfillment, given by the merchant */ fulfillmentUrl?: string; 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} {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 'Add Recipient' 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 'Pay - to' 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 Binary files differnew file mode 100644 index 000000000..1777936c8 --- /dev/null +++ b/packages/taler-wallet-webextension/static-dev/merchant-icon-11.jpeg |