From f5e9d9dd73fd3dec239560b4b50592217bebc747 Mon Sep 17 00:00:00 2001 From: Razvan Tomegea Date: Mon, 11 Nov 2024 10:46:45 +0200 Subject: [PATCH 1/3] Ability to show transaction toast on demand (#1304) * Ability to show transaction toast on demand * Refactor * Added transaction received toast * CHANGELOG.md * Refactor * Refactor --- CHANGELOG.md | 2 + .../TransactionDetails/TransactionDetails.tsx | 27 +++++++++--- .../components/CustomToast/CustomToast.tsx | 22 ++++++++++ .../CustomToast/customToast.types.ts | 4 ++ .../TransactionToast/TransactionToast.tsx | 24 +++++------ .../hooks/useTransactionToast.ts | 9 ++-- .../TransactionToast/transactionToast.type.ts | 10 +++-- .../utils/getToastDataStateByStatus.ts | 26 +++++++++--- .../components/TransactionToastGuard.tsx | 1 + src/hooks/login/useLoginService.ts | 2 + .../useSignMultipleTransactions.tsx | 11 ++++- .../useSignTransactionsWithDevice.tsx | 7 +++- src/services/nativeAuth/nativeAuth.ts | 41 +++++++++++-------- src/types/enums.types.ts | 1 + src/types/toasts.types.ts | 8 +++- src/utils/account/signMessage.ts | 4 +- 16 files changed, 148 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b78f78d2d..46212678e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [Added ability to show transaction toast on demand](https://github.com/multiversx/mx-sdk-dapp/pull/1304) + ## [[v3.0.9](https://github.com/multiversx/mx-sdk-dapp/pull/1299)] - 2024-11-06 - [Fix clear initiated login](https://github.com/multiversx/mx-sdk-dapp/pull/1301) diff --git a/src/UI/TransactionDetails/TransactionDetails.tsx b/src/UI/TransactionDetails/TransactionDetails.tsx index e1b82d604..e42050db5 100644 --- a/src/UI/TransactionDetails/TransactionDetails.tsx +++ b/src/UI/TransactionDetails/TransactionDetails.tsx @@ -1,6 +1,10 @@ -import React, { useMemo, ReactNode } from 'react'; +import React, { ReactNode, useMemo } from 'react'; import { withStyles, WithStylesImportType } from 'hocs/withStyles'; -import { SignedTransactionType } from 'types/index'; +import { useGetAccount } from 'hooks/account/useGetAccount'; +import { + SignedTransactionType, + TransactionServerStatusesEnum +} from 'types/index'; import { isServerTransactionPending } from 'utils/transactions/transactionStateByStatus'; import { TransactionDetailsBody, @@ -25,14 +29,20 @@ const TransactionDetailsComponent = ({ return null; } + const { address } = useGetAccount(); + const processedTransactionsStatus = useMemo(() => { const processedTransactions = transactions.filter( - (tx) => !isServerTransactionPending(tx?.status) + (tx) => + !isServerTransactionPending(TransactionServerStatusesEnum[tx?.status]) ).length; + const totalTransactions = transactions.length; if (totalTransactions === 1 && processedTransactions === 1) { - return isServerTransactionPending(transactions[0].status) + return isServerTransactionPending( + TransactionServerStatusesEnum[transactions[0].status] + ) ? 'Processing transaction' : 'Transaction processed'; } @@ -40,17 +50,22 @@ const TransactionDetailsComponent = ({ return `${processedTransactions} / ${totalTransactions} transactions processed`; }, [transactions]); + const hideProcessedTransactionsStatus = + transactions.length === 1 && transactions[0].sender !== address; + return ( <> {title &&
{title}
} -
{processedTransactionsStatus}
+ {!hideProcessedTransactionsStatus && ( +
{processedTransactionsStatus}
+ )} {transactions.map(({ hash, status }) => { const transactionDetailsBodyProps: TransactionDetailsBodyPropsType = { className, hash, - status, + status: TransactionServerStatusesEnum[status], isTimedOut }; diff --git a/src/UI/TransactionsToastList/components/CustomToast/CustomToast.tsx b/src/UI/TransactionsToastList/components/CustomToast/CustomToast.tsx index 941e2b599..52a74dfbe 100644 --- a/src/UI/TransactionsToastList/components/CustomToast/CustomToast.tsx +++ b/src/UI/TransactionsToastList/components/CustomToast/CustomToast.tsx @@ -1,4 +1,10 @@ import React from 'react'; +import { + ServerTransactionType, + SignedTransactionType, + TransactionBatchStatusesEnum +} from 'types'; +import { TransactionToast } from '../TransactionToast'; import { IconToast, SimpleToast, CustomComponentToast } from './components'; import { CustomToastPropsType } from './customToast.types'; import { useRemoveCustomToast } from './helpers'; @@ -11,6 +17,22 @@ export const CustomToast = (props: CustomToastPropsType) => { return ; } + if (props.transaction) { + const serverTransaction = props.transaction as ServerTransactionType; + const signedTransaction = + props.transaction as unknown as SignedTransactionType; + + const transactionHash = serverTransaction.txHash || signedTransaction.hash; + + return ( + + ); + } + if (props.icon) { return ; } diff --git a/src/UI/TransactionsToastList/components/CustomToast/customToast.types.ts b/src/UI/TransactionsToastList/components/CustomToast/customToast.types.ts index 134d423c6..140b94f26 100644 --- a/src/UI/TransactionsToastList/components/CustomToast/customToast.types.ts +++ b/src/UI/TransactionsToastList/components/CustomToast/customToast.types.ts @@ -18,12 +18,16 @@ type SharedCustomToastPropsType = WithClassnameType & { export type MessageCustomToastPropsType = SharedCustomToastPropsType & MessageCustomToastType; + export type MessageIconToastPropsType = SharedCustomToastPropsType & MessageIconToastType; + export type TransactionIconToastPropsType = SharedCustomToastPropsType & TransactionIconToastType; + export type ComponentIconToastPropsType = SharedCustomToastPropsType & ComponentIconToastType; + export type CustomToastPropsType = | MessageCustomToastPropsType | MessageIconToastPropsType diff --git a/src/UI/TransactionsToastList/components/TransactionToast/TransactionToast.tsx b/src/UI/TransactionsToastList/components/TransactionToast/TransactionToast.tsx index 027fe89cc..b9b7bf1cb 100644 --- a/src/UI/TransactionsToastList/components/TransactionToast/TransactionToast.tsx +++ b/src/UI/TransactionsToastList/components/TransactionToast/TransactionToast.tsx @@ -18,17 +18,17 @@ export interface TransactionToastPropsType } const TransactionToastComponent = ({ - toastId, - title = '', className = 'dapp-transaction-toast', - onDelete, - startTimestamp, + customization, endTimeProgress, lifetimeAfterSuccess, + onDelete, + startTimestamp, status, - transactions, - customization, - styles + styles, + title = '', + toastId, + transactions }: TransactionToastPropsType & WithStylesImportType) => { const { isCrossShard, @@ -62,14 +62,14 @@ const TransactionToastComponent = ({ isCrossShard={isCrossShard} > diff --git a/src/UI/TransactionsToastList/components/TransactionToast/hooks/useTransactionToast.ts b/src/UI/TransactionsToastList/components/TransactionToast/hooks/useTransactionToast.ts index 3c8333738..f68fe87de 100644 --- a/src/UI/TransactionsToastList/components/TransactionToast/hooks/useTransactionToast.ts +++ b/src/UI/TransactionsToastList/components/TransactionToast/hooks/useTransactionToast.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef } from 'react'; import { AVERAGE_TX_DURATION_MS, CROSS_SHARD_ROUNDS } from 'constants/index'; import { useStyles } from 'hocs/useStyles'; -import { useGetTransactionDisplayInfo } from 'hooks'; +import { useGetAccount, useGetTransactionDisplayInfo } from 'hooks'; import { useSelector } from 'reduxStore/DappProviderContext'; import { shardSelector } from 'reduxStore/selectors'; import { getUnixTimestamp } from 'utils/dateTime/getUnixTimestamp'; @@ -37,6 +37,7 @@ export const useTransactionToast = ({ const transactionDisplayInfo = useGetTransactionDisplayInfo(toastId); const accountShard = useSelector(shardSelector); + const { address } = useGetAccount(); const timeoutRef = useRef(); const areSameShardTransactions = useMemo( () => getAreTransactionsOnSameShard(transactions, accountShard), @@ -71,10 +72,12 @@ export const useTransactionToast = ({ const isCompleted = isFailed || isSuccess || isTimedOut; const toastDataState = getToastDataStateByStatus({ + address, + classes: styles ?? {}, + sender: transactions?.[0].sender || address, status, toastId, - transactionDisplayInfo, - classes: styles ?? {} + transactionDisplayInfo }); const handleDeleteToast = () => { diff --git a/src/UI/TransactionsToastList/components/TransactionToast/transactionToast.type.ts b/src/UI/TransactionsToastList/components/TransactionToast/transactionToast.type.ts index 122d680b1..2db821fb8 100644 --- a/src/UI/TransactionsToastList/components/TransactionToast/transactionToast.type.ts +++ b/src/UI/TransactionsToastList/components/TransactionToast/transactionToast.type.ts @@ -1,14 +1,18 @@ import { FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; -import { SignedTransactionType, TransactionBatchStatusesEnum } from 'types'; +import { + SignedTransactionType, + TransactionBatchStatusesEnum, + TransactionServerStatusesEnum +} from 'types'; import { ProgressProps } from 'UI/Progress'; import { TransactionDetailsType } from 'UI/TransactionDetails'; import { ComponentTypeWithChildren } from '../types'; -import { TransactionToastContentProps } from './components/TransactionToastContent'; +import { TransactionToastContentProps } from './components'; export interface TransactionToastDefaultProps { toastId: string; transactions?: SignedTransactionType[]; - status?: TransactionBatchStatusesEnum; + status?: TransactionBatchStatusesEnum | TransactionServerStatusesEnum; classes?: Record; lifetimeAfterSuccess?: number; endTimeProgress?: number; diff --git a/src/UI/TransactionsToastList/components/TransactionToast/utils/getToastDataStateByStatus.ts b/src/UI/TransactionsToastList/components/TransactionToast/utils/getToastDataStateByStatus.ts index 650a01eb6..5597dd9f2 100644 --- a/src/UI/TransactionsToastList/components/TransactionToast/utils/getToastDataStateByStatus.ts +++ b/src/UI/TransactionsToastList/components/TransactionToast/utils/getToastDataStateByStatus.ts @@ -8,7 +8,8 @@ import { import { TransactionBatchStatusesEnum, TransactionsDefaultTitles, - TransactionsDisplayInfoType + TransactionsDisplayInfoType, + TransactionServerStatusesEnum } from 'types'; export interface ToastDataState { @@ -21,23 +22,27 @@ export interface ToastDataState { } interface GetToastsOptionsDataPropsType { - status?: TransactionBatchStatusesEnum; - toastId: string; + address: string; classes?: Record< 'success' | 'warning' | 'danger' | string, 'success' | 'warning' | 'danger' | string >; + sender: string; + status?: TransactionBatchStatusesEnum | TransactionServerStatusesEnum; + toastId: string; transactionDisplayInfo: TransactionsDisplayInfoType; } export const getToastDataStateByStatus = ({ - status, - toastId, + address, classes = { success: 'success', danger: 'danger', warning: 'warning' }, + sender, + status, + toastId, transactionDisplayInfo }: GetToastsOptionsDataPropsType) => { const successToastData: ToastDataState = { @@ -51,6 +56,15 @@ export const getToastDataStateByStatus = ({ iconClassName: classes.success }; + const receivedToastData: ToastDataState = { + id: toastId, + icon: faCheck, + expires: 30000, + hasCloseButton: true, + title: TransactionsDefaultTitles.received, + iconClassName: classes.success + }; + const pendingToastData: ToastDataState = { id: toastId, expires: false, @@ -96,7 +110,7 @@ export const getToastDataStateByStatus = ({ case TransactionBatchStatusesEnum.sent: return pendingToastData; case TransactionBatchStatusesEnum.success: - return successToastData; + return sender !== address ? receivedToastData : successToastData; case TransactionBatchStatusesEnum.cancelled: case TransactionBatchStatusesEnum.fail: return failToastData; diff --git a/src/UI/TransactionsToastList/components/TransactionToastGuard.tsx b/src/UI/TransactionsToastList/components/TransactionToastGuard.tsx index 9aaba94fe..41ea85f68 100644 --- a/src/UI/TransactionsToastList/components/TransactionToastGuard.tsx +++ b/src/UI/TransactionsToastList/components/TransactionToastGuard.tsx @@ -29,6 +29,7 @@ export const TransactionToastGuard = ({ const invalidCurrentTx = currentTx?.transactions == null || currentTx?.status == null; + if (invalidCurrentTx) { return null; } diff --git a/src/hooks/login/useLoginService.ts b/src/hooks/login/useLoginService.ts index 254504ec9..3d29504d8 100644 --- a/src/hooks/login/useLoginService.ts +++ b/src/hooks/login/useLoginService.ts @@ -96,6 +96,7 @@ export const useLoginService = (config?: OnProviderLoginType['nativeAuth']) => { ...(apiAddress ? { nativeAuthConfig: configuration } : {}) }) ); + return nativeAuthToken; }; @@ -119,6 +120,7 @@ export const useLoginService = (config?: OnProviderLoginType['nativeAuth']) => { }); tokenRef.current = loginToken; + if (!loginToken) { return; } diff --git a/src/hooks/transactions/useSignMultipleTransactions.tsx b/src/hooks/transactions/useSignMultipleTransactions.tsx index 484c1f743..2a488b3d3 100644 --- a/src/hooks/transactions/useSignMultipleTransactions.tsx +++ b/src/hooks/transactions/useSignMultipleTransactions.tsx @@ -183,6 +183,7 @@ export const useSignMultipleTransactions = ({ setWaitingForDevice(isLedger); let signedTx: Nullable; + try { signedTx = await onSignTransaction(currentTransaction.transaction); } catch (err) { @@ -205,6 +206,7 @@ export const useSignMultipleTransactions = ({ const newSignedTransactions = signedTransactions ? { ...signedTransactions, ...newSignedTx } : newSignedTx; + setSignedTransactions(newSignedTransactions); if (!isLastTransaction) { @@ -237,6 +239,7 @@ export const useSignMultipleTransactions = ({ if (currentTransaction == null) { return; } + const signature = currentTransaction.transaction.getSignature(); if (signature.toString('hex') && !isLastTransaction) { @@ -245,7 +248,8 @@ export const useSignMultipleTransactions = ({ } await sign(); - } catch { + } catch (e) { + console.error('Error during signing transaction'); // the only way to check if tx has signature is with try catch await sign(); } @@ -274,15 +278,18 @@ export const useSignMultipleTransactions = ({ setCurrentStep((exising) => exising + 1); return; } + await signTx(); }; const onNext = () => { setCurrentStep((current) => { const nextStep = current + 1; + if (nextStep > allTransactions?.length) { return current; } + return nextStep; }); }; @@ -290,9 +297,11 @@ export const useSignMultipleTransactions = ({ const onPrev = () => { setCurrentStep((current) => { const nextStep = current - 1; + if (nextStep < 0) { return current; } + return nextStep; }); }; diff --git a/src/hooks/transactions/useSignTransactionsWithDevice.tsx b/src/hooks/transactions/useSignTransactionsWithDevice.tsx index 27c6000f3..80d3cb776 100644 --- a/src/hooks/transactions/useSignTransactionsWithDevice.tsx +++ b/src/hooks/transactions/useSignTransactionsWithDevice.tsx @@ -160,7 +160,12 @@ export function useSignTransactionsWithDevice( if (!transaction) { return null; } - return await connectedProvider.signTransaction(transaction); + + const signedTransaction = await connectedProvider.signTransaction( + transaction + ); + + return signedTransaction; } const signMultipleTxReturnValues = useSignMultipleTransactions({ diff --git a/src/services/nativeAuth/nativeAuth.ts b/src/services/nativeAuth/nativeAuth.ts index 777a94a67..ca61f650e 100644 --- a/src/services/nativeAuth/nativeAuth.ts +++ b/src/services/nativeAuth/nativeAuth.ts @@ -42,25 +42,32 @@ export const nativeAuth = (config?: NativeAuthConfigType) => { const getBlockHash = (): Promise => nativeAuthClient.getCurrentBlockHash(); - const response = - initProps?.latestBlockInfo ?? - (await getLatestBlockHash( - apiAddress, - blockHashShard, - getBlockHash, - initProps?.noCache - )); - const { hash, timestamp } = response; - const encodedExtraInfo = nativeAuthClient.encodeValue( - JSON.stringify({ - ...(initProps?.extraInfo ?? extraInfoFromConfig), - ...(timestamp ? { timestamp } : {}) - }) - ); - const encodedOrigin = nativeAuthClient.encodeValue(origin); + try { + const response = + initProps?.latestBlockInfo ?? + (await getLatestBlockHash( + apiAddress, + blockHashShard, + getBlockHash, + initProps?.noCache + )); - return `${encodedOrigin}.${hash}.${expirySeconds}.${encodedExtraInfo}`; + const { hash, timestamp } = response; + const encodedExtraInfo = nativeAuthClient.encodeValue( + JSON.stringify({ + ...(initProps?.extraInfo ?? extraInfoFromConfig), + ...(timestamp ? { timestamp } : {}) + }) + ); + + const encodedOrigin = nativeAuthClient.encodeValue(origin); + + return `${encodedOrigin}.${hash}.${expirySeconds}.${encodedExtraInfo}`; + } catch (err) { + console.error('Error getting native auth token: ', err); + return ''; + } }; const getToken = ({ diff --git a/src/types/enums.types.ts b/src/types/enums.types.ts index 64476be1f..7612d0d8c 100644 --- a/src/types/enums.types.ts +++ b/src/types/enums.types.ts @@ -79,6 +79,7 @@ export enum TransactionTypesEnum { export enum TransactionsDefaultTitles { success = 'Transaction successful', + received = 'Transaction received', failed = 'Transaction failed', pending = 'Processing transaction', timedOut = 'Transaction timed out', diff --git a/src/types/toasts.types.ts b/src/types/toasts.types.ts index ffca78481..5f249e653 100644 --- a/src/types/toasts.types.ts +++ b/src/types/toasts.types.ts @@ -1,5 +1,6 @@ import { IconDefinition } from '@fortawesome/free-solid-svg-icons'; import { ServerTransactionType } from './serverTransactions.types'; +import { SignedTransactionType } from './transactions.types'; interface SharedCustomToast { toastId: string; @@ -60,7 +61,12 @@ export type CustomToastType = | TransactionIconToastType; export interface TransactionToastType { - toastId: string; + duration?: number; + icon?: IconDefinition; + iconClassName?: string; startTimestamp: number; + title?: string; + toastId: string; + transaction?: SignedTransactionType; type: string; } diff --git a/src/utils/account/signMessage.ts b/src/utils/account/signMessage.ts index 520b18906..342a5e6ff 100644 --- a/src/utils/account/signMessage.ts +++ b/src/utils/account/signMessage.ts @@ -38,7 +38,9 @@ export const signMessage = async ({ ); } - return await provider.signMessage(signableMessage, { + const signedMessage = await provider.signMessage(signableMessage, { callbackUrl: encodeURIComponent(callbackUrl) }); + + return signedMessage; }; From 42b3b160b3bf8e22866dacec1f0459875bff8437 Mon Sep 17 00:00:00 2001 From: "razvan.tomegea" Date: Mon, 11 Nov 2024 15:30:19 +0200 Subject: [PATCH 2/3] 3.0.10 --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46212678e..e841bbca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [[v3.0.10](https://github.com/multiversx/mx-sdk-dapp/pull/1305)] - 2024-11-11 + - [Added ability to show transaction toast on demand](https://github.com/multiversx/mx-sdk-dapp/pull/1304) ## [[v3.0.9](https://github.com/multiversx/mx-sdk-dapp/pull/1299)] - 2024-11-06 diff --git a/package.json b/package.json index 95361f8ac..1acc4a8b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@multiversx/sdk-dapp", - "version": "3.0.9", + "version": "3.0.10", "description": "A library to hold the main logic for a dapp on the MultiversX blockchain", "author": "MultiversX", "license": "GPL-3.0-or-later", From 4fa0be9ecc0c052a50100129711c1c3929f4159f Mon Sep 17 00:00:00 2001 From: "razvan.tomegea" Date: Mon, 11 Nov 2024 15:44:48 +0200 Subject: [PATCH 3/3] Fixed tests --- src/services/nativeAuth/nativeAuth.ts | 8 ++++++-- src/services/nativeAuth/tests/nativeAuth.test.ts | 6 ++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/services/nativeAuth/nativeAuth.ts b/src/services/nativeAuth/nativeAuth.ts index ca61f650e..cc376718d 100644 --- a/src/services/nativeAuth/nativeAuth.ts +++ b/src/services/nativeAuth/nativeAuth.ts @@ -53,6 +53,10 @@ export const nativeAuth = (config?: NativeAuthConfigType) => { initProps?.noCache )); + if (!response) { + return ''; + } + const { hash, timestamp } = response; const encodedExtraInfo = nativeAuthClient.encodeValue( JSON.stringify({ @@ -64,8 +68,8 @@ export const nativeAuth = (config?: NativeAuthConfigType) => { const encodedOrigin = nativeAuthClient.encodeValue(origin); return `${encodedOrigin}.${hash}.${expirySeconds}.${encodedExtraInfo}`; - } catch (err) { - console.error('Error getting native auth token: ', err); + } catch (err: any) { + console.error('Error getting native auth token: ', err.toString()); return ''; } }; diff --git a/src/services/nativeAuth/tests/nativeAuth.test.ts b/src/services/nativeAuth/tests/nativeAuth.test.ts index 3abc10136..855fef490 100644 --- a/src/services/nativeAuth/tests/nativeAuth.test.ts +++ b/src/services/nativeAuth/tests/nativeAuth.test.ts @@ -98,19 +98,21 @@ describe('Native Auth', () => { expect(token).toStrictEqual(TOKEN); }); - it('Internal server error', async () => { + it('should return empty string when API call fails', async () => { server.use(...handlers.serverError); //this will make sure to expire the cache jest .useFakeTimers() .setSystemTime(new Date().setSeconds(new Date().getSeconds() + 60)); + const client = nativeAuth({ origin: ORIGIN, apiAddress: API_URL }); - await expect(client.initialize()).rejects.toThrow(); + const nativeAuthToken = await client.initialize(); + expect(nativeAuthToken).toStrictEqual(''); }); it('Generate Access token', () => {