/* This file is part of GNU Taler (C) 2022 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ import { AbsoluteTime, amountFractionalLength, AmountJson, Amounts, Location, NotificationType, parsePaytoUri, PaytoUri, stringifyPaytoUri, 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"; import emptyImg from "../../static/img/empty.png"; import { Amount } from "../components/Amount.js"; 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 { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js"; import { CenteredDialog, InfoBox, ListOfProducts, Overlay, Row, SmallLightText, SubTitle, WarningBox, } from "../components/styled/index.js"; import { Time } from "../components/Time.js"; import { useTranslationContext } from "../context/translation.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { Button } from "../mui/Button.js"; import { Pages } from "../NavigationBar.js"; import * as wxApi from "../wxApi.js"; interface Props { tid: string; goToWalletHistory: (currency?: string) => Promise; } async function getTransaction(tid: string): Promise { const res = await wxApi.getTransactions(); const ts = res.transactions.filter((t) => t.transactionId === tid); if (ts.length > 1) throw Error("more than one transaction with this id"); if (ts.length === 1) { return ts[0]; } throw Error("no transaction found"); } export function TransactionPage({ tid, goToWalletHistory }: Props): VNode { const { i18n } = useTranslationContext(); const state = useAsyncAsHook(() => getTransaction(tid), [tid]); useEffect(() => { return wxApi.onUpdateNotification( [NotificationType.WithdrawGroupFinished], () => { state?.retry(); }, ); }); if (!state) { return ; } if (state.hasError) { return ( Could not load the transaction information } error={state} /> ); } const currency = Amounts.parse(state.response.amountRaw)?.currency; return ( wxApi.deleteTransaction(tid).then(() => goToWalletHistory(currency)) } onRetry={() => wxApi.retryTransaction(tid).then(() => goToWalletHistory(currency)) } onRefund={(id) => wxApi.applyRefundFromPurchaseId(id).then()} onBack={() => goToWalletHistory(currency)} /> ); } export interface WalletTransactionProps { transaction: Transaction; onDelete: () => Promise; onRetry: () => Promise; onRefund: (id: string) => Promise; onBack: () => Promise; } const PurchaseDetailsTable = styled.table` width: 100%; & > tr > td:nth-child(2n) { text-align: right; } `; export function TransactionView({ transaction, onDelete, onRetry, onRefund, }: WalletTransactionProps): VNode { const [confirmBeforeForget, setConfirmBeforeForget] = useState(false); async function doCheckBeforeForget(): Promise { if ( transaction.pending && transaction.type === TransactionType.Withdrawal ) { setConfirmBeforeForget(true); } else { onDelete(); } } const { i18n } = useTranslationContext(); function TransactionTemplate({ children, }: { children: ComponentChildren; }): VNode { const showRetry = transaction.error !== undefined || transaction.timestamp.t_s === "never" || (transaction.pending && differenceInSeconds(new Date(), transaction.timestamp.t_s * 1000) > 10); return (
There was an error trying to complete the transaction } error={transaction?.error} /> {transaction.pending && ( This transaction is not completed )}
{children}
{showRetry ? ( ) : null}
); } if (transaction.type === TransactionType.Withdrawal) { const total = Amounts.parseOrThrow(transaction.amountEffective); const chosen = Amounts.parseOrThrow(transaction.amountRaw); return ( {confirmBeforeForget ? (
Caution!
If you have already wired money to the exchange you will loose the chance to get the coins form it.
) : undefined}
{transaction.exchangeBaseUrl}
{!transaction.pending ? undefined : transaction.withdrawalDetails .type === WithdrawalType.ManualTransfer ? ( Make sure to use the correct subject, otherwise the money will not arrive in this wallet. ) : ( {!transaction.withdrawalDetails.confirmed && transaction.withdrawalDetails.bankConfirmationUrl ? (
The bank did not yet confirmed the wire transfer. Go to the {` `} bank site {" "} and check there is no pending step.
) : undefined} {transaction.withdrawalDetails.confirmed && ( Bank has confirmed the wire transfer. Waiting for the exchange to send the coins )}
)} Details} text={} />
); } if (transaction.type === TransactionType.Payment) { const pendingRefund = transaction.refundPending === undefined ? undefined : Amounts.parseOrThrow(transaction.refundPending); const total = Amounts.sub( Amounts.parseOrThrow(transaction.amountEffective), Amounts.parseOrThrow(transaction.totalRefundEffective), ).amount; return (
{transaction.info.fulfillmentUrl ? ( {transaction.info.summary} ) : ( transaction.info.summary )}

{transaction.refunds.length > 0 ? ( Refunds} text={ {transaction.refunds.map((r, i) => { return ( ); })}
{}{" "} was refunded {" "} on{" "} {
} kind="neutral" /> ) : undefined} {pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && ( Merchant created a refund for this order but was not automatically picked up. Offer} text={} kind="positive" />
)} Merchant} text={
{transaction.info.merchant.logo && (
)}

{transaction.info.merchant.name}

{transaction.info.merchant.website && ( {transaction.info.merchant.website} )} {transaction.info.merchant.email && ( {transaction.info.merchant.email} )}
} kind="neutral" /> Invoice ID} text={transaction.info.orderId} kind="neutral" /> Details} text={} kind="neutral" /> ); } if (transaction.type === TransactionType.Deposit) { const total = Amounts.parseOrThrow(transaction.amountRaw); const payto = parsePaytoUri(transaction.targetPaytoUri); return (
{!payto ? transaction.targetPaytoUri : }
{payto && } Details} text={} kind="neutral" />
); } if (transaction.type === TransactionType.Refresh) { const total = Amounts.sub( Amounts.parseOrThrow(transaction.amountRaw), Amounts.parseOrThrow(transaction.amountEffective), ).amount; return (
{transaction.exchangeBaseUrl}
Details} text={} />
); } if (transaction.type === TransactionType.Tip) { const total = Amounts.parseOrThrow(transaction.amountEffective); return (
{transaction.merchantBaseUrl}
{/* Merchant} text={transaction.info.merchant.name} kind="neutral" /> Invoice ID} text={transaction.info.orderId} kind="neutral" /> */} Details} text={} />
); } if (transaction.type === TransactionType.Refund) { const total = Amounts.parseOrThrow(transaction.amountEffective); return (
{transaction.info.summary}
Merchant} text={transaction.info.merchant.name} kind="neutral" /> Original order ID} text={ {transaction.info.orderId} } kind="neutral" /> Purchase summary} text={transaction.info.summary} kind="neutral" /> Details} text={} />
); } return
; } function DeliveryDetails({ date, location, }: { date: TalerProtocolTimestamp | undefined; location: Location | undefined; }): VNode { const { i18n } = useTranslationContext(); return ( {location && ( {location.country && ( Country {location.country} )} {location.address_lines && ( Address lines {location.address_lines} )} {location.building_number && ( Building number {location.building_number} )} {location.building_name && ( Building name {location.building_name} )} {location.street && ( Street {location.street} )} {location.post_code && ( Post code {location.post_code} )} {location.town_location && ( Town location {location.town_location} )} {location.town && ( Town {location.town} )} {location.district && ( District {location.district} )} {location.country_subdivision && ( Country subdivision {location.country_subdivision} )} )} {!location || !date ? undefined : (
)} {date && ( Date )}
); } 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 ( Price {Amounts.isNonZero(refundRaw) && ( Refunded )} {Amounts.isNonZero(fee) && ( Transaction fees )}
Total {hasProducts && ( Products} text={ {transaction.info.products?.map((p, k) => (
{p.quantity && p.quantity > 0 && ( x {p.quantity} {p.unit} )}
{p.description}
))}
} /> )} {hasShipping && ( Delivery} text={ } /> )}
); } function RefundDetails({ transaction, }: { transaction: TransactionRefund; }): VNode { const { i18n } = useTranslationContext(); const r = Amounts.parseOrThrow(transaction.amountRaw); const e = Amounts.parseOrThrow(transaction.amountEffective); const fee = Amounts.sub(r, e).amount; const maxFrac = [r, e, fee] .map((a) => Amounts.maxFractionalDigits(a)) .reduce((c, p) => Math.max(c, p), 0); return ( Amount {Amounts.isNonZero(fee) && ( Transaction fees )}
Total
); } function DepositDetails({ transaction, }: { transaction: TransactionDeposit; }): VNode { const { i18n } = useTranslationContext(); const r = Amounts.parseOrThrow(transaction.amountRaw); const e = Amounts.parseOrThrow(transaction.amountEffective); const fee = Amounts.sub(r, e).amount; const maxFrac = [r, e, fee] .map((a) => Amounts.maxFractionalDigits(a)) .reduce((c, p) => Math.max(c, p), 0); return ( Amount {Amounts.isNonZero(fee) && ( Transaction fees )}
Total transfer
); } function RefreshDetails({ transaction, }: { transaction: TransactionRefresh; }): VNode { const { i18n } = useTranslationContext(); const r = Amounts.parseOrThrow(transaction.amountRaw); const e = Amounts.parseOrThrow(transaction.amountEffective); const fee = Amounts.sub(r, e).amount; const maxFrac = [r, e, fee] .map((a) => Amounts.maxFractionalDigits(a)) .reduce((c, p) => Math.max(c, p), 0); return ( Amount Transaction fees
Total
); } function TipDetails({ transaction }: { transaction: TransactionTip }): VNode { const { i18n } = useTranslationContext(); const r = Amounts.parseOrThrow(transaction.amountRaw); const e = Amounts.parseOrThrow(transaction.amountEffective); const fee = Amounts.sub(r, e).amount; const maxFrac = [r, e, fee] .map((a) => Amounts.maxFractionalDigits(a)) .reduce((c, p) => Math.max(c, p), 0); return ( Amount {Amounts.isNonZero(fee) && ( Transaction fees )}
Total
); } function WithdrawDetails({ transaction, }: { transaction: TransactionWithdrawal; }): VNode { const { i18n } = useTranslationContext(); const r = Amounts.parseOrThrow(transaction.amountRaw); const e = Amounts.parseOrThrow(transaction.amountEffective); const fee = Amounts.sub(r, e).amount; const maxFrac = [r, e, fee] .map((a) => Amounts.maxFractionalDigits(a)) .reduce((c, p) => Math.max(c, p), 0); return ( Withdraw {Amounts.isNonZero(fee) && ( Transaction fees )}
Total
); } function Header({ timestamp, total, children, kind, type, }: { timestamp: TalerProtocolTimestamp; total: AmountJson; children: ComponentChildren; kind: Kind; type: string; }): VNode { return (
{children}
} kind={kind} />
); } function NicePayto({ payto }: { payto: PaytoUri }): VNode { if (payto.isKnown) { switch (payto.targetType) { case "bitcoin": { return
{payto.targetPath.substring(0, 20)}...
; } case "x-taler-bank": { const url = new URL("/", `https://${payto.host}`); return (
{payto.account}
{url.toString()}
); } case "iban": { return
{payto.targetPath.substring(0, 20)}
; } } } return {stringifyPaytoUri(payto)}; }