|
1 | | -import React, { useEffect, useMemo, useState } from 'react'; |
| 1 | +import React, { |
| 2 | + useCallback, |
| 3 | + useEffect, |
| 4 | + useMemo, |
| 5 | + useRef, |
| 6 | + useState, |
| 7 | +} from 'react'; |
2 | 8 | import { useNavigate, useParams } from 'react-router-dom-v5-compat'; |
3 | 9 | import { useSelector } from 'react-redux'; |
4 | 10 | import { Hex } from '@metamask/utils'; |
@@ -58,7 +64,38 @@ export const ReviewGatorPermissionsPage = ({ |
58 | 64 | const [, evmNetworks] = useSelector( |
59 | 65 | getMultichainNetworkConfigurationsByChainId, |
60 | 66 | ); |
61 | | - const [totalGatorPermissions, setTotalGatorPermissions] = useState(0); |
| 67 | + const [pendingRevokeClicks, setPendingRevokeClicks] = useState<Set<string>>( |
| 68 | + new Set(), |
| 69 | + ); |
| 70 | + const revokeTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>( |
| 71 | + new Map(), |
| 72 | + ); |
| 73 | + |
| 74 | + // Cleanup all pending timeouts on unmount |
| 75 | + useEffect(() => { |
| 76 | + const timeouts = revokeTimeoutsRef.current; |
| 77 | + return () => { |
| 78 | + timeouts.forEach((timeout) => clearTimeout(timeout)); |
| 79 | + timeouts.clear(); |
| 80 | + }; |
| 81 | + }, []); |
| 82 | + |
| 83 | + // Helper functions for managing pending state |
| 84 | + const addPendingContext = useCallback((context: string) => { |
| 85 | + setPendingRevokeClicks((prev) => { |
| 86 | + const next = new Set(prev); |
| 87 | + next.add(context); |
| 88 | + return next; |
| 89 | + }); |
| 90 | + }, []); |
| 91 | + |
| 92 | + const removePendingContext = useCallback((context: string) => { |
| 93 | + setPendingRevokeClicks((prev) => { |
| 94 | + const next = new Set(prev); |
| 95 | + next.delete(context); |
| 96 | + return next; |
| 97 | + }); |
| 98 | + }, []); |
62 | 99 |
|
63 | 100 | const networkName: string = useMemo(() => { |
64 | 101 | if (!chainId) { |
@@ -89,39 +126,65 @@ export const ReviewGatorPermissionsPage = ({ |
89 | 126 | chainId: (chainId ?? '') as Hex, |
90 | 127 | }); |
91 | 128 |
|
92 | | - useEffect(() => { |
93 | | - setTotalGatorPermissions(gatorPermissions.length); |
94 | | - }, [chainId, gatorPermissions]); |
| 129 | + const handleRevokeClick = useCallback( |
| 130 | + async ( |
| 131 | + permission: StoredGatorPermissionSanitized< |
| 132 | + Signer, |
| 133 | + PermissionTypesWithCustom |
| 134 | + >, |
| 135 | + ) => { |
| 136 | + const { context } = permission.permissionResponse; |
95 | 137 |
|
96 | | - const handleRevokeClick = async ( |
97 | | - permission: StoredGatorPermissionSanitized< |
98 | | - Signer, |
99 | | - PermissionTypesWithCustom |
100 | | - >, |
101 | | - ) => { |
102 | | - try { |
103 | | - await revokeGatorPermission(permission); |
104 | | - } catch (error) { |
105 | | - console.error('Error revoking gator permission:', error); |
106 | | - } |
107 | | - }; |
| 138 | + // Set pending state immediately to disable button and show "Pending..." text |
| 139 | + addPendingContext(context); |
| 140 | + |
| 141 | + try { |
| 142 | + await revokeGatorPermission(permission); |
| 143 | + |
| 144 | + // Clear any existing timeout for this context |
| 145 | + const existingTimeout = revokeTimeoutsRef.current.get(context); |
| 146 | + clearTimeout(existingTimeout); |
| 147 | + revokeTimeoutsRef.current.delete(context); |
| 148 | + |
| 149 | + // Delay clearing to prevent visual flash before transaction window shows |
| 150 | + const timeoutId = setTimeout(() => { |
| 151 | + removePendingContext(context); |
| 152 | + revokeTimeoutsRef.current.delete(context); |
| 153 | + }, 800); // 800ms delay to prevent visual flash before transaction window shows |
| 154 | + |
| 155 | + revokeTimeoutsRef.current.set(context, timeoutId); |
| 156 | + } catch (error) { |
| 157 | + console.error('Error revoking gator permission:', error); |
| 158 | + |
| 159 | + // Clean up any pending timeout |
| 160 | + const existingTimeout = revokeTimeoutsRef.current.get(context); |
| 161 | + clearTimeout(existingTimeout); |
| 162 | + revokeTimeoutsRef.current.delete(context); |
| 163 | + |
| 164 | + // Clear pending state immediately on error |
| 165 | + removePendingContext(context); |
| 166 | + } |
| 167 | + }, |
| 168 | + [revokeGatorPermission, addPendingContext, removePendingContext], |
| 169 | + ); |
108 | 170 |
|
109 | 171 | const renderGatorPermissions = ( |
110 | 172 | permissions: StoredGatorPermissionSanitized< |
111 | 173 | Signer, |
112 | 174 | PermissionTypesWithCustom |
113 | 175 | >[], |
114 | 176 | ) => |
115 | | - permissions.map((permission) => { |
116 | | - return ( |
117 | | - <ReviewGatorPermissionItem |
118 | | - key={`${permission.siteOrigin}-${permission.permissionResponse.context}`} |
119 | | - networkName={networkName} |
120 | | - gatorPermission={permission} |
121 | | - onRevokeClick={() => handleRevokeClick(permission)} |
122 | | - /> |
123 | | - ); |
124 | | - }); |
| 177 | + permissions.map((permission) => ( |
| 178 | + <ReviewGatorPermissionItem |
| 179 | + key={`${permission.siteOrigin}-${permission.permissionResponse.context}`} |
| 180 | + networkName={networkName} |
| 181 | + gatorPermission={permission} |
| 182 | + onRevokeClick={() => handleRevokeClick(permission)} |
| 183 | + isPendingRevokeClick={pendingRevokeClicks.has( |
| 184 | + permission.permissionResponse.context, |
| 185 | + )} |
| 186 | + /> |
| 187 | + )); |
125 | 188 |
|
126 | 189 | return ( |
127 | 190 | <Page |
@@ -151,7 +214,7 @@ export const ReviewGatorPermissionsPage = ({ |
151 | 214 | </Text> |
152 | 215 | </Header> |
153 | 216 | <Content padding={0}> |
154 | | - {totalGatorPermissions > 0 ? ( |
| 217 | + {gatorPermissions.length > 0 ? ( |
155 | 218 | renderGatorPermissions(gatorPermissions) |
156 | 219 | ) : ( |
157 | 220 | <Box |
|
0 commit comments