/*
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,
AmountJson,
Amounts,
AmountString,
DenomLossEventType,
MerchantInfo,
NotificationType,
OrderShortInfo,
parsePaytoUri,
PaytoUri,
stringifyPaytoUri,
TalerErrorCode,
TalerPreciseTimestamp,
Transaction,
TransactionAction,
TransactionDeposit,
TransactionIdStr,
TransactionInternalWithdrawal,
TransactionMajorState,
TransactionMinorState,
TransactionType,
TransactionWithdrawal,
TranslatedString,
WithdrawalType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { styled } from "@linaria/react";
import { isPast } from "date-fns";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Amount } from "../components/Amount.js";
import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType.js";
import { AlertView, ErrorAlertView } from "../components/CurrentAlerts.js";
import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { Loading } from "../components/Loading.js";
import { Kind, Part, PartPayto } from "../components/Part.js";
import { QR } from "../components/QR.js";
import { ShowFullContractTermPopup } from "../components/ShowFullContractTermPopup.js";
import {
CenteredDialog,
ErrorBox,
InfoBox,
Link,
Overlay,
SmallLightText,
SubTitle,
SvgIcon,
WarningBox,
} from "../components/styled/index.js";
import { Time } from "../components/Time.js";
import { alertFromError, useAlertContext } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { useSettings } from "../hooks/useSettings.js";
import { Button } from "../mui/Button.js";
import { SafeHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js";
import refreshIcon from "../svg/refresh_24px.inline.svg";
import { assertUnreachable } from "../utils/index.js";
interface Props {
tid: string;
goToWalletHistory: (currency?: string) => Promise;
}
export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
const transactionId = tid as TransactionIdStr; //FIXME: validate
const { i18n } = useTranslationContext();
const api = useBackendContext();
const state = useAsyncAsHook(
() =>
api.wallet.call(WalletApiOperation.GetTransactionById, {
transactionId,
}),
[transactionId],
);
useEffect(() =>
api.listener.onUpdateNotification(
[NotificationType.TransactionStateTransition],
state?.retry,
),
);
if (!state) {
return ;
}
if (state.hasError) {
return (
);
}
const currency = Amounts.parse(state.response.amountRaw)?.currency;
return (
{
await api.wallet.call(WalletApiOperation.FailTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onSuspend={async () => {
await api.wallet.call(WalletApiOperation.SuspendTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onResume={async () => {
await api.wallet.call(WalletApiOperation.ResumeTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onAbort={async () => {
await api.wallet.call(WalletApiOperation.AbortTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onRetry={async () => {
await api.wallet.call(WalletApiOperation.RetryTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onDelete={async () => {
await api.wallet.call(WalletApiOperation.DeleteTransaction, {
transactionId,
});
goToWalletHistory(currency);
}}
onRefund={async (transactionId) => {
await api.wallet.call(WalletApiOperation.StartRefundQuery, {
transactionId,
});
}}
onBack={() => goToWalletHistory(currency)}
/>
);
}
export interface WalletTransactionProps {
transaction: Transaction;
onCancel: () => Promise;
onSuspend: () => Promise;
onResume: () => Promise;
onAbort: () => Promise;
onDelete: () => Promise;
onRetry: () => Promise;
onRefund: (id: TransactionIdStr) => Promise;
onBack: () => Promise;
}
const PurchaseDetailsTable = styled.table`
width: 100%;
& > tr > td:nth-child(2n) {
text-align: right;
}
`;
type TransactionTemplateProps = Omit<
Omit,
"onBack"
> & {
children: ComponentChildren;
};
function TransactionTemplate({
transaction,
onDelete,
onRetry,
onAbort,
onResume,
onSuspend,
onCancel,
children,
}: TransactionTemplateProps): VNode {
const { i18n } = useTranslationContext();
const [confirmBeforeForget, setConfirmBeforeForget] = useState(false);
const [confirmBeforeCancel, setConfirmBeforeCancel] = useState(false);
const { safely } = useAlertContext();
const [settings] = useSettings();
async function doCheckBeforeForget(): Promise {
if (
transaction.txState.major === TransactionMajorState.Pending &&
transaction.type === TransactionType.Withdrawal
) {
setConfirmBeforeForget(true);
} else {
onDelete();
}
}
async function doCheckBeforeCancel(): Promise {
setConfirmBeforeCancel(true);
}
const showButton = getShowButtonStates(transaction);
return (
{transaction?.error &&
// FIXME: wallet core should stop sending this error on KYC
transaction.error.code !==
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED ? (
) : undefined}
{transaction.txState.major === TransactionMajorState.Pending &&
(transaction.txState.minor === TransactionMinorState.KycRequired ? (
Follow this link to the{` `}
KYC verifier.
) : (
i18n.str`No additional information has been provided.`
),
}}
/>
) : transaction.txState.minor ===
TransactionMinorState.AmlRequired ? (
The transaction has been blocked since the account required an
AML check.
) : (
This transaction is not completed
))}
{transaction.txState.major === TransactionMajorState.Aborted && (
This transaction was aborted.
)}
{transaction.txState.major === TransactionMajorState.Failed && (
This transaction failed.
)}
{confirmBeforeForget ? (
Caution!
If you have already wired money to the exchange you will loose
the chance to get the coins form it.
) : undefined}
{confirmBeforeCancel ? (
Caution!
Doing a cancellation while the transaction still active might
result in lost coins. Do you still want to cancel the
transaction?
) : undefined}
{children}
);
}
export function TransactionView({
transaction,
onDelete,
onAbort,
// onBack,
onResume,
onSuspend,
onRetry,
onRefund,
onCancel,
}: WalletTransactionProps): VNode {
const { i18n } = useTranslationContext();
const { safely } = useAlertContext();
const raw = Amounts.parseOrThrow(transaction.amountRaw);
const effective = Amounts.parseOrThrow(transaction.amountEffective);
if (
transaction.type === TransactionType.Withdrawal ||
transaction.type === TransactionType.InternalWithdrawal
) {
// const conversion =
// transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
// ? transaction.withdrawalDetails.exchangeCreditAccountDetails ?? []
// : [];
const blockedByKycOrAml =
transaction.txState.minor === TransactionMinorState.KycRequired ||
transaction.txState.minor === TransactionMinorState.AmlRequired;
return (
{transaction.exchangeBaseUrl}
{transaction.txState.major !== TransactionMajorState.Pending ||
blockedByKycOrAml ? undefined : transaction.withdrawalDetails.type ===
WithdrawalType.ManualTransfer &&
transaction.withdrawalDetails.exchangeCreditAccountDetails ? (
{transaction.withdrawalDetails.exchangeCreditAccountDetails
.length > 1 ? (
Now the payment service provider is waiting for{" "}
to be transferred. Select one of the
accounts and use the information below to complete the
operation by making a wire transfer from your bank account.
) : (
Now the payment service provider is waiting for{" "}
to be transferred. Use the
information below to complete the operation by making a wire
transfer from your bank account.
)}
) : (
//integrated bank withdrawal
)}
}
/>
);
}
if (transaction.type === TransactionType.Payment) {
const pendingRefund =
transaction.refundPending === undefined
? undefined
: Amounts.parseOrThrow(transaction.refundPending);
const effectiveRefund = Amounts.parseOrThrow(
transaction.totalRefundEffective,
);
return (
{transaction.info.fulfillmentUrl ? (
{transaction.info.summary}
) : (
transaction.info.summary
)}
{transaction.refunds.length > 0 ? (
{transaction.refunds.map((r, i) => {
return (
);
})}
}
kind="neutral"
/>
) : undefined}
{pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
{transaction.refundQueryActive ? (
Refund is in progress.
) : (
Merchant created a refund for this order but was not
automatically picked up.
)}
}
kind="positive"
/>
{transaction.refundQueryActive ? undefined : (
;
}
}
}
return {stringifyPaytoUri(payto)};
}
function ShowQrWithCopy({ text }: { text: string }): VNode {
const [showing, setShowing] = useState(false);
const { i18n } = useTranslationContext();
async function copy(): Promise {
navigator.clipboard.writeText(text);
}
async function toggle(): Promise {
setShowing((s) => !s);
}
if (showing) {
return (
);
}
return (
{text.substring(0, 64)}...
);
}
function getShowButtonStates(transaction: Transaction) {
let abort = false;
let fail = false;
let resume = false;
let remove = false;
let suspend = false;
transaction.txActions.forEach((a) => {
switch (a) {
case TransactionAction.Delete:
remove = true;
break;
case TransactionAction.Suspend:
suspend = true;
break;
case TransactionAction.Resume:
resume = true;
break;
case TransactionAction.Abort:
abort = true;
break;
case TransactionAction.Fail:
fail = true;
break;
case TransactionAction.Retry:
break;
default:
assertUnreachable(a);
break;
}
});
return { abort, fail, resume, remove, suspend };
}
function ShowWithdrawalDetailForBankIntegrated({
transaction,
}: {
transaction: TransactionWithdrawal | TransactionInternalWithdrawal;
}): VNode {
const { i18n } = useTranslationContext();
const [showDetails, setShowDetails] = useState(false);
if (
transaction.txState.major !== TransactionMajorState.Pending ||
transaction.withdrawalDetails.type === WithdrawalType.ManualTransfer
) {
return ;
}
const raw = Amounts.parseOrThrow(transaction.amountRaw);
return (
{
e.preventDefault();
setShowDetails(!showDetails);
}}
>
Show details.
{showDetails && (
)}
{!transaction.withdrawalDetails.confirmed &&
transaction.withdrawalDetails.bankConfirmationUrl ? (
Wire transfer need a confirmation. Go to the{" "}
bank site{" "}
and check wire transfer operation to exchange account is complete.
) : undefined}
{transaction.withdrawalDetails.confirmed &&
!transaction.withdrawalDetails.reserveIsReady && (
Bank has confirmed the wire transfer. Waiting for the exchange to
send the coins.
)}
{transaction.withdrawalDetails.confirmed &&
transaction.withdrawalDetails.reserveIsReady && (
Exchange is ready to send the coins, withdrawal in progress.
)}
);
}