Skip to content

Commit

Permalink
Added ability to close transaction toasts on timeout (#1352)
Browse files Browse the repository at this point in the history
* Added ability to close transaction toasts on timeout

* Refactor

* Refactor

* Added WS fallback mechanisms for transactions tracker

* Lint

* Refactor

* Refactor

* Refactor
  • Loading branch information
razvantomegea authored Jan 16, 2025
1 parent e7c49a7 commit dd29454
Show file tree
Hide file tree
Showing 20 changed files with 186 additions and 69 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- [Added ability to close transaction toasts on timeout](https://github.com/multiversx/mx-sdk-dapp/pull/1352)

## [[v3.1.8](https://github.com/multiversx/mx-sdk-dapp/pull/1360)] - 2025-01-15

- [Added search tooltip to the Ledger address table pagination](https://github.com/multiversx/mx-sdk-dapp/pull/1359)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const useTransactionToast = ({
const transactionDisplayInfo = useGetTransactionDisplayInfo(toastId);
const accountShard = useSelector(shardSelector);
const { address } = useGetAccount();
const timeoutRef = useRef<NodeJS.Timeout>();
const lifetimeAfterSuccessTimeoutRef = useRef<NodeJS.Timeout>();
const areSameShardTransactions = useMemo(
() => getAreTransactionsOnSameShard(transactions, accountShard),
[transactions, accountShard]
Expand Down Expand Up @@ -85,21 +85,25 @@ export const useTransactionToast = ({
};

useEffect(() => {
if (!isCompleted || !lifetimeAfterSuccess || timeoutRef.current) {
if (
!isCompleted ||
!lifetimeAfterSuccess ||
lifetimeAfterSuccessTimeoutRef.current
) {
return;
}

timeoutRef.current = setTimeout(() => {
lifetimeAfterSuccessTimeoutRef.current = setTimeout(() => {
handleDeleteToast();
}, lifetimeAfterSuccess);

return () => {
if (timeoutRef.current) {
if (lifetimeAfterSuccessTimeoutRef.current) {
// Clear timer on unmount and also delete the toast
// The toast may have been removed before the timer finished by the re-rendering
// of the toasts list during another toast removal from the store
handleDeleteToast();
clearTimeout(timeoutRef.current);
clearTimeout(lifetimeAfterSuccessTimeoutRef.current);
}
};
}, [lifetimeAfterSuccess, isCompleted]);
Expand Down
2 changes: 1 addition & 1 deletion src/components/TransactionsTracker/TransactionTracker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useBatchTransactionsTracker } from 'hooks/transactions/batch/tracker/useBatchTransactionsTracker';
import { useTransactionsTracker } from 'hooks/transactions/useTransactionsTracker';
import { useTransactionsTracker } from 'hooks/transactions/useTransactionsTracker/useTransactionsTracker';
import { TransactionsTrackerType } from 'types/transactionsTracker.types';

export function TransactionsTracker(props: TransactionsTrackerType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { useCallback, useEffect, useRef } from 'react';
import { TRANSACTIONS_STATUS_DROP_INTERVAL_MS } from 'constants/transactionStatus';
import {
TRANSACTIONS_STATUS_DROP_INTERVAL_MS,
TRANSACTIONS_STATUS_POLLING_INTERVAL_MS
} from 'constants/transactionStatus';
import { removeBatchTransactions } from 'services/transactions';
import { getTransactionsStatus } from 'utils/transactions/batch/getTransactionsStatus';
import { sequentialToFlatArray } from 'utils/transactions/batch/sequentialToFlatArray';
import {
websocketConnection,
WebsocketConnectionStatusEnum
} from '../../../websocketListener/websocketConnection';
import { extractSessionId } from '../../helpers/extractSessionId';
import { timestampIsOlderThan } from '../../helpers/timestampIsOlderThan';
import { useGetPollingInterval } from '../../useGetPollingInterval';
import { useUpdateTrackedTransactions } from '../../useTransactionsTracker/useUpdateTrackedTransactions';
import { useGetBatches } from '../useGetBatches';
import { useUpdateBatch } from './useUpdateBatch';

/**
* Fallback mechanism to check hanging batches
Expand All @@ -22,11 +20,8 @@ export const useCheckHangingBatchesFallback = (props?: {
onFail?: (sessionId: string | null, errorMessage?: string) => void;
}) => {
const { batchTransactionsArray } = useGetBatches();
const pollingInterval = useGetPollingInterval();
const updateBatch = useUpdateBatch();
const updateBatch = useUpdateTrackedTransactions();
const pollingIntervalTimer = useRef<NodeJS.Timeout | null>(null);
const isWebsocketCompleted =
websocketConnection.status === WebsocketConnectionStatusEnum.COMPLETED;
const onSuccess = props?.onSuccess;
const onFail = props?.onFail;

Expand Down Expand Up @@ -73,22 +68,13 @@ export const useCheckHangingBatchesFallback = (props?: {
}, [batchTransactionsArray, updateBatch, onSuccess, onFail]);

useEffect(() => {
if (isWebsocketCompleted) {
// Do not setInterval if we already subscribe to websocket event
if (pollingIntervalTimer.current) {
clearInterval(pollingIntervalTimer.current);
}

return;
}

if (pollingIntervalTimer.current) {
return;
}

pollingIntervalTimer.current = setInterval(() => {
checkHangingBatches();
}, pollingInterval);
}, TRANSACTIONS_STATUS_POLLING_INTERVAL_MS);

return () => {
if (pollingIntervalTimer.current) {
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/transactions/batch/tracker/useVerifyBatchStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { extractSessionId } from 'hooks/transactions/helpers/extractSessionId';
import { useGetSignedTransactions } from 'hooks/transactions/useGetSignedTransactions';
import { useDispatch } from 'reduxStore/DappProviderContext';
import { getTransactionsStatus } from 'utils/transactions/batch/getTransactionsStatus';
import { useUpdateTrackedTransactions } from '../../useTransactionsTracker/useUpdateTrackedTransactions';
import { useCheckBatch } from './useCheckBatch';
import { useUpdateBatch } from './useUpdateBatch';

export const useVerifyBatchStatus = (props?: {
onSuccess?: (sessionId: string | null) => void;
Expand All @@ -15,7 +15,7 @@ export const useVerifyBatchStatus = (props?: {
const { signedTransactions } = useGetSignedTransactions();

const checkBatch = useCheckBatch();
const updateBatch = useUpdateBatch();
const updateBatch = useUpdateTrackedTransactions();
const resolveBatchStatusResponse = useResolveBatchStatusResponse();

const onSuccess = props?.onSuccess;
Expand Down Expand Up @@ -56,7 +56,7 @@ export const useVerifyBatchStatus = (props?: {
const data = await checkBatch({ batchId });
await updateBatch({
sessionId: sessionId.toString(),
isBatchFailed: data?.isBatchFailed,
isFailed: data?.isBatchFailed,
shouldRefreshBalance: true,
transactions: sessionTransactions
});
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './useCheckTransactionStatus';
export * from './useTransactionsTracker/useCheckTransactionStatus';
export * from './useGetActiveTransactionsStatus';
export * from './useGetFailedTransactions';
export * from './useGetLastPendingTransactionHash';
Expand All @@ -16,4 +16,4 @@ export * from './useSignTransactions';
export * from './useSignTransactionsCommonData';
export * from './useSignTransactionsWithDevice';
export * from './useSignTransactionsWithLedger';
export * from './useTrackTransactionStatus';
export * from './useTransactionsTracker/useTrackTransactionStatus';
1 change: 1 addition & 0 deletions src/hooks/transactions/useTransactionsTracker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useTransactionsTracker';
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect, useRef } from 'react';
import { getTransactionsByHashes as defaultGetTxByHash } from 'apiCalls/transactions';
import { TRANSACTIONS_STATUS_POLLING_INTERVAL_MS } from 'constants/transactionStatus';
import { TransactionsTrackerType } from 'types/transactionsTracker.types';
import { timestampIsOlderThan } from '../helpers/timestampIsOlderThan';
import { useGetPendingTransactions } from '../useGetPendingTransactions';
import { useCheckTransactionStatus } from './useCheckTransactionStatus';

/**
* Fallback mechanism to check hanging transactions
* Resolves the toast and set the status to failed for each transaction after a certain time (90 seconds)
* */
export const useCheckHangingTransactionsFallback = (
props?: TransactionsTrackerType
) => {
const { pendingTransactionsArray } = useGetPendingTransactions();
const checkTransactionStatus = useCheckTransactionStatus();
const pollingIntervalTimer = useRef<NodeJS.Timeout | null>(null);

const getTransactionsByHash =
props?.getTransactionsByHash ?? defaultGetTxByHash;

const checkHangingTransactions = async () => {
for (const [sessionId] of pendingTransactionsArray) {
if (
!timestampIsOlderThan(
Number(sessionId),
TRANSACTIONS_STATUS_POLLING_INTERVAL_MS
)
) {
continue;
}

await checkTransactionStatus({
getTransactionsByHash,
...props
});
}
};

useEffect(() => {
if (pollingIntervalTimer.current) {
return;
}

pollingIntervalTimer.current = setInterval(() => {
checkHangingTransactions();
}, TRANSACTIONS_STATUS_POLLING_INTERVAL_MS);

return () => {
if (pollingIntervalTimer.current) {
clearInterval(pollingIntervalTimer.current);
}
};
}, []);
};
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { useEffect, useRef } from 'react';
import { getTransactionsByHashes as defaultGetTxByHash } from 'apiCalls/transactions';
import { TransactionsTrackerType } from 'types/transactionsTracker.types';
import { useRegisterWebsocketListener } from '../websocketListener';
import {
websocketConnection,
WebsocketConnectionStatusEnum
} from '../websocketListener/websocketConnection';
} from '../../websocketListener/websocketConnection';
import { useGetPollingInterval } from '../useGetPollingInterval';
import { useCheckTransactionStatus } from './useCheckTransactionStatus';
import { useGetPollingInterval } from './useGetPollingInterval';

export function useTransactionsTracker(props?: TransactionsTrackerType) {
/**
* Fallback mechanism to check the transaction in case of ws connection failure
* Resolves the toast by checking the transaction after a certain time (90seconds)
* */
export const useCheckTransactionOnWsFailureFallback = (
props?: TransactionsTrackerType
) => {
const checkTransactionStatus = useCheckTransactionStatus();
const pollingInterval = useGetPollingInterval();
const pollingIntervalTimer = useRef<NodeJS.Timeout | null>(null);
Expand All @@ -19,15 +24,6 @@ export function useTransactionsTracker(props?: TransactionsTrackerType) {
const getTransactionsByHash =
props?.getTransactionsByHash ?? defaultGetTxByHash;

const onMessage = () => {
checkTransactionStatus({
getTransactionsByHash,
...props
});
};

useRegisterWebsocketListener(onMessage);

useEffect(() => {
if (isWebsocketCompleted) {
// Do not setInterval if we already subscribe to websocket event
Expand All @@ -42,12 +38,17 @@ export function useTransactionsTracker(props?: TransactionsTrackerType) {
return;
}

pollingIntervalTimer.current = setInterval(onMessage, pollingInterval);
pollingIntervalTimer.current = setInterval(() => {
checkTransactionStatus({
getTransactionsByHash,
...props
});
}, pollingInterval);

return () => {
if (pollingIntervalTimer.current) {
clearInterval(pollingIntervalTimer.current);
}
};
}, [onMessage, websocketConnection]);
}
}, [checkTransactionStatus]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface UseTrackTransactionStatusArgsType {
onCancelled?: (transactionId: string | null) => void;
}

// TODO: Seems unused and replaced by useCheckTransactionStatus
export function useTrackTransactionStatus({
transactionId: txId,
onSuccess,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect } from 'react';
import { getTransactionsByHashes as defaultGetTxByHash } from 'apiCalls/transactions';
import { TransactionsTrackerType } from 'types/transactionsTracker.types';
import { useRegisterWebsocketListener } from '../../websocketListener';
import { useCheckHangingTransactionsFallback } from './useCheckHangingTransactionsFallback';
import { useCheckTransactionOnWsFailureFallback } from './useCheckTransactionOnWsFailureFallback';
import { useCheckTransactionStatus } from './useCheckTransactionStatus';

export function useTransactionsTracker(props?: TransactionsTrackerType) {
const checkTransactionStatus = useCheckTransactionStatus();

const getTransactionsByHash =
props?.getTransactionsByHash ?? defaultGetTxByHash;

const onMessage = () => {
checkTransactionStatus({
getTransactionsByHash,
...props
});
};

// register ws listener
useRegisterWebsocketListener(onMessage);

// Fallbacks
useCheckTransactionOnWsFailureFallback(props);

useCheckHangingTransactionsFallback(props);

useEffect(() => {
onMessage();
}, []);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,24 @@ import {
} from 'types';
import { refreshAccount } from 'utils/account/refreshAccount';

export function useUpdateBatch() {
// TODO: This function seems duplicate to the manageTransaction from checkBatch.ts file
export function useUpdateTrackedTransactions() {
const dispatch = useDispatch();
const { address } = useGetAccount();

const handleBatchSuccess = useCallback(
const handleSuccess = useCallback(
({
sessionId,
dropUnprocessedTransactions,
serverTransactions,
batchTransactions
signedTransactions
}: {
sessionId: string;
dropUnprocessedTransactions?: boolean;
serverTransactions: ServerTransactionType[];
batchTransactions: SignedTransactionType[];
signedTransactions: SignedTransactionType[];
}) => {
for (const transaction of batchTransactions) {
for (const transaction of signedTransactions) {
const apiTx = serverTransactions.find(
(tx) => tx.txHash === transaction.hash
);
Expand Down Expand Up @@ -60,7 +61,7 @@ export function useUpdateBatch() {
return useCallback(
async (props?: {
sessionId: string;
isBatchFailed?: boolean;
isFailed?: boolean;
dropUnprocessedTransactions?: boolean;
shouldRefreshBalance?: boolean;
transactions?: SignedTransactionType[];
Expand All @@ -69,13 +70,13 @@ export function useUpdateBatch() {
return;
}

const { transactions, isBatchFailed, sessionId } = props;
const { transactions, isFailed, sessionId } = props;

if (!transactions || transactions.length === 0) {
return;
}

if (isBatchFailed) {
if (isFailed) {
for (const transaction of transactions) {
dispatch(
updateSignedTransactionStatus({
Expand All @@ -93,18 +94,18 @@ export function useUpdateBatch() {
);

if (success && data) {
handleBatchSuccess({
handleSuccess({
sessionId,
dropUnprocessedTransactions: props.dropUnprocessedTransactions,
serverTransactions: data,
batchTransactions: transactions
signedTransactions: transactions
});
}

if (props.shouldRefreshBalance) {
await refreshAccount();
}
},
[dispatch, address, handleBatchSuccess]
[dispatch, address, handleSuccess]
);
}
Loading

0 comments on commit dd29454

Please sign in to comment.