diff --git a/src/hooks/useLocalNotifications.tsx b/src/hooks/useLocalNotifications.tsx new file mode 100644 index 0000000..f85c1fd --- /dev/null +++ b/src/hooks/useLocalNotifications.tsx @@ -0,0 +1,87 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import { useQuery } from 'react-query'; + +import { LocalStorageKey } from '@/constants/localStorage'; +import { + type TransferNotifcation, +} from '@/constants/notifications'; + +import { useAccounts } from '@/hooks/useAccounts'; +import { useSquid } from '@/hooks/useSquid'; +import { useLocalStorage } from './useLocalStorage'; + +import { StatusResponse } from '@0xsquid/sdk'; + +export const useLocalNotifications = () => { + + // transfer notifications + const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{[key: `dydx${string}`]: TransferNotifcation[]}>({ + key: LocalStorageKey.TransferNotifications, + defaultValue: {}, + }); + + const { dydxAddress } = useAccounts(); + + const transferNotifications = useMemo(() => { + if (!dydxAddress) return []; + console.log({allTransferNotifications, transferNotifications: allTransferNotifications[dydxAddress]}) + return allTransferNotifications[dydxAddress] || []; + }, [allTransferNotifications, dydxAddress]); + + + const setTransferNotifications = useCallback( + (notifications: TransferNotifcation[]) => { + if (!dydxAddress) return; + const updatedNotifications = { ...allTransferNotifications }; + updatedNotifications[dydxAddress] = notifications; + setAllTransferNotifications(updatedNotifications); + }, + [setAllTransferNotifications, dydxAddress] + ); + + const addTransferNotification = useCallback( + (notification: TransferNotifcation) => + setTransferNotifications([...transferNotifications, notification]), + [transferNotifications] + ); + + const squid = useSquid(); + + const { data: transferStatuses } = useQuery({ + queryKey: ['getTransactionStatus', transferNotifications], + queryFn: async () => { + const statuses: { [key: string]: StatusResponse } = {}; + for (const { + txHash, + toChainId, + fromChainId, + status: currentStatus, + } of transferNotifications) { + if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue; + + const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId }); + if (status) statuses[txHash] = status; + } + return statuses; + }, + refetchInterval: 10_000, + }); + + useEffect(() => { + if (!transferStatuses) return; + const newTransferNotifications = transferNotifications.map((notification) => { + const status = transferStatuses[notification.txHash]; + if (!status) return notification; + return { + ...notification, + status, + }; + }); + setTransferNotifications(newTransferNotifications); + }, [transferStatuses]); + + return { + transferNotifications, + addTransferNotification, + }; +}; diff --git a/src/hooks/useNotificationTypes.tsx b/src/hooks/useNotificationTypes.tsx index cea23f5..6fd9dd5 100644 --- a/src/hooks/useNotificationTypes.tsx +++ b/src/hooks/useNotificationTypes.tsx @@ -12,6 +12,8 @@ import { } from '@/constants/notifications'; import { ORDER_SIDE_STRINGS, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade'; +import { useLocalNotifications } from '@/hooks/useLocalNotifications'; + import { Icon, IconName } from '@/components/Icon'; import { TransferStatusToast } from '@/views/TransferStatus'; @@ -28,123 +30,124 @@ import { useStringGetter } from './useStringGetter'; import { TransferStatusSteps } from '@/views/TransferStatusSteps'; import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client'; -export const notificationTypes = (transferNotifications: TransferNotifcation[]) => - [ - { - type: NotificationType.OrderStatusChanged, +export const notificationTypes = [ + { + type: NotificationType.OrderStatusChanged, - useTrigger: ({ trigger, lastUpdated }) => { - const stringGetter = useStringGetter(); + useTrigger: ({ trigger, lastUpdated }) => { + const stringGetter = useStringGetter(); - const orders = useSelector(getSubaccountOrders, shallowEqual) || []; - const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order])); + const orders = useSelector(getSubaccountOrders, shallowEqual) || []; + const ordersByOrderId = Object.fromEntries(orders.map((order) => [order.id, order])); - const fills = useSelector(getSubaccountFills, shallowEqual) || []; - const fillsByOrderId = groupBy(fills, (fill) => fill.orderId); + const fills = useSelector(getSubaccountFills, shallowEqual) || []; + const fillsByOrderId = groupBy(fills, (fill) => fill.orderId); - const orderIds = useMemo( - () => [...Object.keys(ordersByOrderId), ...Object.keys(fillsByOrderId)], - [orders, fills] - ); + const orderIds = useMemo( + () => [...Object.keys(ordersByOrderId), ...Object.keys(fillsByOrderId)], + [orders, fills] + ); - useEffect(() => { - for (const orderId of orderIds) { - const fills = fillsByOrderId[orderId]; + useEffect(() => { + for (const orderId of orderIds) { + const fills = fillsByOrderId[orderId]; - const order = - ordersByOrderId[orderId] ?? - (fills?.length - ? { - ...fills[fills.length - 1], - id: orderId, - createdAtMilliseconds: Math.max( - ...fills.map((fill) => fill.createdAtMilliseconds) - ), - status: AbacusOrderStatus.filled, - } - : undefined); - - if (order) - trigger( - order.id, - { - icon: ( - + const order = + ordersByOrderId[orderId] ?? + (fills?.length + ? { + ...fills[fills.length - 1], + id: orderId, + createdAtMilliseconds: Math.max( + ...fills.map((fill) => fill.createdAtMilliseconds) ), - title: `${stringGetter({ - key: TRADE_TYPE_STRINGS[order.type.rawValue as TradeTypes].tradeTypeKey, - })} ${ - order.status === AbacusOrderStatus.open && order.totalFilled > 0 - ? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED }) - : stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] }) - }`, - description: `${stringGetter({ - key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]], - })} ${order.size} ${order.marketId} @ $${order.price}`, - actionDescription: 'View Order', - actionAltText: 'View this order in the Orders tab or the Notifications menu.', - toastSensitivity: - order.status === AbacusOrderStatus.pending ? 'foreground' : 'background', - toastDuration: 5000, - }, - [order.status.name, order.size], - !order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated - ); - } - }, [orderIds]); - }, - - useNotificationAction: () => { - const dispatch = useDispatch(); - - return (orderId) => { - dispatch( - openDialog({ - type: DialogTypes.OrderDetails, - dialogProps: { orderId }, - }) - ); - }; - }, - } as NotificationTypeConfig, - { - type: NotificationType.SquidTransfer, - useTrigger: ({ trigger, lastUpdated }) => { - const stringGetter = useStringGetter(); - - const getTitleStringKey = useCallback((type: 'deposit' | 'withdraw', finished: boolean) => { - if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS; - if (type === 'deposit' && finished) return STRING_KEYS.DEPOSIT; - if (type === 'withdraw' && !finished) return STRING_KEYS.WITHDRAW_IN_PROGRESS; - return STRING_KEYS.WITHDRAW; - }, []); - - useEffect(() => { - for (const transfer of transferNotifications) { - const { toChainId, status, txHash, toAmount } = transfer; - const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing'; - const type = toChainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdraw'; + status: AbacusOrderStatus.filled, + } + : undefined); + if (order) trigger( - txHash, + order.id, { - icon: , - title: stringGetter({ key: getTitleStringKey(type, finished) }), - description: type === 'deposit' ? `Deposit of $${toAmount}` : `Withdraw of ${toAmount}`, - customContent: ( - + icon: ( + ), - customMenuContent: !finished && , - toastSensitivity: 'foreground', + title: `${stringGetter({ + key: TRADE_TYPE_STRINGS[order.type.rawValue as TradeTypes].tradeTypeKey, + })} ${ + order.status === AbacusOrderStatus.open && order.totalFilled > 0 + ? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED }) + : stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] }) + }`, + description: `${stringGetter({ + key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]], + })} ${order.size} ${order.marketId} @ $${order.price}`, + actionDescription: 'View Order', + actionAltText: 'View this order in the Orders tab or the Notifications menu.', + toastSensitivity: + order.status === AbacusOrderStatus.pending ? 'foreground' : 'background', + toastDuration: 5000, }, - [] + [order.status.name, order.size], + !order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated ); - } - }, [transferNotifications]); - }, + } + }, [orderIds]); }, - ] satisfies NotificationTypeConfig[]; + + useNotificationAction: () => { + const dispatch = useDispatch(); + + return (orderId) => { + dispatch( + openDialog({ + type: DialogTypes.OrderDetails, + dialogProps: { orderId }, + }) + ); + }; + }, + } as NotificationTypeConfig, + { + type: NotificationType.SquidTransfer, + useTrigger: ({ trigger, lastUpdated }) => { + const stringGetter = useStringGetter(); + const { transferNotifications } = useLocalNotifications(); + + const getTitleStringKey = useCallback((type: 'deposit' | 'withdraw', finished: boolean) => { + if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS; + if (type === 'deposit' && finished) return STRING_KEYS.DEPOSIT; + if (type === 'withdraw' && !finished) return STRING_KEYS.WITHDRAW_IN_PROGRESS; + return STRING_KEYS.WITHDRAW; + }, []); + + useEffect(() => { + for (const transfer of transferNotifications) { + const { toChainId, status, txHash, toAmount } = transfer; + const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing'; + const type = toChainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdraw'; + + trigger( + txHash, + { + icon: , + title: stringGetter({ key: getTitleStringKey(type, finished) }), + description: + type === 'deposit' ? `Deposit of $${toAmount}` : `Withdraw of ${toAmount}`, + customContent: ( + + ), + customMenuContent: !finished && , + toastSensitivity: 'foreground', + }, + [] + ); + } + }, [transferNotifications]); + }, + }, +] satisfies NotificationTypeConfig[]; diff --git a/src/hooks/useNotifications.tsx b/src/hooks/useNotifications.tsx index c4677f6..6e2ed47 100644 --- a/src/hooks/useNotifications.tsx +++ b/src/hooks/useNotifications.tsx @@ -12,7 +12,7 @@ import { import { useSquid } from '@/hooks/useSquid'; import { useLocalStorage } from './useLocalStorage'; -import { notificationTypes as notificationTypesFactory } from './useNotificationTypes'; +import { notificationTypes } from './useNotificationTypes'; import { renderSvgToDataUrl } from '../lib/renderSvgToDataUrl'; import { StatusResponse } from '@0xsquid/sdk'; @@ -41,58 +41,6 @@ const useNotificationsContext = () => { defaultValue: Date.now(), }); - // transfer notifications - const [transferNotifications, setTransferNotifications] = useLocalStorage({ - key: LocalStorageKey.TransferNotifications, - defaultValue: [], - }); - - const addTransferNotification = useCallback( - (notification: TransferNotifcation) => - setTransferNotifications([...transferNotifications, notification]), - [transferNotifications] - ); - - const notificationTypes = useMemo( - () => notificationTypesFactory(transferNotifications), - [transferNotifications] - ); - - const squid = useSquid(); - - const { data: transferStatuses } = useQuery({ - queryKey: ['getTransactionStatus', transferNotifications], - queryFn: async () => { - const statuses: { [key: string]: StatusResponse } = {}; - for (const { - txHash, - toChainId, - fromChainId, - status: currentStatus, - } of transferNotifications) { - if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue; - - const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId }); - if (status) statuses[txHash] = status; - } - return statuses; - }, - refetchInterval: 10_000, - }); - - useEffect(() => { - if (!transferStatuses) return; - const newTransferNotifications = transferNotifications.map((notification) => { - const status = transferStatuses[notification.txHash]; - if (!status) return notification; - return { - ...notification, - status, - }; - }); - setTransferNotifications(newTransferNotifications); - }, [transferStatuses]); - useEffect(() => { setNotificationsLastUpdated(Date.now()); }, [notifications]); @@ -295,8 +243,5 @@ const useNotificationsContext = () => { // Menu state isMenuOpen, setIsMenuOpen, - - addTransferNotification, }; }; -// } diff --git a/src/localization/en/app.json b/src/localization/en/app.json index 6cd2781..5a6ba71 100644 --- a/src/localization/en/app.json +++ b/src/localization/en/app.json @@ -922,7 +922,7 @@ "CLOSE_MARKET_POSITIONS_BODY": "{MARKET} is set to close only mode and the close price of {MARKET} is now fixed. Please close your open {MARKET} position as soon as you are able. We're here to help via the help chat if you run into issues.", "COMPLIANCE_ALERT": "Compliance alert", "DEPOSIT_IN_PROGRESS_DESCRIPTION": "Your deposit of {AMOUNT_ELEMENT} will be available after 14 confirmations.", - "DEPOSIT_IN_PROGRESS": "Deposit in progress", + "DEPOSIT_IN_PROGRESS": "Deposit in progress...", "DEPOSIT_SUCCESS_DESCRIPTION": "Your deposit of {AMOUNT_ELEMENT} has been confirmed and is now available for trading.", "DEPOSIT_SUCCESS": "Deposit success!", "DEPOSIT_TO_CHAIN": "Deposit to {CHAIN}", @@ -952,7 +952,7 @@ "SLOW_WITHDRAW_PENDING": "Slow withdraw(s) pending", "SUSPICIOUS_TRADE": "Suspicious activity", "SUSPICIOUS_TRADE_BODY": "We have noticed suspicious trading activity related to your account on dYdX, including potential wash trading activity. We will be reviewing your activity on an ongoing basis. If we notice similar activity in the future, then you will be permanently blocked from performing transfers within the protocol and from placing orders other than market orders that reduce your positions.", - "WITHDRAW_IN_PROGRESS": "Withdrawal(s) in progress", + "WITHDRAW_IN_PROGRESS": "Withdrawal(s) in progress...", "WITHDRAW_TO_CHAIN": "Withdraw to {CHAIN}" }, "EMAIL_NOTIFICATIONS": { diff --git a/src/views/forms/AccountManagementForms/DepositForm.tsx b/src/views/forms/AccountManagementForms/DepositForm.tsx index fd48b66..f62d3aa 100644 --- a/src/views/forms/AccountManagementForms/DepositForm.tsx +++ b/src/views/forms/AccountManagementForms/DepositForm.tsx @@ -13,7 +13,7 @@ import { NumberSign } from '@/constants/numbers'; import { useAccounts, useDebounce, useStringGetter } from '@/hooks'; import { useAccountBalance } from '@/hooks/useAccountBalance'; -import { useNotifications } from '@/hooks/useNotifications'; +import { useLocalNotifications } from '@/hooks/useLocalNotifications'; import { layoutMixins } from '@/styles/layoutMixins'; import { formMixins } from '@/styles/formMixins'; @@ -52,7 +52,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { const { signerWagmi } = useAccounts(); - const { addTransferNotification } = useNotifications(); + const { addTransferNotification } = useLocalNotifications(); const { requestPayload, @@ -74,8 +74,6 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { const debouncedAmount = useDebounce(fromAmount, 500); // Async Data - const [transactionHash, setTransactionHash] = useState(); - const { balance, queryStatus, isQueryFetching } = useAccountBalance({ addressOrDenom: sourceToken?.address || undefined, assetSymbol: sourceToken?.symbol || undefined, @@ -194,7 +192,6 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => { onDeposit?.(); if (txHash) { - setTransactionHash(txHash); addTransferNotification({ txHash, toChainId: TESTNET_CHAIN_ID, diff --git a/src/views/forms/AccountManagementForms/WithdrawForm.tsx b/src/views/forms/AccountManagementForms/WithdrawForm.tsx index 5bbd320..d2247cb 100644 --- a/src/views/forms/AccountManagementForms/WithdrawForm.tsx +++ b/src/views/forms/AccountManagementForms/WithdrawForm.tsx @@ -16,7 +16,7 @@ import { STRING_KEYS } from '@/constants/localization'; import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers'; import { useDebounce, useStringGetter, useSubaccount } from '@/hooks'; -import { useNotifications } from '@/hooks/useNotifications'; +import { useLocalNotifications } from '@/hooks/useLocalNotifications'; import { layoutMixins } from '@/styles/layoutMixins'; @@ -67,7 +67,7 @@ export const WithdrawForm = () => { [token, resources] ); - const { addTransferNotification } = useNotifications(); + const { addTransferNotification } = useLocalNotifications(); // Async Data const [transactionHash, setTransactionHash] = useState();