diff --git a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx index 80dc18f49..f9b30907d 100644 --- a/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx +++ b/packages/taler-wallet-webextension/src/components/PendingTransactions.tsx @@ -16,6 +16,7 @@ import { AbsoluteTime, Amounts, + ExtendedStatus, NotificationType, Transaction, } from "@gnu-taler/taler-util"; @@ -56,7 +57,9 @@ export function PendingTransactions({ goToTransaction }: Props): VNode { const transactions = !state || state.hasError ? cache.tx - : state.response.transactions.filter((t) => t.pending); + : state.response.transactions.filter( + (t) => t.extendedStatus === ExtendedStatus.Pending, + ); if (state && !state.hasError) { cache.tx = transactions; diff --git a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx index c2c4b52e3..71d7edaf0 100644 --- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx +++ b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx @@ -22,6 +22,7 @@ import { Transaction, TransactionType, WithdrawalType, + ExtendedStatus, } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; import { useTranslationContext } from "../context/translation.js"; @@ -52,7 +53,7 @@ export function TransactionItem(props: { tx: Transaction }): VNode { timestamp={AbsoluteTime.fromTimestamp(tx.timestamp)} iconPath={"W"} pending={ - tx.pending + tx.extendedStatus === ExtendedStatus.Pending ? tx.withdrawalDetails.type === WithdrawalType.TalerBankIntegrationApi ? !tx.withdrawalDetails.confirmed diff --git a/packages/taler-wallet-webextension/src/context/alert.ts b/packages/taler-wallet-webextension/src/context/alert.ts index e67d94671..8527f30f6 100644 --- a/packages/taler-wallet-webextension/src/context/alert.ts +++ b/packages/taler-wallet-webextension/src/context/alert.ts @@ -48,7 +48,14 @@ type Type = { alerts: Alert[]; pushAlert: (n: Alert) => void; removeAlert: (n: Alert) => void; + /** + * + * @param h + * @returns + * @deprecated use safely + */ pushAlertOnError: (h: (p: T) => Promise) => SafeHandler; + safely: (h: (p: T) => Promise, error: TranslatedString) => SafeHandler; }; const initial: Type = { @@ -56,6 +63,9 @@ const initial: Type = { pushAlertOnError: () => { throw Error("alert context not initialized"); }, + safely: () => { + throw Error("alert context not initialized"); + }, pushAlert: () => { null; }, @@ -100,8 +110,18 @@ export const AlertProvider = ({ children }: Props): VNode => { }); } + function safely( + handler: (p: T) => Promise, + message: TranslatedString + ): SafeHandler { + return withSafe(handler, (e) => { + const a = alertFromError(message, e); + pushAlert(a); + }); + } + return h(Context.Provider, { - value: { alerts, pushAlert, removeAlert, pushAlertOnError }, + value: { alerts, pushAlert, removeAlert, pushAlertOnError, safely }, children, }); }; diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx index bca0d6231..31e97f008 100644 --- a/packages/taler-wallet-webextension/src/mui/Button.tsx +++ b/packages/taler-wallet-webextension/src/mui/Button.tsx @@ -20,6 +20,7 @@ import { theme, Colors, rippleEnabled, rippleEnabledOutlined } from "./style"; // eslint-disable-next-line import/extensions import { alpha } from "./colors/manipulation"; import { useState } from "preact/hooks"; +import { SafeHandler } from "./handlers.js"; export const buttonBaseStyle = css` display: inline-flex; @@ -55,6 +56,7 @@ interface Props { tooltip?: string; color?: Colors; onClick?: () => Promise; + // onClick?: SafeHandler; } const button = css` diff --git a/packages/taler-wallet-webextension/src/stories.tsx b/packages/taler-wallet-webextension/src/stories.tsx index a7b8a4d06..b5748d962 100644 --- a/packages/taler-wallet-webextension/src/stories.tsx +++ b/packages/taler-wallet-webextension/src/stories.tsx @@ -35,6 +35,7 @@ import * as popup from "./popup/index.stories.js"; import * as wallet from "./wallet/index.stories.js"; import { renderStories } from "@gnu-taler/web-util/lib/index.browser"; +import { AlertProvider } from "./context/alert.js"; function main(): void { renderStories( @@ -56,28 +57,28 @@ function getWrapperForGroup(group: string): FunctionComponent { case "popup": return function PopupWrapper({ children }: any) { return ( - + {children} - + ); }; case "wallet": return function WalletWrapper({ children }: any) { return ( - + {children} - + ); }; case "cta": return function WalletWrapper({ children }: any) { return ( - + {children} - + ); }; default: diff --git a/packages/taler-wallet-webextension/src/wallet/Application.tsx b/packages/taler-wallet-webextension/src/wallet/Application.tsx index 46fe02225..0db06fe08 100644 --- a/packages/taler-wallet-webextension/src/wallet/Application.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Application.tsx @@ -556,9 +556,11 @@ function WalletTemplate({ {goToTransaction ? ( ) : undefined} - - {children} + + + {children} + ); diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx index fea96a02c..21e9ee55e 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx @@ -39,14 +39,13 @@ import { WithdrawalDetails, WithdrawalType, } from "@gnu-taler/taler-util"; -import { DevContextProviderForTesting } from "../context/devContext.js"; // import { // createExample, // createExampleWithCustomContext as createExampleInCustomContext, // } from "../test-utils.js"; -import { TransactionView as TestedComponent } from "./Transaction.js"; import { tests } from "@gnu-taler/web-util/lib/index.browser"; import beer from "../../static-dev/beer.png"; +import { TransactionView as TestedComponent } from "./Transaction.js"; export default { title: "transaction details", @@ -54,6 +53,7 @@ export default { argTypes: { onRetry: { action: "onRetry" }, onDelete: { action: "onDelete" }, + onCancel: { action: "onCancel" }, onBack: { action: "onBack" }, }, }; @@ -62,10 +62,10 @@ const commonTransaction = { amountRaw: "KUDOS:11", amountEffective: "KUDOS:9.2", extendedStatus: ExtendedStatus.Done, - pending: false, + pending: undefined as any as boolean, //deprecated timestamp: TalerProtocolTimestamp.now(), transactionId: "txn:deposit:12", - frozen: false, + frozen: undefined as any as boolean, //deprecated type: TransactionType.Deposit, } as TransactionCommon; @@ -255,7 +255,7 @@ export const WithdrawFiveMinutesAgoAndPending = tests.createExample( timestamp: TalerProtocolTimestamp.fromSeconds( new Date().getTime() / 1000 - 60 * 5, ), - pending: true, + extendedStatus: ExtendedStatus.Pending, }, }), ); @@ -295,7 +295,7 @@ export const WithdrawPendingManual = tests.createExample( exchangePaytoUris: ["payto://iban/ES8877998399652238"], reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", } as WithdrawalDetails, - pending: true, + extendedStatus: ExtendedStatus.Pending, }, }), ); @@ -311,7 +311,7 @@ export const WithdrawPendingTalerBankUnconfirmed = tests.createExample( reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", bankConfirmationUrl: "http://bank.demo.taler.net", }, - pending: true, + extendedStatus: ExtendedStatus.Pending, }, }, ); @@ -326,7 +326,7 @@ export const WithdrawPendingTalerBankConfirmed = tests.createExample( confirmed: true, reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", }, - pending: true, + extendedStatus: ExtendedStatus.Pending, }, }, ); @@ -443,7 +443,10 @@ export const PaymentWithoutFee = tests.createExample(TestedComponent, { }); export const PaymentPending = tests.createExample(TestedComponent, { - transaction: { ...exampleData.payment, pending: true }, + transaction: { + ...exampleData.payment, + extendedStatus: ExtendedStatus.Pending, + }, }); export const PaymentWithProducts = tests.createExample(TestedComponent, { @@ -540,7 +543,10 @@ export const DepositError = tests.createExample(TestedComponent, { }); export const DepositPending = tests.createExample(TestedComponent, { - transaction: { ...exampleData.deposit, pending: true }, + transaction: { + ...exampleData.deposit, + extendedStatus: ExtendedStatus.Pending, + }, }); export const Refresh = tests.createExample(TestedComponent, { @@ -566,7 +572,7 @@ export const TipError = tests.createExample(TestedComponent, { }); export const TipPending = tests.createExample(TestedComponent, { - transaction: { ...exampleData.tip, pending: true }, + transaction: { ...exampleData.tip, extendedStatus: ExtendedStatus.Pending }, }); export const Refund = tests.createExample(TestedComponent, { @@ -581,7 +587,10 @@ export const RefundError = tests.createExample(TestedComponent, { }); export const RefundPending = tests.createExample(TestedComponent, { - transaction: { ...exampleData.refund, pending: true }, + transaction: { + ...exampleData.refund, + extendedStatus: ExtendedStatus.Pending, + }, }); export const InvoiceCreditComplete = tests.createExample(TestedComponent, { @@ -591,7 +600,7 @@ export const InvoiceCreditComplete = tests.createExample(TestedComponent, { export const InvoiceCreditIncomplete = tests.createExample(TestedComponent, { transaction: { ...exampleData.pull_credit, - pending: true, + extendedStatus: ExtendedStatus.Pending, }, }); @@ -609,6 +618,6 @@ export const TransferDebitComplete = tests.createExample(TestedComponent, { export const TransferDebitIncomplete = tests.createExample(TestedComponent, { transaction: { ...exampleData.push_debit, - pending: true, + extendedStatus: ExtendedStatus.Pending, }, }); diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index c6fa9ec68..5ed05f87f 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -18,6 +18,7 @@ import { AbsoluteTime, AmountJson, Amounts, + ExtendedStatus, Location, MerchantInfo, NotificationType, @@ -60,11 +61,12 @@ import { WarningBox, } from "../components/styled/index.js"; import { Time } from "../components/Time.js"; -import { alertFromError } from "../context/alert.js"; +import { alertFromError, useAlertContext } from "../context/alert.js"; import { useBackendContext } from "../context/backend.js"; import { useTranslationContext } from "../context/translation.js"; import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js"; import { Button } from "../mui/Button.js"; +import { SafeHandler } from "../mui/handlers.js"; import { Pages } from "../NavigationBar.js"; interface Props { @@ -116,6 +118,12 @@ export function TransactionPage({ onSend={async () => { null; }} + onCancel={async () => { + await api.wallet.call(WalletApiOperation.AbortTransaction, { + transactionId, + }); + goToWalletHistory(currency); + }} onDelete={async () => { await api.wallet.call(WalletApiOperation.DeleteTransaction, { transactionId, @@ -141,6 +149,7 @@ export function TransactionPage({ export interface WalletTransactionProps { transaction: Transaction; onSend: () => Promise; + onCancel: () => Promise; onDelete: () => Promise; onRetry: () => Promise; onRefund: (id: string) => Promise; @@ -155,18 +164,29 @@ const PurchaseDetailsTable = styled.table` } `; -export function TransactionView({ +type TransactionTemplateProps = Omit< + Omit, + "onBack" +> & { + children: ComponentChildren; +}; + +function TransactionTemplate({ transaction, onDelete, onRetry, onSend, - onRefund, -}: WalletTransactionProps): VNode { + onCancel, + children, +}: TransactionTemplateProps): VNode { + const { i18n } = useTranslationContext(); const [confirmBeforeForget, setConfirmBeforeForget] = useState(false); + const [confirmBeforeCancel, setConfirmBeforeCancel] = useState(false); + const { safely } = useAlertContext(); async function doCheckBeforeForget(): Promise { if ( - transaction.pending && + transaction.extendedStatus === ExtendedStatus.Pending && transaction.type === TransactionType.Withdrawal ) { setConfirmBeforeForget(true); @@ -175,97 +195,64 @@ export function TransactionView({ } } - const SHOWING_RETRY_THRESHOLD_SECS = 30; - - const { i18n } = useTranslationContext(); - - function TransactionTemplate({ - children, - }: { - children: ComponentChildren; - }): VNode { - const showSend = false; - // (transaction.type === TransactionType.PeerPullCredit || - // transaction.type === TransactionType.PeerPushDebit) && - // !transaction.info.completed; - const showRetry = - transaction.error !== undefined || - transaction.timestamp.t_s === "never" || - (transaction.pending && - differenceInSeconds(new Date(), transaction.timestamp.t_s * 1000) > - SHOWING_RETRY_THRESHOLD_SECS); - - return ( - -
- {transaction?.error ? ( - transaction.error.code === 7025 ? ( - - - Follow this link to the{` `} - KYC verifier - - - ) : ( - i18n.str`No more information has been provided` - ), - }} - /> - ) : ( - - ) - ) : undefined} - {transaction.pending && ( - - This transaction is not completed - - )} -
-
{children}
-
-
- {showSend ? ( - - ) : null} -
-
- {showRetry ? ( - - ) : null} - -
-
-
- ); + async function doCheckBeforeCancel(): Promise { + setConfirmBeforeCancel(true); } - if (transaction.type === TransactionType.Withdrawal) { - const total = Amounts.parseOrThrow(transaction.amountEffective); - const chosen = Amounts.parseOrThrow(transaction.amountRaw); - return ( - + const SHOWING_RETRY_THRESHOLD_SECS = 30; + + const showSend = false; + // (transaction.type === TransactionType.PeerPullCredit || + // transaction.type === TransactionType.PeerPushDebit) && + // !transaction.info.completed; + const showRetry = + transaction.error !== undefined || + transaction.timestamp.t_s === "never" || + (transaction.extendedStatus === ExtendedStatus.Pending && + differenceInSeconds(new Date(), transaction.timestamp.t_s * 1000) > + SHOWING_RETRY_THRESHOLD_SECS); + + const transactionStillActive = + transaction.extendedStatus !== ExtendedStatus.Aborted && + transaction.extendedStatus !== ExtendedStatus.Done && + transaction.extendedStatus !== ExtendedStatus.Failed; + return ( + +
+ {transaction?.error ? ( + transaction.error.code === 7025 ? ( + + + Follow this link to the{` `} + KYC verifier + + + ) : ( + i18n.str`No more information has been provided` + ), + }} + /> + ) : ( + + ) + ) : undefined} + {transaction.extendedStatus === ExtendedStatus.Pending && ( + + This transaction is not completed + + )} {confirmBeforeForget ? ( @@ -282,18 +269,134 @@ export function TransactionView({ - ) : undefined} + {confirmBeforeCancel ? ( + + +
+ Caution! +
+
+ + Doing a cancelation while the transaction still active might + result in lost coins. Do you still want to cancel the + transaction? + +
+
+ + + +
+
+
+ ) : undefined} +
+
{children}
+
+
+ {showSend ? ( + + ) : null} +
+
+ {showRetry ? ( + + ) : null} + {transactionStillActive ? ( + + ) : ( + + )} +
+
+
+ ); +} + +export function TransactionView({ + transaction, + onDelete, + onRetry, + onSend, + onRefund, + onCancel, +}: WalletTransactionProps): VNode { + const { i18n } = useTranslationContext(); + const { safely } = useAlertContext(); + + if (transaction.type === TransactionType.Withdrawal) { + const total = Amounts.parseOrThrow(transaction.amountEffective); + const chosen = Amounts.parseOrThrow(transaction.amountRaw); + return ( +
- {!transaction.pending ? undefined : transaction.withdrawalDetails + {transaction.extendedStatus !== + ExtendedStatus.Pending ? undefined : transaction.withdrawalDetails .type === WithdrawalType.ManualTransfer ? ( +
@@ -529,7 +642,13 @@ export function TransactionView({ const total = Amounts.parseOrThrow(transaction.amountRaw); const payto = parsePaytoUri(transaction.targetPaytoUri); return ( - +
+
+
+
- - @@ -678,10 +815,10 @@ export function TransactionView({ return (
{text.substring(0, 64)}...
- -
@@ -691,7 +828,13 @@ export function TransactionView({ if (transaction.type === TransactionType.PeerPullCredit) { const total = Amounts.parseOrThrow(transaction.amountEffective); return ( - +
- {transaction.pending /** pending is not-pay */ && ( + {transaction.extendedStatus === + ExtendedStatus.Pending /** pending is not-pay */ && ( } @@ -738,7 +882,13 @@ export function TransactionView({ if (transaction.type === TransactionType.PeerPullDebit) { const total = Amounts.parseOrThrow(transaction.amountEffective); return ( - +
+
+