diff --git a/ui/components/multichain/pages/gator-permissions/components/review-gator-permission-item.tsx b/ui/components/multichain/pages/gator-permissions/components/review-gator-permission-item.tsx index 2715eef06e43..1b298b2c453d 100644 --- a/ui/components/multichain/pages/gator-permissions/components/review-gator-permission-item.tsx +++ b/ui/components/multichain/pages/gator-permissions/components/review-gator-permission-item.tsx @@ -73,6 +73,11 @@ type ReviewGatorPermissionItemProps = { * The function to call when the revoke is clicked */ onRevokeClick: () => void; + + /** + * Whether this permission has a pending revoke click (temporary UI state) + */ + hasRevokeBeenClicked?: boolean; }; type PermissionExpandedDetails = Record< @@ -102,6 +107,7 @@ export const ReviewGatorPermissionItem = ({ networkName, gatorPermission, onRevokeClick, + hasRevokeBeenClicked = false, }: ReviewGatorPermissionItemProps) => { const t = useI18nContext(); const { permissionResponse, siteOrigin } = gatorPermission; @@ -151,10 +157,13 @@ export const ReviewGatorPermissionItem = ({ }, [tokensByChain, chainId, tokenAddress, nativeTokenMetadata]); const isPendingRevocation = useMemo(() => { - return pendingRevocations.some( - (revocation) => revocation.permissionContext === permissionContext, + return ( + hasRevokeBeenClicked || + pendingRevocations.some( + (revocation) => revocation.permissionContext === permissionContext, + ) ); - }, [pendingRevocations, permissionContext]); + }, [pendingRevocations, permissionContext, hasRevokeBeenClicked]); /** * Handles the click event for the expand/collapse button diff --git a/ui/components/multichain/pages/gator-permissions/review-permissions/review-gator-permissions-page.tsx b/ui/components/multichain/pages/gator-permissions/review-permissions/review-gator-permissions-page.tsx index ccded1f527f1..7344520d119a 100644 --- a/ui/components/multichain/pages/gator-permissions/review-permissions/review-gator-permissions-page.tsx +++ b/ui/components/multichain/pages/gator-permissions/review-permissions/review-gator-permissions-page.tsx @@ -1,4 +1,10 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { useNavigate, useParams } from 'react-router-dom-v5-compat'; import { useSelector } from 'react-redux'; import { Hex } from '@metamask/utils'; @@ -65,7 +71,38 @@ export const ReviewGatorPermissionsPage = ({ const [, evmNetworks] = useSelector( getMultichainNetworkConfigurationsByChainId, ); - const [totalGatorPermissions, setTotalGatorPermissions] = useState(0); + const [pendingRevokeClicks, setPendingRevokeClicks] = useState>( + new Set(), + ); + const revokeTimeoutsRef = useRef>>( + new Map(), + ); + + // Cleanup all pending timeouts on unmount + useEffect(() => { + const timeouts = revokeTimeoutsRef.current; + return () => { + timeouts.forEach((timeout) => clearTimeout(timeout)); + timeouts.clear(); + }; + }, []); + + // Helper functions for managing pending state + const addPendingContext = useCallback((context: string) => { + setPendingRevokeClicks((prev) => { + const next = new Set(prev); + next.add(context); + return next; + }); + }, []); + + const removePendingContext = useCallback((context: string) => { + setPendingRevokeClicks((prev) => { + const next = new Set(prev); + next.delete(context); + return next; + }); + }, []); const networkName: string = useMemo(() => { const networkNameKey = extractNetworkName(evmNetworks, chainId as Hex); @@ -100,22 +137,42 @@ export const ReviewGatorPermissionsPage = ({ chainId: chainId as Hex, }); - useEffect(() => { - setTotalGatorPermissions(gatorPermissions.length); - }, [chainId, gatorPermissions]); + const handleRevokeClick = useCallback( + async ( + permission: StoredGatorPermissionSanitized< + Signer, + PermissionTypesWithCustom + >, + ) => { + const { context } = permission.permissionResponse; - const handleRevokeClick = async ( - permission: StoredGatorPermissionSanitized< - Signer, - PermissionTypesWithCustom - >, - ) => { - try { - await revokeGatorPermission(permission); - } catch (error) { - console.error('Error revoking gator permission:', error); - } - }; + // Set pending state immediately to disable button and show "Pending..." text + addPendingContext(context); + + try { + await revokeGatorPermission(permission); + + // Delay clearing to prevent visual flash before transaction window shows + const timeoutId = setTimeout(() => { + removePendingContext(context); + revokeTimeoutsRef.current.delete(context); + }, 800); // 800ms delay to prevent visual flash before transaction window shows + + revokeTimeoutsRef.current.set(context, timeoutId); + } catch (error) { + console.error('Error revoking gator permission:', error); + + // Clean up any pending timeout + const existingTimeout = revokeTimeoutsRef.current.get(context); + clearTimeout(existingTimeout); + revokeTimeoutsRef.current.delete(context); + + // Clear pending state immediately on error + removePendingContext(context); + } + }, + [revokeGatorPermission, addPendingContext, removePendingContext], + ); const renderGatorPermissions = ( permissions: StoredGatorPermissionSanitized< @@ -123,16 +180,17 @@ export const ReviewGatorPermissionsPage = ({ PermissionTypesWithCustom >[], ) => - permissions.map((permission) => { - return ( - handleRevokeClick(permission)} - /> - ); - }); + permissions.map((permission) => ( + handleRevokeClick(permission)} + hasRevokeBeenClicked={pendingRevokeClicks.has( + permission.permissionResponse.context, + )} + /> + )); return ( - {totalGatorPermissions > 0 ? ( + {gatorPermissions.length > 0 ? ( renderGatorPermissions(gatorPermissions) ) : (