Skip to content

Commit

Permalink
added transaction toasts (#53)
Browse files Browse the repository at this point in the history
* added transaction toasts
  • Loading branch information
DanutIlie authored Dec 20, 2024
1 parent 5d746e7 commit 31c6ed4
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- [Added transaction toasts](https://github.com/multiversx/mx-sdk-dapp-core/pull/53)
- [Added transactions helpers](https://github.com/multiversx/mx-sdk-dapp-core/pull/52)
- [Added transactions tracking](https://github.com/multiversx/mx-sdk-dapp-core/pull/51)
- [Added provider constants and getTransactions API call](https://github.com/multiversx/mx-sdk-dapp-core/pull/50)
Expand Down
202 changes: 202 additions & 0 deletions src/core/managers/ToastManager/ToastManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { TransactionToastList } from 'lib/sdkDappCoreUi';
import { removeTransactionToast } from 'store/actions/toasts/toastsActions';
import { isServerTransactionPending } from 'store/actions/trackedTransactions/transactionStateByStatus';
import { ToastsSliceState } from 'store/slices/toast/toastSlice.types';
import { getStore } from 'store/store';
import {
ProviderErrorsEnum,
TransactionBatchStatusesEnum,
TransactionServerStatusesEnum
} from 'types';
import { SignedTransactionType } from 'types/transactions.types';
import { createModalElement } from 'utils/createModalElement';
import {
GetToastsOptionsDataPropsType,
ITransactionToast,
TransactionsDefaultTitles,
TransactionToastEventsEnum,
IToastDataState
} from './types';

export class ToastManager {
private transactionToastsList: TransactionToastList | undefined;
private unsubscribe: () => void;
store = getStore();

constructor() {
const { toasts, trackedTransactions } = this.store.getState();
this.onToastListChange(toasts);

let previousToasts = toasts;
let previousTrackedTransactions = trackedTransactions;
this.unsubscribe = this.store.subscribe(() => {
const { toasts, trackedTransactions } = this.store.getState();
const currentToasts = toasts;

const currentTrackedTransactions = trackedTransactions;

if (
previousToasts !== currentToasts ||
previousTrackedTransactions !== currentTrackedTransactions
) {
previousToasts = currentToasts;
previousTrackedTransactions = currentTrackedTransactions;
this.onToastListChange(currentToasts);
}
});
}

private async onToastListChange(toastList: ToastsSliceState) {
const { trackedTransactions, account } = this.store.getState();
const transactionToasts: ITransactionToast[] = [];

toastList.transactionToasts.forEach(async (toast) => {
const sessionTransactions = trackedTransactions[toast.toastId];
if (!sessionTransactions) {
return;
}

const transaction: ITransactionToast = {
toastDataState: this.getToastDataStateByStatus({
address: account.address,
sender: sessionTransactions.transactions[0].sender,
toastId: toast.toastId,
status: sessionTransactions.status
}),
processedTransactionsStatus: this.getToastProceededStatus(
sessionTransactions.transactions
),
toastId: toast.toastId,
transactions: sessionTransactions.transactions.map((transaction) => ({
hash: transaction.hash,
status: transaction.status
}))
};

transactionToasts.push(transaction);
});
await this.renderUIToasts(transactionToasts);
}

private async renderUIToasts(transactionsToasts: ITransactionToast[]) {
if (!this.transactionToastsList) {
this.transactionToastsList =
await createModalElement<TransactionToastList>(
'transaction-toast-list'
);
}

const eventBus = await this.transactionToastsList.getEventBus();

if (!eventBus) {
throw new Error(ProviderErrorsEnum.eventBusError);
}

eventBus.publish(
TransactionToastEventsEnum.TRANSACTION_TOAST_DATA_UPDATE,
transactionsToasts
);
eventBus.subscribe(
TransactionToastEventsEnum.CLOSE_TOAST,
(toastId: string) => {
removeTransactionToast(toastId);
}
);
return this.transactionToastsList;
}

private getToastDataStateByStatus = ({
address,
sender,
status,
toastId
}: GetToastsOptionsDataPropsType) => {
const successToastData: IToastDataState = {
id: toastId,
icon: 'check',
hasCloseButton: true,
title: TransactionsDefaultTitles.success,
iconClassName: 'success'
};

const receivedToastData: IToastDataState = {
id: toastId,
icon: 'check',
hasCloseButton: true,
title: TransactionsDefaultTitles.received,
iconClassName: 'success'
};

const pendingToastData: IToastDataState = {
id: toastId,
icon: 'hourglass',
hasCloseButton: false,
title: TransactionsDefaultTitles.pending,
iconClassName: 'warning'
};

const failToastData: IToastDataState = {
id: toastId,
icon: 'times',
title: TransactionsDefaultTitles.failed,
hasCloseButton: true,
iconClassName: 'danger'
};

const invalidToastData: IToastDataState = {
id: toastId,
icon: 'ban',
title: TransactionsDefaultTitles.invalid,
hasCloseButton: true,
iconClassName: 'warning'
};

const timedOutToastData = {
id: toastId,
icon: 'times',
title: TransactionsDefaultTitles.timedOut,
hasCloseButton: true,
iconClassName: 'warning'
};

switch (status) {
case TransactionBatchStatusesEnum.signed:
case TransactionBatchStatusesEnum.sent:
return pendingToastData;
case TransactionBatchStatusesEnum.success:
return sender !== address ? receivedToastData : successToastData;
case TransactionBatchStatusesEnum.cancelled:
case TransactionBatchStatusesEnum.fail:
return failToastData;
case TransactionBatchStatusesEnum.timedOut:
return timedOutToastData;
case TransactionBatchStatusesEnum.invalid:
return invalidToastData;
default:
return pendingToastData;
}
};

private getToastProceededStatus = (transactions: SignedTransactionType[]) => {
const processedTransactions = transactions.filter(
(tx) =>
!isServerTransactionPending(tx.status as TransactionServerStatusesEnum)
).length;

const totalTransactions = transactions.length;

if (totalTransactions === 1 && processedTransactions === 1) {
return isServerTransactionPending(
transactions[0].status as TransactionServerStatusesEnum
)
? 'Processing transaction'
: 'Transaction processed';
}

return `${processedTransactions} / ${totalTransactions} transactions processed`;
};

public destroy() {
this.unsubscribe();
}
}
1 change: 1 addition & 0 deletions src/core/managers/ToastManager/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './toast.types';
49 changes: 49 additions & 0 deletions src/core/managers/ToastManager/types/toast.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
TransactionBatchStatusesEnum,
TransactionServerStatusesEnum
} from 'types';

export enum TransactionsDefaultTitles {
success = 'Transaction successful',
received = 'Transaction received',
failed = 'Transaction failed',
pending = 'Processing transaction',
timedOut = 'Transaction timed out',
invalid = 'Transaction invalid'
}

export interface GetToastsOptionsDataPropsType {
address: string;
sender: string;
status?: TransactionBatchStatusesEnum | TransactionServerStatusesEnum;
toastId: string;
}

export interface IToastDataState {
id: string;
icon: string;
hasCloseButton: boolean;
title: string;
iconClassName: string;
}
export interface ITransactionProgressState {
progressClass?: string;
currentRemaining: number;
}
export interface ITransaction {
hash: string;
status: string;
}
export interface ITransactionToast {
toastId: string;
wrapperClass?: string; // TODO: remove ?
processedTransactionsStatus: string;
transactions: ITransaction[];
toastDataState: IToastDataState;
transactionProgressState?: ITransactionProgressState;
}

export enum TransactionToastEventsEnum {
'CLOSE_TOAST' = 'CLOSE_TOAST',
'TRANSACTION_TOAST_DATA_UPDATE' = 'TRANSACTION_TOAST_DATA_UPDATE'
}
11 changes: 7 additions & 4 deletions src/core/managers/TransactionManager/TransactionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Transaction } from '@multiversx/sdk-core/out';
import axios, { AxiosError } from 'axios';
import { BATCH_TRANSACTIONS_ID_SEPARATOR } from 'constants/transactions.constants';
import { getAccount } from 'core/methods/account/getAccount';
import { addTransactionToast } from 'store/actions/toasts/toastsActions';
import { createTrackedTransactionsSession } from 'store/actions/trackedTransactions/trackedTransactionsActions';
import { networkSelector } from 'store/selectors';
import { getState } from 'store/store';
Expand Down Expand Up @@ -58,15 +59,17 @@ export class TransactionManager {

public track = async (
signedTransactions: Transaction[],
options: { enableToasts: boolean } = { enableToasts: true }
options: { disableToasts?: boolean } = { disableToasts: false }
) => {
const parsedTransactions = signedTransactions.map((transaction) =>
this.parseSignedTransaction(transaction)
);
createTrackedTransactionsSession({
transactions: parsedTransactions,
enableToasts: options.enableToasts
const sessionId = createTrackedTransactionsSession({
transactions: parsedTransactions
});
if (!options.disableToasts) {
addTransactionToast(sessionId);
}
};

private sendSignedTransactions = async (
Expand Down
2 changes: 2 additions & 0 deletions src/core/methods/initApp/initApp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { safeWindow } from 'constants/index';
import { ToastManager } from 'core/managers/ToastManager/ToastManager';
import { restoreProvider } from 'core/providers/helpers/restoreProvider';
import { ProviderFactory } from 'core/providers/ProviderFactory';
import { getDefaultNativeAuthConfig } from 'services/nativeAuth/methods/getDefaultNativeAuthConfig';
Expand Down Expand Up @@ -64,6 +65,7 @@ export async function initApp({
}

trackTransactions();
new ToastManager(); // TODO: change to something more clear

const isLoggedIn = getIsLoggedIn();

Expand Down
1 change: 1 addition & 0 deletions src/lib/sdkDappCoreUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type { LedgerConnectModal } from '@multiversx/sdk-dapp-core-ui/dist/compo
export type { SignTransactionsModal } from '@multiversx/sdk-dapp-core-ui/dist/components/sign-transactions-modal';
export type { WalletConnectModal } from '@multiversx/sdk-dapp-core-ui/dist/components/wallet-connect-modal';
export type { PendingTransactionsModal } from '@multiversx/sdk-dapp-core-ui/dist/components/pending-transactions-modal';
export type { TransactionToastList } from '@multiversx/sdk-dapp-core-ui/dist/components/transaction-toast-list';

export async function defineCustomElements(
win?: Window,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,15 @@ export interface UpdateTrackedTransactionStatusPayloadType {
}

export const createTrackedTransactionsSession = ({
transactions,
enableToasts = true
transactions
}: {
transactions: SignedTransactionType[];
enableToasts?: boolean;
}) => {
const sessionId = Date.now().toString();
getStore().setState(({ trackedTransactions: state }) => {
state[sessionId] = {
transactions,
status: TransactionBatchStatusesEnum.sent,
enableToasts
status: TransactionBatchStatusesEnum.sent
};
});
return sessionId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@ export interface TrackedTransactionsSliceType {
transactions: SignedTransactionType[];
status?: TransactionBatchStatusesEnum | TransactionServerStatusesEnum;
errorMessage?: string;
enableToasts?: boolean;
};
}

0 comments on commit 31c6ed4

Please sign in to comment.