|
1 | 1 | import { useQueryClient } from '@tanstack/react-query' |
2 | | -import { useCallback, useEffect, useState } from 'react' |
| 2 | +import { useCallback, useEffect, useRef, useState } from 'react' |
3 | 3 | import { useTranslation } from 'react-i18next' |
4 | 4 | import styled, { css } from 'styled-components' |
| 5 | +import { useClient } from 'wagmi' |
5 | 6 |
|
6 | 7 | import { Button, Toast } from '@ensdomains/thorin' |
7 | 8 |
|
8 | 9 | import { useChainName } from '@app/hooks/chain/useChainName' |
9 | 10 | import { META_DATA_QUERY_KEY } from '@app/hooks/useEnsAvatar' |
10 | 11 | import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' |
| 12 | +import { ClientWithEns } from '@app/types' |
11 | 13 | import { useBreakpoint } from '@app/utils/BreakpointProvider' |
| 14 | +import { bustMediaCache } from '@app/utils/metadataCache' |
12 | 15 | import { UpdateCallback, useCallbackOnTransaction } from '@app/utils/SyncProvider/SyncProvider' |
13 | 16 | import { makeEtherscanLink } from '@app/utils/utils' |
14 | 17 |
|
@@ -36,40 +39,127 @@ export const TransactionNotifications = () => { |
36 | 39 |
|
37 | 40 | const chainName = useChainName() |
38 | 41 | const queryClient = useQueryClient() |
| 42 | + const client = useClient() |
39 | 43 |
|
40 | 44 | const [open, setOpen] = useState(false) |
41 | 45 |
|
42 | | - const { resumeTransactionFlow, getResumable } = useTransactionFlow() |
| 46 | + const { resumeTransactionFlow, getResumable, getLatestTransaction } = useTransactionFlow() |
43 | 47 |
|
44 | 48 | const [notificationQueue, setNotificationQueue] = useState<Notification[]>([]) |
45 | 49 | const currentNotification = notificationQueue[0] |
46 | 50 |
|
| 51 | + // Use ref to avoid recreating callback when state.items changes |
| 52 | + // This prevents cascading re-renders that can restart gas estimation |
| 53 | + const getLatestTransactionRef = useRef(getLatestTransaction) |
| 54 | + useEffect(() => { |
| 55 | + getLatestTransactionRef.current = getLatestTransaction |
| 56 | + }, [getLatestTransaction]) |
| 57 | + |
47 | 58 | const updateCallback = useCallback<UpdateCallback>( |
48 | 59 | ({ action, key, status, hash }) => { |
49 | 60 | if (status === 'pending' || status === 'repriced') return |
50 | 61 | if (status === 'confirmed') { |
| 62 | + // Get transaction data for cache busting (use ref to avoid dependency) |
| 63 | + const latestTx = key ? getLatestTransactionRef.current(key) : undefined |
| 64 | + const txData = latestTx?.data as { name?: string; records?: unknown } | undefined |
| 65 | + const ensName = txData?.name |
| 66 | + |
51 | 67 | switch (action) { |
52 | | - case 'registerName': |
| 68 | + case 'registerName': { |
53 | 69 | trackEvent('register', chainName) |
| 70 | + // Bust cache for avatar/header if being set during registration |
| 71 | + if (ensName && client) { |
| 72 | + const registrationData = txData as { |
| 73 | + name: string |
| 74 | + records?: { texts?: Array<{ key: string }> } |
| 75 | + } |
| 76 | + const hasAvatarChange = registrationData.records?.texts?.some( |
| 77 | + (t) => t.key === 'avatar', |
| 78 | + ) |
| 79 | + const hasHeaderChange = registrationData.records?.texts?.some( |
| 80 | + (t) => t.key === 'header', |
| 81 | + ) |
| 82 | + if (hasAvatarChange) |
| 83 | + bustMediaCache(ensName, client as ClientWithEns, 'avatar') |
| 84 | + if (hasHeaderChange) |
| 85 | + bustMediaCache(ensName, client as ClientWithEns, 'header') |
| 86 | + } |
54 | 87 | queryClient.invalidateQueries({ queryKey: [META_DATA_QUERY_KEY] }) |
55 | 88 | break |
| 89 | + } |
56 | 90 | case 'commitName': |
57 | 91 | trackEvent('commit', chainName) |
58 | 92 | break |
59 | 93 | case 'extendNames': |
60 | 94 | trackEvent('renew', chainName) |
61 | 95 | break |
62 | | - case 'updateProfileRecords': |
| 96 | + case 'updateResolver': |
| 97 | + case 'resetProfile': { |
| 98 | + // These actions always bust both avatar and header |
| 99 | + if (ensName && client) { |
| 100 | + bustMediaCache(ensName, client as ClientWithEns) |
| 101 | + } |
| 102 | + queryClient.invalidateQueries({ queryKey: [META_DATA_QUERY_KEY] }) |
| 103 | + break |
| 104 | + } |
63 | 105 | case 'updateProfile': |
64 | | - case 'resetProfile': |
| 106 | + case 'updateProfileRecords': { |
| 107 | + // Check if avatar or header are being modified in records |
| 108 | + if (ensName && client) { |
| 109 | + const profileData = txData as { |
| 110 | + name: string |
| 111 | + records?: |
| 112 | + | { texts?: Array<{ key: string }> } |
| 113 | + | Array<{ key: string; group?: string }> |
| 114 | + } |
| 115 | + // Handle both RecordOptions format and ProfileRecord[] format |
| 116 | + const records = profileData.records |
| 117 | + let hasAvatarChange = false |
| 118 | + let hasHeaderChange = false |
| 119 | + if (Array.isArray(records)) { |
| 120 | + // ProfileRecord[] format (updateProfileRecords) |
| 121 | + hasAvatarChange = records.some( |
| 122 | + (r) => r.key === 'avatar' && (r as { group?: string }).group === 'media', |
| 123 | + ) |
| 124 | + hasHeaderChange = records.some( |
| 125 | + (r) => r.key === 'header' && (r as { group?: string }).group === 'media', |
| 126 | + ) |
| 127 | + } else if (records?.texts) { |
| 128 | + // RecordOptions format (updateProfile) |
| 129 | + hasAvatarChange = records.texts.some((t) => t.key === 'avatar') |
| 130 | + hasHeaderChange = records.texts.some((t) => t.key === 'header') |
| 131 | + } |
| 132 | + if (hasAvatarChange) |
| 133 | + bustMediaCache(ensName, client as ClientWithEns, 'avatar') |
| 134 | + if (hasHeaderChange) |
| 135 | + bustMediaCache(ensName, client as ClientWithEns, 'header') |
| 136 | + } |
| 137 | + queryClient.invalidateQueries({ queryKey: [META_DATA_QUERY_KEY] }) |
| 138 | + break |
| 139 | + } |
65 | 140 | case 'resetProfileWithRecords': |
66 | 141 | case 'migrateProfile': |
67 | | - case 'migrateProfileWithReset': |
68 | | - case 'updateResolver': |
69 | | - // Cache busting now handled in transaction files |
70 | | - // Still need to invalidate queries to trigger refetch |
| 142 | + case 'migrateProfileWithReset': { |
| 143 | + // Check if avatar or header are in the records being set |
| 144 | + if (ensName && client) { |
| 145 | + const recordsData = txData as { |
| 146 | + name: string |
| 147 | + records?: { texts?: Array<{ key: string }> } |
| 148 | + } |
| 149 | + const hasAvatarChange = recordsData.records?.texts?.some( |
| 150 | + (t) => t.key === 'avatar', |
| 151 | + ) |
| 152 | + const hasHeaderChange = recordsData.records?.texts?.some( |
| 153 | + (t) => t.key === 'header', |
| 154 | + ) |
| 155 | + if (hasAvatarChange) |
| 156 | + bustMediaCache(ensName, client as ClientWithEns, 'avatar') |
| 157 | + if (hasHeaderChange) |
| 158 | + bustMediaCache(ensName, client as ClientWithEns, 'header') |
| 159 | + } |
71 | 160 | queryClient.invalidateQueries({ queryKey: [META_DATA_QUERY_KEY] }) |
72 | 161 | break |
| 162 | + } |
73 | 163 | default: |
74 | 164 | break |
75 | 165 | } |
@@ -106,7 +196,7 @@ export const TransactionNotifications = () => { |
106 | 196 |
|
107 | 197 | setNotificationQueue((queue) => [...queue, item]) |
108 | 198 | }, |
109 | | - [chainName, getResumable, resumeTransactionFlow, t, queryClient], |
| 199 | + [chainName, client, getResumable, resumeTransactionFlow, t, queryClient], |
110 | 200 | ) |
111 | 201 |
|
112 | 202 | useCallbackOnTransaction(updateCallback) |
|
0 commit comments