Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down Expand Up @@ -102,6 +107,7 @@ export const ReviewGatorPermissionItem = ({
networkName,
gatorPermission,
onRevokeClick,
hasRevokeBeenClicked = false,
}: ReviewGatorPermissionItemProps) => {
const t = useI18nContext();
const { permissionResponse, siteOrigin } = gatorPermission;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -65,7 +71,38 @@ export const ReviewGatorPermissionsPage = ({
const [, evmNetworks] = useSelector(
getMultichainNetworkConfigurationsByChainId,
);
const [totalGatorPermissions, setTotalGatorPermissions] = useState(0);
const [pendingRevokeClicks, setPendingRevokeClicks] = useState<Set<string>>(
new Set(),
);
const revokeTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(
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);
Expand Down Expand Up @@ -100,39 +137,60 @@ 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<
Signer,
PermissionTypesWithCustom
>[],
) =>
permissions.map((permission) => {
return (
<ReviewGatorPermissionItem
key={`${permission.siteOrigin}-${permission.permissionResponse.context}`}
networkName={networkName}
gatorPermission={permission}
onRevokeClick={() => handleRevokeClick(permission)}
/>
);
});
permissions.map((permission) => (
<ReviewGatorPermissionItem
key={`${permission.siteOrigin}-${permission.permissionResponse.context}`}
networkName={networkName}
gatorPermission={permission}
onRevokeClick={() => handleRevokeClick(permission)}
hasRevokeBeenClicked={pendingRevokeClicks.has(
permission.permissionResponse.context,
)}
/>
));

return (
<Page
Expand Down Expand Up @@ -162,7 +220,7 @@ export const ReviewGatorPermissionsPage = ({
</Text>
</Header>
<Content padding={0}>
{totalGatorPermissions > 0 ? (
{gatorPermissions.length > 0 ? (
renderGatorPermissions(gatorPermissions)
) : (
<Box
Expand Down
Loading