diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9f245..e14121f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/core/managers/ToastManager/ToastManager.ts b/src/core/managers/ToastManager/ToastManager.ts new file mode 100644 index 0000000..fd8546f --- /dev/null +++ b/src/core/managers/ToastManager/ToastManager.ts @@ -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( + '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(); + } +} diff --git a/src/core/managers/ToastManager/types/index.ts b/src/core/managers/ToastManager/types/index.ts new file mode 100644 index 0000000..4ffb46a --- /dev/null +++ b/src/core/managers/ToastManager/types/index.ts @@ -0,0 +1 @@ +export * from './toast.types'; diff --git a/src/core/managers/ToastManager/types/toast.types.ts b/src/core/managers/ToastManager/types/toast.types.ts new file mode 100644 index 0000000..b8f19f9 --- /dev/null +++ b/src/core/managers/ToastManager/types/toast.types.ts @@ -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' +} diff --git a/src/core/managers/TransactionManager/TransactionManager.ts b/src/core/managers/TransactionManager/TransactionManager.ts index e627bc2..9a7540b 100644 --- a/src/core/managers/TransactionManager/TransactionManager.ts +++ b/src/core/managers/TransactionManager/TransactionManager.ts @@ -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'; @@ -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 ( diff --git a/src/core/methods/initApp/initApp.ts b/src/core/methods/initApp/initApp.ts index dbb972f..560c6ce 100644 --- a/src/core/methods/initApp/initApp.ts +++ b/src/core/methods/initApp/initApp.ts @@ -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'; @@ -64,6 +65,7 @@ export async function initApp({ } trackTransactions(); + new ToastManager(); // TODO: change to something more clear const isLoggedIn = getIsLoggedIn(); diff --git a/src/lib/sdkDappCoreUi.ts b/src/lib/sdkDappCoreUi.ts index 8cc3be8..c389f0a 100644 --- a/src/lib/sdkDappCoreUi.ts +++ b/src/lib/sdkDappCoreUi.ts @@ -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, diff --git a/src/store/actions/trackedTransactions/trackedTransactionsActions.ts b/src/store/actions/trackedTransactions/trackedTransactionsActions.ts index adadaaa..c174398 100644 --- a/src/store/actions/trackedTransactions/trackedTransactionsActions.ts +++ b/src/store/actions/trackedTransactions/trackedTransactionsActions.ts @@ -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; diff --git a/src/store/slices/trackedTransactions/trackedTransactionsSlice.types.ts b/src/store/slices/trackedTransactions/trackedTransactionsSlice.types.ts index 4801ac5..40802b2 100644 --- a/src/store/slices/trackedTransactions/trackedTransactionsSlice.types.ts +++ b/src/store/slices/trackedTransactions/trackedTransactionsSlice.types.ts @@ -9,6 +9,5 @@ export interface TrackedTransactionsSliceType { transactions: SignedTransactionType[]; status?: TransactionBatchStatusesEnum | TransactionServerStatusesEnum; errorMessage?: string; - enableToasts?: boolean; }; }