diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet')
-rw-r--r-- | packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx | 161 | ||||
-rw-r--r-- | packages/taler-wallet-webextension/src/wallet/Transaction.tsx | 1044 |
2 files changed, 794 insertions, 411 deletions
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> + ); +} |