diff --git a/packages/gator-permissions-snap/locales/en.json b/packages/gator-permissions-snap/locales/en.json
index c61f85c2..930eb657 100644
--- a/packages/gator-permissions-snap/locales/en.json
+++ b/packages/gator-permissions-snap/locales/en.json
@@ -257,10 +257,22 @@
"message": "Existing permissions"
},
"existingPermissionsDescription": {
- "message": "You've already granted permissions for this site. Do you want to continue?"
+ "message": "Permissions you’ve already granted to this site"
},
"existingPermissionsConfirmButton": {
- "message": "Continue"
+ "message": "Back to request"
+ },
+ "existingPermissionsExistingMessage": {
+ "message": "You have granted permissions to this site in the past."
+ },
+ "existingPermissionsSimilarMessage": {
+ "message": "You have granted similar permissions to this site in the past."
+ },
+ "existingPermissionsLink": {
+ "message": "Review them"
+ },
+ "existingPermissionsLoadError": {
+ "message": "We couldn’t load the list. You can go back and try again."
},
"chainLabel": {
"message": "Network"
diff --git a/packages/gator-permissions-snap/src/core/confirmation.tsx b/packages/gator-permissions-snap/src/core/confirmation.tsx
index df215c0e..fc37e97c 100644
--- a/packages/gator-permissions-snap/src/core/confirmation.tsx
+++ b/packages/gator-permissions-snap/src/core/confirmation.tsx
@@ -1,21 +1,19 @@
import { UserInputEventType } from '@metamask/snaps-sdk';
import type { SnapElement } from '@metamask/snaps-sdk/jsx';
-import { Button, Container, Footer } from '@metamask/snaps-sdk/jsx';
import type { DialogInterface } from './dialogInterface';
import type { UserEventDispatcher } from '../userEventDispatcher';
import type { Timeout, TimeoutFactory } from './timeoutFactory';
import type { ConfirmationProps } from './types';
-import { t } from '../utils/i18n';
/**
* Dialog for handling user confirmation of permission grants.
* Manages the UI state, timeout behavior, and user interactions.
*/
export class ConfirmationDialog {
- static readonly #cancelButton = 'cancel-button';
+ static readonly cancelButton = 'cancel-button';
- static readonly #grantButton = 'grant-button';
+ static readonly grantButton = 'grant-button';
readonly #dialogInterface: DialogInterface;
@@ -25,8 +23,6 @@ export class ConfirmationDialog {
#ui: SnapElement;
- #isGrantDisabled = true;
-
#timeout: Timeout | undefined;
#hasTimedOut = false;
@@ -119,7 +115,7 @@ export class ConfirmationDialog {
});
const { unbind: unbindGrantButtonClick } = this.#userEventDispatcher.on({
- elementName: ConfirmationDialog.#grantButton,
+ elementName: ConfirmationDialog.grantButton,
eventType: UserInputEventType.ButtonClickEvent,
interfaceId,
handler: async () => {
@@ -141,7 +137,7 @@ export class ConfirmationDialog {
});
const { unbind: unbindCancelButtonClick } = this.#userEventDispatcher.on({
- elementName: ConfirmationDialog.#cancelButton,
+ elementName: ConfirmationDialog.cancelButton,
eventType: UserInputEventType.ButtonClickEvent,
interfaceId,
handler: async () => {
@@ -184,41 +180,18 @@ export class ConfirmationDialog {
}
#buildConfirmation(): JSX.Element {
- return (
-
- {this.#ui}
-
-
- );
+ return this.#ui;
}
/**
* Updates the confirmation dialog content.
+ * Grant enable/disable is encoded in the `ui` tree (for example `PermissionHandlerContent` props).
+ *
* @param options - The update options.
* @param options.ui - The new UI content.
- * @param options.isGrantDisabled - Whether the grant button should be disabled.
*/
- async updateContent({
- ui,
- isGrantDisabled,
- }: {
- ui: SnapElement;
- isGrantDisabled: boolean;
- }): Promise {
+ async updateContent({ ui }: { ui: SnapElement }): Promise {
this.#ui = ui;
- this.#isGrantDisabled = isGrantDisabled;
-
await this.#dialogInterface.show(this.#buildConfirmation());
}
diff --git a/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsContent.tsx b/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsContent.tsx
index f41243a0..ff8ab647 100644
--- a/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsContent.tsx
+++ b/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsContent.tsx
@@ -1,17 +1,15 @@
import {
Box,
- Button,
- Container,
Section,
- Footer,
Heading,
Text,
Address,
- Divider,
- Bold,
+ Container,
+ Button,
+ Footer,
Skeleton,
+ SnapElement,
} from '@metamask/snaps-sdk/jsx';
-import type { SnapElement } from '@metamask/snaps-sdk/jsx';
import { Hex } from '@metamask/utils';
import { groupPermissionsByFromAddress } from './permissionFormatter';
@@ -23,15 +21,12 @@ import { t } from '../../utils/i18n';
export const EXISTING_PERMISSIONS_CONFIRM_BUTTON =
'existing-permissions-confirm';
-// Maximum number of permissions to display per account
-const MAX_PERMISSIONS_PER_ACCOUNT = 3;
-
/**
- * Builds a skeleton loading state for the existing permissions dialog.
- * Shows placeholder UI while permissions are being fetched and formatted.
+ * Builds a skeleton loading UI for the existing permissions page.
+ * Displays placeholder content while permissions are being loaded and formatted.
*
- * @param config - The configuration for the existing permissions display (used for title/description).
- * @returns The skeleton loading UI as a JSX.Element.
+ * @param config - Title, description, and button label keys (same as the full list view).
+ * @returns The skeleton UI as a JSX.Element.
*/
export function buildExistingPermissionsSkeletonContent(
config: ExistingPermissionDisplayConfig,
@@ -54,14 +49,12 @@ export function buildExistingPermissionsSkeletonContent(
{t('accountLabel')}
-
{/* Show 2 skeleton permission cards */}
{[0, 1].map((permIndex) => (
- {permIndex > 0 && }
@@ -83,8 +76,40 @@ export function buildExistingPermissionsSkeletonContent(
}
/**
- * Builds the existing permissions display content.
- * Shows a comparison between an existing permission and what the user is about to grant.
+ * Fallback when loading or formatting the existing-permissions list fails.
+ * Keeps the confirm action enabled so the user can return to the main request.
+ *
+ * @param config - Title, description, and button label keys.
+ * @returns Fallback UI as a SnapElement.
+ */
+export function buildExistingPermissionsFallbackContent(
+ config: Pick<
+ ExistingPermissionDisplayConfig,
+ 'title' | 'description' | 'buttonLabel'
+ >,
+): SnapElement {
+ const { title, description, buttonLabel } = config;
+
+ return (
+
+
+
+ {t(title)}
+ {t(description)}
+ {t('existingPermissionsLoadError')}
+
+
+
+
+ );
+}
+
+/**
+ * Builds the existing permissions display content: a grouped list of stored granted permissions for review.
*
* @param config - The configuration for the existing permissions display.
* @returns The existing permissions UI as a JSX.Element.
@@ -105,42 +130,20 @@ export function buildExistingPermissionsContent(
{Object.entries(grouped).map(([accountAddress, permissions]) => {
- const displayedPermissions = permissions.slice(
- 0,
- MAX_PERMISSIONS_PER_ACCOUNT,
- );
- const hasMorePermissions =
- permissions.length > MAX_PERMISSIONS_PER_ACCOUNT;
- const moreCount = permissions.length - MAX_PERMISSIONS_PER_ACCOUNT;
-
return (
-
-
-
- {t('accountLabel')}
-
-
-
- {displayedPermissions.map((detail, index) => (
-
- ))}
- {hasMorePermissions && (
-
-
-
- {moreCount === 1
- ? t('morePermissionsCountSingle')
- : t('morePermissionsCountPlural', [String(moreCount)])}
- {t('dappConnectionsLink')}
-
-
- )}
-
-
+
+
+ {t('accountLabel')}
+
+
+ {permissions.map((detail, index) => (
+
+ ))}
+
);
})}
diff --git a/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsService.ts b/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsService.ts
index 9b69960d..52a2848c 100644
--- a/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsService.ts
+++ b/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsService.ts
@@ -1,10 +1,15 @@
-import { UserInputEventType } from '@metamask/snaps-sdk';
+import type { Permission } from '@metamask/7715-permissions-shared/types';
+import {
+ extractDescriptorName,
+ logger,
+} from '@metamask/7715-permissions-shared/utils';
import {
buildExistingPermissionsContent,
+ buildExistingPermissionsFallbackContent,
buildExistingPermissionsSkeletonContent,
- EXISTING_PERMISSIONS_CONFIRM_BUTTON,
} from './existingPermissionsContent';
+import { ExistingPermissionsState } from './existingPermissionsState';
import { formatPermissionWithTokenMetadata } from './permissionFormatter';
import type { ExistingPermissionDisplayConfig } from './types';
import type {
@@ -12,182 +17,219 @@ import type {
StoredGrantedPermission,
} from '../../profileSync/profileSync';
import type { TokenMetadataService } from '../../services/tokenMetadataService';
-import type { UserEventDispatcher } from '../../userEventDispatcher';
import type { DialogInterface } from '../dialogInterface';
+export { ExistingPermissionsState } from './existingPermissionsState';
+
+/**
+ * Extracts the category (stream or periodic) from a permission type name.
+ * Uses suffix checks so unrelated names that merely contain "stream" as a substring are not misclassified.
+ *
+ * @param permissionTypeName - The permission type name to extract category from.
+ * @returns The category ('stream' or 'periodic') or null if unrecognized.
+ */
+function extractPermissionCategory(
+ permissionTypeName: string,
+): 'stream' | 'periodic' | null {
+ if (permissionTypeName.endsWith('-periodic')) {
+ return 'periodic';
+ }
+ if (permissionTypeName.endsWith('-stream')) {
+ return 'stream';
+ }
+ return null;
+}
+
/**
- * Service for displaying existing permissions when a dApp requests new ones.
- * Provides UI for showing the comparison between existing and requested permissions.
+ * Loads stored permissions for a site, classifies them against the current request for banner UI,
+ * and builds the full existing-permissions review screen.
*/
export class ExistingPermissionsService {
readonly #profileSyncManager: ProfileSyncManager;
- readonly #userEventDispatcher: UserEventDispatcher;
-
readonly #tokenMetadataService: TokenMetadataService;
constructor({
profileSyncManager,
- userEventDispatcher,
tokenMetadataService,
}: {
profileSyncManager: ProfileSyncManager;
- userEventDispatcher: UserEventDispatcher;
tokenMetadataService: TokenMetadataService;
}) {
this.#profileSyncManager = profileSyncManager;
- this.#userEventDispatcher = userEventDispatcher;
this.#tokenMetadataService = tokenMetadataService;
}
/**
- * Finds existing permissions matching the given origin.
- * Returns all permissions granted to the origin across all chains (not limited to the requested chainId).
- * Filters by isRevoked and siteOrigin only.
+ * Gets existing permissions matching the given origin.
+ * Entries without `permissionResponse.from` or `permissionResponse.chainId` are omitted.
*
* @param siteOrigin - The origin of the requesting dApp.
- * @returns An array of matching stored permissions across all chains, or an empty array if not found.
+ * @returns Non-revoked stored granted permissions for the origin, or an empty array on failure or if none match.
*/
- async #findMatchingExistingPermission(
+ async getExistingPermissions(
siteOrigin: string,
): Promise {
- const allPermissions =
- await this.#profileSyncManager.getAllGrantedPermissions();
-
- // Return all non-revoked permissions for the origin across all chains
- const matching = allPermissions.filter(
- (permission) =>
- permission.revocationMetadata === undefined &&
- permission.siteOrigin.toLowerCase() === siteOrigin.toLowerCase(),
+ try {
+ const allPermissions =
+ await this.#profileSyncManager.getAllGrantedPermissions();
+
+ // Normalize origin once instead of on each iteration
+ const normalizedOrigin = siteOrigin.toLowerCase();
+
+ // Return all non-revoked permissions for the origin across all chains.
+ // A permission is considered valid if it has both 'from' (account) and 'chainId'.
+ const matching = allPermissions.filter(
+ (permission) =>
+ permission.revocationMetadata === undefined &&
+ permission.siteOrigin.toLowerCase() === normalizedOrigin &&
+ permission.permissionResponse.from &&
+ permission.permissionResponse.chainId,
+ );
+
+ return matching;
+ } catch (error) {
+ logger.error(
+ 'ExistingPermissionsService.getExistingPermissions() failed',
+ {
+ siteOrigin,
+ error: error instanceof Error ? error.message : error,
+ },
+ );
+ return [];
+ }
+ }
+
+ /**
+ * Builds the full-screen list of stored permissions (formatted for display) and a confirm control.
+ *
+ * @param existingPermissions - Stored grants to render; typically from {@link getExistingPermissions}.
+ * @returns JSX for the existing-permissions review container.
+ */
+ async createExistingPermissionsContent(
+ existingPermissions: StoredGrantedPermission[],
+ ): Promise {
+ const formattedPermissions = await Promise.all(
+ existingPermissions.map(async (stored) =>
+ formatPermissionWithTokenMetadata(
+ stored.permissionResponse,
+ this.#tokenMetadataService,
+ ),
+ ),
);
- return matching;
+ const config: ExistingPermissionDisplayConfig = {
+ existingPermissions: formattedPermissions,
+ title: 'existingPermissionsTitle',
+ description: 'existingPermissionsDescription',
+ buttonLabel: 'existingPermissionsConfirmButton',
+ };
+
+ return buildExistingPermissionsContent(config);
}
/**
- * Gets existing permissions matching the given origin.
- * @param siteOrigin - The origin of the requesting dApp.
- * @returns An array of matching stored permissions, or an empty array if not found.
+ * Derives banner state from an in-memory snapshot of stored grants (no profile sync I/O).
+ * Use with {@link getExistingPermissions} once per permission request lifecycle.
+ *
+ * @param existingPermissions - Stored grants for the requesting origin (already filtered).
+ * @param requestedPermission - The permission the user is about to grant.
+ * @returns Banner-driving status, or {@link ExistingPermissionsState.None} if none stored or on error.
*/
- async getExistingPermissions(
- siteOrigin: string,
- ): Promise {
+ getExistingPermissionsStatusFromList(
+ existingPermissions: StoredGrantedPermission[],
+ requestedPermission: Permission,
+ ): ExistingPermissionsState {
try {
- return await this.#findMatchingExistingPermission(siteOrigin);
- } catch {
- return [];
+ if (existingPermissions.length === 0) {
+ return ExistingPermissionsState.None;
+ }
+ const requestedCategory = extractPermissionCategory(
+ extractDescriptorName(requestedPermission.type),
+ );
+ // Only treat as similar when both have a recognized category (stream/periodic) and they match.
+ // Unrecognized types (e.g. revocation) return null; null === null would falsely mark them as similar.
+ const hasSimilar =
+ requestedCategory !== null &&
+ existingPermissions.some((stored) => {
+ const storedCategory = extractPermissionCategory(
+ extractDescriptorName(stored.permissionResponse.permission.type),
+ );
+ return (
+ storedCategory !== null && storedCategory === requestedCategory
+ );
+ });
+ return hasSimilar
+ ? ExistingPermissionsState.SimilarPermissions
+ : ExistingPermissionsState.DissimilarPermissions;
+ } catch (error) {
+ logger.error(
+ 'ExistingPermissionsService.getExistingPermissionsStatusFromList() failed',
+ {
+ error: error instanceof Error ? error.message : error,
+ },
+ );
+ return ExistingPermissionsState.None;
}
}
/**
- * Shows the existing permissions dialog and waits for user acknowledgement.
- * @param options - The options object.
- * @param options.dialogInterface - The dialog interface to use for displaying content.
- * @param options.existingPermissions - The existing permissions to display.
- * @returns Object with wasCancelled flag indicating if user dismissed the dialog.
+ * Compares stored granted permissions for the site to the requested permission (stream vs periodic category).
+ * Fetches from profile sync. Prefer {@link getExistingPermissions} + {@link getExistingPermissionsStatusFromList}
+ * when you already have a snapshot for this origin.
+ *
+ * @param siteOrigin - The requesting dApp origin.
+ * @param requestedPermission - The permission the user is about to grant.
+ * @returns Banner-driving status, or {@link ExistingPermissionsState.None} if none stored or on error.
*/
- async showExistingPermissions({
- dialogInterface,
- existingPermissions,
- }: {
- dialogInterface: DialogInterface;
- existingPermissions: StoredGrantedPermission[] | undefined;
- }): Promise<{ wasCancelled: boolean }> {
- try {
- if (!existingPermissions || existingPermissions.length === 0) {
- return { wasCancelled: false };
- }
+ async getExistingPermissionsStatus(
+ siteOrigin: string,
+ requestedPermission: Permission,
+ ): Promise {
+ const existingPermissions = await this.getExistingPermissions(siteOrigin);
+ return this.getExistingPermissionsStatusFromList(
+ existingPermissions,
+ requestedPermission,
+ );
+ }
+
+ /**
+ * Shows existing permissions in a dialog with skeleton loading state.
+ * Uses a snapshot from {@link getExistingPermissions} so profile sync is not queried again.
+ *
+ * @param dialogInterface - The dialog interface to show content in.
+ * @param existingPermissions - Stored grants for this origin (same snapshot as status computation).
+ */
+ async showExistingPermissions(
+ dialogInterface: DialogInterface,
+ existingPermissions: StoredGrantedPermission[],
+ ): Promise {
+ const skeletonConfig: ExistingPermissionDisplayConfig = {
+ existingPermissions: [],
+ title: 'existingPermissionsTitle',
+ description: 'existingPermissionsDescription',
+ buttonLabel: 'existingPermissionsConfirmButton',
+ };
- // Validate permissions before displaying them
- // A permission is valid if it has both 'from' and 'chainId' fields
- const validPermissions = existingPermissions.filter(
- (stored) =>
- stored.permissionResponse.from && stored.permissionResponse.chainId,
+ try {
+ await dialogInterface.show(
+ buildExistingPermissionsSkeletonContent(skeletonConfig),
);
- // If all permissions are invalid, treat as no existing permissions
- if (validPermissions.length === 0) {
- return { wasCancelled: false };
- }
+ const formattedContent =
+ await this.createExistingPermissionsContent(existingPermissions);
- // Build configuration for the skeleton display (shown immediately)
- const config: ExistingPermissionDisplayConfig = {
- existingPermissions: [],
- title: 'existingPermissionsTitle',
- description: 'existingPermissionsDescription',
- buttonLabel: 'existingPermissionsConfirmButton',
- };
-
- // Track unbind functions to clean up handlers
- const unbindFunctions: (() => void)[] = [];
-
- // Helper to cleanup all event handlers
- const unbindAll = (): void => {
- unbindFunctions.forEach((fn) => fn());
- };
-
- const wasConfirmed = await new Promise((resolve) => {
- // Show skeleton immediately
- const skeletonContent = buildExistingPermissionsSkeletonContent(config);
-
- dialogInterface
- .show(skeletonContent, () => {
- unbindAll();
- resolve(false); // User cancelled via X button
- })
- .then(async (interfaceId) => {
- try {
- // Format permissions with token metadata (this may take time)
- const formattedPermissions = await Promise.all(
- validPermissions.map(async (stored) =>
- formatPermissionWithTokenMetadata(
- stored.permissionResponse,
- this.#tokenMetadataService,
- ),
- ),
- );
-
- // Build configuration for the actual permissions display
- const actualConfig: ExistingPermissionDisplayConfig = {
- existingPermissions: formattedPermissions,
- title: 'existingPermissionsTitle',
- description: 'existingPermissionsDescription',
- buttonLabel: 'existingPermissionsConfirmButton',
- };
-
- // Update dialog with actual content
- const actualContent =
- buildExistingPermissionsContent(actualConfig);
- await dialogInterface.show(actualContent);
- } catch {
- // If formatting fails, dialog still shows with skeleton
- // This is acceptable - user can still interact with the dialog
- }
-
- // Handler for acknowledge button
- const { unbind: unbindConfirm } = this.#userEventDispatcher.on({
- elementName: EXISTING_PERMISSIONS_CONFIRM_BUTTON,
- eventType: UserInputEventType.ButtonClickEvent,
- interfaceId,
- handler: async () => {
- unbindAll();
- resolve(true); // User acknowledged
- },
- });
- unbindFunctions.push(unbindConfirm);
-
- return undefined;
- })
- .catch(() => {
- unbindAll();
- resolve(false); // Error = treat as cancelled
- });
- });
-
- return { wasCancelled: !wasConfirmed };
- } catch {
- // If anything goes wrong, just return false to continue with the flow
- return { wasCancelled: false };
+ await dialogInterface.show(formattedContent);
+ } catch (error) {
+ logger.error(
+ 'ExistingPermissionsService.showExistingPermissions() failed',
+ {
+ error: error instanceof Error ? error.message : error,
+ },
+ );
+ await dialogInterface.show(
+ buildExistingPermissionsFallbackContent(skeletonConfig),
+ );
}
}
}
diff --git a/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsState.ts b/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsState.ts
new file mode 100644
index 00000000..b45591af
--- /dev/null
+++ b/packages/gator-permissions-snap/src/core/existingpermissions/existingPermissionsState.ts
@@ -0,0 +1,9 @@
+/**
+ * Status of existing permissions for a site with respect to the currently requested permission.
+ * Drives banner severity and whether the confirmation flow prefetches stored grants for this origin.
+ */
+export enum ExistingPermissionsState {
+ None = 'None',
+ DissimilarPermissions = 'DissimilarPermissions',
+ SimilarPermissions = 'SimilarPermissions',
+}
diff --git a/packages/gator-permissions-snap/src/core/existingpermissions/index.ts b/packages/gator-permissions-snap/src/core/existingpermissions/index.ts
index a523b290..c066b959 100644
--- a/packages/gator-permissions-snap/src/core/existingpermissions/index.ts
+++ b/packages/gator-permissions-snap/src/core/existingpermissions/index.ts
@@ -1,6 +1,8 @@
+export { ExistingPermissionsState } from './existingPermissionsState';
export { ExistingPermissionsService } from './existingPermissionsService';
export {
buildExistingPermissionsContent,
+ buildExistingPermissionsFallbackContent,
buildExistingPermissionsSkeletonContent,
EXISTING_PERMISSIONS_CONFIRM_BUTTON,
} from './existingPermissionsContent';
@@ -8,7 +10,4 @@ export {
formatPermissionWithTokenMetadata,
groupPermissionsByFromAddress,
} from './permissionFormatter';
-export type {
- ExistingPermissionDisplayConfig,
- FormattedPermissionForDisplay,
-} from './types';
+export type { ExistingPermissionDisplayConfig } from './types';
diff --git a/packages/gator-permissions-snap/src/core/existingpermissions/permissionFormatter.ts b/packages/gator-permissions-snap/src/core/existingpermissions/permissionFormatter.ts
index 846df782..b8df9ea5 100644
--- a/packages/gator-permissions-snap/src/core/existingpermissions/permissionFormatter.ts
+++ b/packages/gator-permissions-snap/src/core/existingpermissions/permissionFormatter.ts
@@ -1,9 +1,11 @@
import type { PermissionResponse } from '@metamask/7715-permissions-shared/types';
-import { extractDescriptorName } from '@metamask/7715-permissions-shared/utils';
+import {
+ extractDescriptorName,
+ logger,
+} from '@metamask/7715-permissions-shared/utils';
import { hexToNumber } from '@metamask/utils';
import type { Hex } from '@metamask/utils';
-import type { FormattedPermissionForDisplay } from './types';
import { DEFAULT_MAX_AMOUNT } from '../../permissions/erc20TokenStream/context';
import type { TokenMetadataService } from '../../services/tokenMetadataService';
import { t } from '../../utils/i18n';
@@ -15,7 +17,10 @@ import { nameAndExplorerUrlByChainId } from '../chainMetadata';
* Represents formatted permission details as an object.
*/
export type PermissionDetail = {
- [key: string]: string;
+ [key: string]: {
+ label: string;
+ value: string;
+ };
};
/**
@@ -45,14 +50,14 @@ function formatTokenAmountWithMetadata(
/**
* Extracts permission details into a display-friendly format.
- * Converts a permission (response or display-formatted) into a key-value object for UI rendering.
+ * Converts a permission (response or display-formatted) into a structured object with translated labels and formatted values.
* Note: Token amounts (periodAmount, maxAmount) should be pre-formatted with metadata by formatPermissionWithTokenMetadata.
*
* @param permission - The permission to extract details from (should be pre-formatted with token metadata).
- * @returns Object of permission details for display.
+ * @returns Object of permission details with label, value, and key for each field.
*/
function extractPermissionDetails(
- permission: FormattedPermissionForDisplay,
+ permission: PermissionResponse,
): PermissionDetail {
const details: PermissionDetail = {};
@@ -61,11 +66,12 @@ function extractPermissionDetails(
// Extract chain information
const chainMetadata =
nameAndExplorerUrlByChainId[hexToNumber(permission.chainId)];
- if (chainMetadata) {
- details[t('chainLabel')] = chainMetadata.name;
- } else {
- details[t('chainLabel')] = permission.chainId;
- }
+ const chainLabel = t('chainLabel');
+ const chainValue = chainMetadata ? chainMetadata.name : permission.chainId;
+ details.chainId = {
+ label: chainLabel,
+ value: chainValue,
+ };
// Extract permission details based on permission type
const permissionData = permission.permission.data;
@@ -73,14 +79,22 @@ function extractPermissionDetails(
if (permissionData && typeof permissionData === 'object') {
// For revocation-type permissions
if (permissionType === 'erc20-token-revocation') {
- details[t('revokeTokenApprovalsLabel')] = t('allTokens');
+ const revokeLabel = t('revokeTokenApprovalsLabel');
+ const revokeValue = t('allTokens');
+ details.tokenApprovals = {
+ label: revokeLabel,
+ value: revokeValue,
+ };
// Add justification if available
if ('justification' in permissionData) {
const { justification } = permissionData;
if (justification !== undefined && justification !== null) {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string -- display value from permission data
- details[t('justificationLabel')] = String(justification);
+ const justificationLabel = t('justificationLabel');
+ details.justification = {
+ label: justificationLabel,
+ value: String(justification),
+ };
}
}
}
@@ -92,14 +106,18 @@ function extractPermissionDetails(
const { periodAmount, periodDuration, justification } = permissionData;
if (periodAmount !== undefined && periodAmount !== null) {
+ const amountLabel = t('amountLabel');
// periodAmount is already formatted with token metadata by formatPermissionWithTokenMetadata
- // eslint-disable-next-line @typescript-eslint/no-base-to-string -- display value from permission data
- details[t('amountLabel')] = String(periodAmount);
+ details.periodAmount = {
+ label: amountLabel,
+ value: String(periodAmount),
+ };
}
if (periodDuration !== undefined && periodDuration !== null) {
const timePeriod = getClosestTimePeriod(Number(periodDuration));
- details[t('periodDurationLabel')] = t(
+ const durationLabel = t('periodDurationLabel');
+ const durationValue = t(
timePeriod.toLowerCase() as
| 'hourly'
| 'daily'
@@ -108,11 +126,18 @@ function extractPermissionDetails(
| 'monthly'
| 'yearly',
);
+ details.periodDuration = {
+ label: durationLabel,
+ value: durationValue,
+ };
}
if (justification !== undefined && justification !== null) {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string -- display value from permission data
- details[t('justificationLabel')] = String(justification);
+ const justificationLabel = t('justificationLabel');
+ details.justification = {
+ label: justificationLabel,
+ value: String(justification),
+ };
}
}
// For stream-type permissions
@@ -123,21 +148,32 @@ function extractPermissionDetails(
const { maxAmount, startTime, justification } = permissionData;
if (maxAmount !== undefined && maxAmount !== null) {
+ const maxAmountLabel = t('maxAmountLabel');
// maxAmount is already formatted with token metadata by formatPermissionWithTokenMetadata
- // eslint-disable-next-line @typescript-eslint/no-base-to-string -- display value from permission data
- details[t('maxAmountLabel')] = String(maxAmount);
+ details.maxAmount = {
+ label: maxAmountLabel,
+ value: String(maxAmount),
+ };
}
if (startTime !== undefined && startTime !== null) {
+ const startTimeLabel = t('startTimeLabel');
const date = new Date(Number(startTime) * 1000);
- details[t('startTimeLabel')] = date.toLocaleString(undefined, {
+ const startTimeValue = date.toLocaleString(undefined, {
timeZone: 'UTC',
});
+ details.startTime = {
+ label: startTimeLabel,
+ value: startTimeValue,
+ };
}
if (justification !== undefined && justification !== null) {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string -- display value from permission data
- details[t('justificationLabel')] = String(justification);
+ const justificationLabel = t('justificationLabel');
+ details.justification = {
+ label: justificationLabel,
+ value: String(justification),
+ };
}
}
}
@@ -146,14 +182,15 @@ function extractPermissionDetails(
}
/**
- * Converts existingPermissions array to an object keyed by CAIP-10 from address.
- * Groups already-formatted permissions by account address.
+ * Converts permissions to an object keyed by CAIP-10 `from` address.
+ * Callers should pass responses that have been through {@link formatPermissionWithTokenMetadata}
+ * when token amounts should appear human-readable in the UI.
*
- * @param permissions - The display-formatted permissions to group.
+ * @param permissions - Permission responses to group (entries without `from`/`chainId` are skipped).
* @returns Object with CAIP-10 addresses as keys and arrays of permission details as values.
*/
export function groupPermissionsByFromAddress(
- permissions: FormattedPermissionForDisplay[],
+ permissions: PermissionResponse[],
): Record {
const result: Record = {};
@@ -183,12 +220,13 @@ export function groupPermissionsByFromAddress(
*
* @param permission - The permission response to format.
* @param tokenMetadataService - Service for fetching token metadata.
- * @returns The permission with display-formatted token amounts; typed as FormattedPermissionForDisplay to prevent misuse.
+ * @returns The same permission shape with display-formatted token amounts in `data`; still typed as
+ * `PermissionResponse`, so treat `data` as UI-only after this call (not raw hex for on-chain math).
*/
export async function formatPermissionWithTokenMetadata(
permission: PermissionResponse,
tokenMetadataService: TokenMetadataService,
-): Promise {
+): Promise {
const permissionData = permission.permission.data as Record;
if (!permissionData || typeof permissionData !== 'object') {
@@ -260,8 +298,14 @@ export async function formatPermissionWithTokenMetadata(
data: formattedData,
},
};
- } catch {
- // If token metadata fetch fails, return original permission
+ } catch (error) {
+ logger.debug(
+ 'formatPermissionWithTokenMetadata: token metadata fetch failed, using raw permission data',
+ {
+ chainId: permission.chainId,
+ error: error instanceof Error ? error.message : error,
+ },
+ );
return permission;
}
}
diff --git a/packages/gator-permissions-snap/src/core/existingpermissions/types.ts b/packages/gator-permissions-snap/src/core/existingpermissions/types.ts
index f85790d7..c1e0cc53 100644
--- a/packages/gator-permissions-snap/src/core/existingpermissions/types.ts
+++ b/packages/gator-permissions-snap/src/core/existingpermissions/types.ts
@@ -2,25 +2,12 @@ import type { PermissionResponse } from '@metamask/7715-permissions-shared/types
import type { MessageKey } from '../../utils/i18n';
-/**
- * Permission response with display-formatted fields (e.g. `maxAmount` as "1.5 ETH" instead of Hex).
- * Use only for UI display. Do not pass to code that expects raw Hex (e.g. formatUnitsFromHex, hexToNumber).
- */
-export type FormattedPermissionForDisplay = Omit<
- PermissionResponse,
- 'permission'
-> & {
- permission: Omit & {
- data: Record;
- };
-};
-
/**
* Configuration for displaying existing permissions.
*/
export type ExistingPermissionDisplayConfig = {
/** The existing permissions to display (with formatted values for UI only) */
- existingPermissions: FormattedPermissionForDisplay[];
+ existingPermissions: PermissionResponse[];
/** The translation key for the dialog title */
title: MessageKey;
/** The translation key for the comparison description */
diff --git a/packages/gator-permissions-snap/src/core/permissionHandler.ts b/packages/gator-permissions-snap/src/core/permissionHandler.ts
index 8457db4a..b2a0402d 100644
--- a/packages/gator-permissions-snap/src/core/permissionHandler.ts
+++ b/packages/gator-permissions-snap/src/core/permissionHandler.ts
@@ -30,9 +30,11 @@ import type {
AccountUpgradeStatus,
} from './accountController';
import { getChainMetadata } from './chainMetadata';
+import { EXISTING_PERMISSIONS_CONFIRM_BUTTON } from './existingpermissions';
import {
ACCOUNT_SELECTOR_NAME,
PermissionHandlerContent,
+ SHOW_EXISTING_PERMISSIONS_BUTTON_NAME,
SkeletonPermissionHandlerContent,
} from './permissionHandlerContent';
import type { PermissionRequestLifecycleOrchestrator } from './permissionRequestLifecycleOrchestrator';
@@ -51,6 +53,7 @@ import { logger } from '../../../shared/src/utils/logger';
import { createCancellableOperation } from '../utils/cancellableOperation';
import type { MessageKey } from '../utils/i18n';
import { formatUnits } from '../utils/value';
+import type { ExistingPermissionsState } from './existingpermissions/existingPermissionsState';
export const JUSTIFICATION_SHOW_MORE_BUTTON_NAME = 'show-more-justification';
@@ -217,6 +220,8 @@ export class PermissionHandler<
chainId,
scanDappUrlResult,
scanAddressResult,
+ existingPermissionsStatus,
+ isGrantDisabled,
}: {
context: TContext;
metadata: TMetadata;
@@ -224,6 +229,8 @@ export class PermissionHandler<
chainId: number;
scanDappUrlResult: ScanDappUrlResult | null;
scanAddressResult: FetchAddressScanResult | null;
+ existingPermissionsStatus: ExistingPermissionsState;
+ isGrantDisabled: boolean;
}): Promise => {
const { name: networkName, explorerUrl } = getChainMetadata({ chainId });
@@ -264,6 +271,8 @@ export class PermissionHandler<
chainId,
explorerUrl,
isAccountUpgraded: this.#accountUpgradeStatus.isUpgraded,
+ existingPermissionsStatus,
+ isGrantDisabled,
});
};
@@ -418,6 +427,34 @@ export class PermissionHandler<
},
});
+ const { unbind: unbindShowExistingPermissionsButtonClick } =
+ this.#userEventDispatcher.on({
+ elementName: SHOW_EXISTING_PERMISSIONS_BUTTON_NAME,
+ eventType: UserInputEventType.ButtonClickEvent,
+ interfaceId,
+ handler: async () => {
+ currentContext = {
+ ...currentContext,
+ showExistingPermissions: true,
+ };
+ await rerender();
+ },
+ });
+
+ const { unbind: unbindExistingPermissionsConfirmButtonClick } =
+ this.#userEventDispatcher.on({
+ elementName: EXISTING_PERMISSIONS_CONFIRM_BUTTON,
+ eventType: UserInputEventType.ButtonClickEvent,
+ interfaceId,
+ handler: async () => {
+ currentContext = {
+ ...currentContext,
+ showExistingPermissions: false,
+ };
+ await rerender();
+ },
+ });
+
const unbindRuleHandlers = bindRuleHandlers({
rules: this.#rules,
userEventDispatcher: this.#userEventDispatcher,
@@ -433,6 +470,8 @@ export class PermissionHandler<
unbindRuleHandlers();
unbindShowMoreButtonClick();
unbindAccountSelected();
+ unbindShowExistingPermissionsButtonClick();
+ unbindExistingPermissionsConfirmButtonClick();
};
};
diff --git a/packages/gator-permissions-snap/src/core/permissionHandlerContent.tsx b/packages/gator-permissions-snap/src/core/permissionHandlerContent.tsx
index c1928d7a..1231d2e3 100644
--- a/packages/gator-permissions-snap/src/core/permissionHandlerContent.tsx
+++ b/packages/gator-permissions-snap/src/core/permissionHandlerContent.tsx
@@ -7,9 +7,14 @@ import {
Text,
Skeleton,
AccountSelector,
+ Banner,
+ Button,
+ Container,
+ Footer,
} from '@metamask/snaps-sdk/jsx';
import { parseCaipAssetType } from '@metamask/utils';
+import { ConfirmationDialog } from './confirmation';
import { JUSTIFICATION_SHOW_MORE_BUTTON_NAME } from './permissionHandler';
import type { BaseContext, IconData } from './types';
import {
@@ -31,8 +36,11 @@ import {
} from '../ui/components';
import type { MessageKey } from '../utils/i18n';
import { t } from '../utils/i18n';
+import { ExistingPermissionsState } from './existingpermissions/existingPermissionsState';
export const ACCOUNT_SELECTOR_NAME = 'account-selector';
+export const SHOW_EXISTING_PERMISSIONS_BUTTON_NAME =
+ 'show-existing-permissions-button';
export type PermissionHandlerContentProps = {
children: SnapElement;
@@ -55,6 +63,9 @@ export type PermissionHandlerContentProps = {
chainId: number;
explorerUrl: string | undefined;
isAccountUpgraded: boolean;
+ existingPermissionsStatus: ExistingPermissionsState;
+ /** When true, the primary grant button is not clickable. */
+ isGrantDisabled: boolean;
};
/**
@@ -78,6 +89,8 @@ export type PermissionHandlerContentProps = {
* @param options.chainId - The chain ID of the network.
* @param options.explorerUrl - The URL of the block explorer for the token.
* @param options.isAccountUpgraded - Whether the account is upgraded to a smart account.
+ * @param options.existingPermissionsStatus - Status of existing permissions for banner UI.
+ * @param options.isGrantDisabled - Whether the grant button should render disabled.
* @returns The confirmation content.
*/
export const PermissionHandlerContent = ({
@@ -99,6 +112,8 @@ export const PermissionHandlerContent = ({
chainId,
explorerUrl,
isAccountUpgraded,
+ existingPermissionsStatus,
+ isGrantDisabled,
}: PermissionHandlerContentProps): SnapElement => {
const tokenBalanceComponent = TokenBalanceField({
tokenBalance,
@@ -175,74 +190,106 @@ export const PermissionHandlerContent = ({
);
return (
-
-
-
- {t(permissionTitle)}
- {t(permissionSubtitle)}
-
-
-
-
+
+
+
+
+ {t(permissionTitle)}
+ {t(permissionSubtitle)}
+
+
+
+
+
+ {t('accountLabel')}
+
+
+
+
+ {!isAccountUpgraded && (
+
+ {t('accountUpgradeWarning')}
+
+ )}
+ {hasAsset && (
+
+ {fiatBalanceComponent}
+ {tokenBalanceComponent}
+
+ )}
+
+
+ {existingPermissionsStatus ===
+ ExistingPermissionsState.SimilarPermissions && (
+
+ {t('existingPermissionsSimilarMessage')}
+
+
+ )}
+ {existingPermissionsStatus ===
+ ExistingPermissionsState.DissimilarPermissions && (
+
+ {t('existingPermissionsExistingMessage')}
+
+
+ )}
+
+
- {t('accountLabel')}
-
+ {t('justificationLabel')}
+
+
-
+
+ {fromField}
+ {addressField}
+
- {!isAccountUpgraded && (
-
- {t('accountUpgradeWarning')}
-
- )}
{hasAsset && (
-
- {fiatBalanceComponent}
- {tokenBalanceComponent}
-
+
)}
-
-
-
-
-
- {t('justificationLabel')}
-
-
-
-
-
-
- {fromField}
- {addressField}
-
- {hasAsset && (
-
- )}
-
- {children}
+
+ {children}
+
-
+
+
);
};
@@ -254,49 +301,66 @@ export const SkeletonPermissionHandlerContent = ({
permissionSubtitle: MessageKey;
}): JSX.Element => {
return (
-
-
-
- {t(permissionTitle)}
- {t(permissionSubtitle)}
-
-
-
-
-
- {t('accountLabel')}
-
+
+
+
+
+ {t(permissionTitle)}
+ {t(permissionSubtitle)}
+
+
+
+
+
+ {t('accountLabel')}
+
+
+
+
+
+
+
-
-
-
-
+
+
+
-
+
+
);
};
diff --git a/packages/gator-permissions-snap/src/core/permissionRequestLifecycleOrchestrator.ts b/packages/gator-permissions-snap/src/core/permissionRequestLifecycleOrchestrator.ts
index 1c6cf294..ac026a26 100644
--- a/packages/gator-permissions-snap/src/core/permissionRequestLifecycleOrchestrator.ts
+++ b/packages/gator-permissions-snap/src/core/permissionRequestLifecycleOrchestrator.ts
@@ -43,6 +43,7 @@ import type {
} from '../clients/trustSignalsClient';
import type { NonceCaveatService } from '../services/nonceCaveatService';
import type { SnapsMetricsService } from '../services/snapsMetricsService';
+import { ExistingPermissionsState } from './existingpermissions/existingPermissionsState';
/**
* Orchestrator for the permission request lifecycle.
@@ -161,11 +162,32 @@ export class PermissionRequestLifecycleOrchestrator {
const dialogInterface =
this.#dialogInterfaceFactory.createDialogInterface();
- // Start loading existing permissions in the background before showing introduction
- // This way if the introduction is shown, the existing permissions will already be loaded
- const existingPermissionsPromise =
+ // One profile-sync read per permission request: same snapshot drives the banner and the
+ // "review existing" list. getExistingPermissions() already resolves to [] on failure (never rejects).
+ const existingPermissionsForOriginPromise =
this.#existingPermissionsService.getExistingPermissions(origin);
+ // Derive banner state from that snapshot in parallel with the introduction flow.
+ // Chain .catch() so the promise never rejects: if the user cancels at intro we return
+ // without awaiting it, and any processing error should not cause an unhandled rejection.
+ const existingPermissionsStatusPromise = existingPermissionsForOriginPromise
+ .then((list) =>
+ this.#existingPermissionsService.getExistingPermissionsStatusFromList(
+ list,
+ validatedPermissionRequest.permission,
+ ),
+ )
+ .catch((error: unknown) => {
+ logger.error(
+ 'PermissionRequestLifecycleOrchestrator: existing permissions status from snapshot failed',
+ {
+ origin,
+ error: error instanceof Error ? error.message : error,
+ },
+ );
+ return ExistingPermissionsState.None;
+ });
+
// Check if we need to show introduction
if (
await this.#permissionIntroductionService.shouldShowIntroduction(
@@ -198,32 +220,6 @@ export class PermissionRequestLifecycleOrchestrator {
);
}
- // Await the existing permissions that were started loading earlier
- const existingPermissions = await existingPermissionsPromise;
-
- if (existingPermissions?.length > 0) {
- const { wasCancelled } =
- await this.#existingPermissionsService.showExistingPermissions({
- dialogInterface,
- existingPermissions,
- });
-
- // If user cancelled the existing permissions dialog, reject the permission request
- if (wasCancelled) {
- await this.#snapsMetricsService.trackPermissionRejected({
- origin,
- permissionType,
- chainId: permissionRequest.chainId,
- permissionData: permissionRequest.permission.data,
- });
-
- return {
- approved: false,
- reason: 'Permission request denied at existing permissions screen',
- };
- }
- }
-
// only necessary when not pre-installed, to ensure that the account
// permissions are requested before the confirmation dialog is shown.
await this.#accountController.getAccountAddresses();
@@ -278,6 +274,9 @@ export class PermissionRequestLifecycleOrchestrator {
let scanDappUrlResult: ScanDappUrlResult | null = null;
let scanAddressResult: FetchAddressScanResult | null = null;
+ /** Avoid re-running skeleton + format when trust-signal refreshes fire while the subview is open. */
+ let existingPermissionsSubviewActive = false;
+
const updateConfirmation = async ({
newContext,
isGrantDisabled,
@@ -292,19 +291,37 @@ export class PermissionRequestLifecycleOrchestrator {
const metadata = await lifecycleHandlers.deriveMetadata({ context });
- const ui = await lifecycleHandlers.createConfirmationContent({
- context,
- metadata,
- origin,
- chainId,
- scanDappUrlResult,
- scanAddressResult,
- });
+ const existingPermissionsStatus =
+ await existingPermissionsStatusPromise;
+
+ const grantDisabled = isGrantDisabled || hasValidationErrors(metadata);
+
+ if (context.showExistingPermissions) {
+ if (!existingPermissionsSubviewActive) {
+ // Set synchronously before awaits so eslint require-atomic-updates is satisfied;
+ // runUpdate calls are serialized via lastUpdateConfirmationPromise.
+ existingPermissionsSubviewActive = true;
+ const snapshot = await existingPermissionsForOriginPromise;
+ await this.#existingPermissionsService.showExistingPermissions(
+ dialogInterface,
+ snapshot,
+ );
+ }
+ } else {
+ existingPermissionsSubviewActive = false;
+ const ui = await lifecycleHandlers.createConfirmationContent({
+ context,
+ metadata,
+ origin,
+ chainId,
+ scanDappUrlResult,
+ scanAddressResult,
+ existingPermissionsStatus,
+ isGrantDisabled: grantDisabled,
+ });
- await confirmationDialog.updateContent({
- ui,
- isGrantDisabled: isGrantDisabled || hasValidationErrors(metadata),
- });
+ await confirmationDialog.updateContent({ ui });
+ }
};
lastUpdateConfirmationPromise =
diff --git a/packages/gator-permissions-snap/src/core/types.ts b/packages/gator-permissions-snap/src/core/types.ts
index d8a4c981..5e0d713a 100644
--- a/packages/gator-permissions-snap/src/core/types.ts
+++ b/packages/gator-permissions-snap/src/core/types.ts
@@ -20,6 +20,7 @@ import type { PermissionRequestLifecycleOrchestrator } from './permissionRequest
import type { TimeoutFactory } from './timeoutFactory';
import type { TokenPricesService } from '../services/tokenPricesService';
import type { MessageKey } from '../utils/i18n';
+import type { ExistingPermissionsState } from './existingpermissions/existingPermissionsState';
/**
* Represents the result of a permission request.
@@ -57,6 +58,11 @@ export type BaseContext = {
symbol: string;
iconDataBase64: string | null;
};
+ /**
+ * When true, the confirmation UI shows stored permissions for this site instead of the grant form.
+ * Omitted or falsey means the normal confirmation content. Set by the permission handler when the user opens the existing-permissions view.
+ */
+ showExistingPermissions?: boolean | null;
};
export type BaseMetadata = {
@@ -144,6 +150,9 @@ export type LifecycleOrchestrationHandlers<
chainId: number;
scanDappUrlResult: ScanDappUrlResult | null;
scanAddressResult: FetchAddressScanResult | null;
+ existingPermissionsStatus: ExistingPermissionsState;
+ /** Whether the grant control should render disabled (validation + caller intent). */
+ isGrantDisabled: boolean;
}) => Promise;
applyContext: (args: {
context: TContext;
@@ -163,7 +172,6 @@ export type LifecycleOrchestrationHandlers<
* @param confirmationCreatedArgs.interfaceId - The interface ID for the confirmation dialog
* @param confirmationCreatedArgs.initialContext - The initial context for the confirmation dialog
* @param confirmationCreatedArgs.updateContext - Function to update the context of the confirmation dialog
- * @param confirmationCreatedArgs.isAdjustmentAllowed - Whether adjustments to the confirmation dialog are allowed
*/
onConfirmationCreated?: (confirmationCreatedArgs: {
interfaceId: string;
diff --git a/packages/gator-permissions-snap/src/index.ts b/packages/gator-permissions-snap/src/index.ts
index 0ce451e9..74a922f7 100644
--- a/packages/gator-permissions-snap/src/index.ts
+++ b/packages/gator-permissions-snap/src/index.ts
@@ -191,7 +191,6 @@ const permissionIntroductionService = new PermissionIntroductionService({
const existingPermissionsService = new ExistingPermissionsService({
profileSyncManager,
- userEventDispatcher,
tokenMetadataService,
});
diff --git a/packages/gator-permissions-snap/src/ui/components/PermissionCard.tsx b/packages/gator-permissions-snap/src/ui/components/PermissionCard.tsx
index 734b1495..45b12483 100644
--- a/packages/gator-permissions-snap/src/ui/components/PermissionCard.tsx
+++ b/packages/gator-permissions-snap/src/ui/components/PermissionCard.tsx
@@ -1,4 +1,4 @@
-import { Box, Divider, Text } from '@metamask/snaps-sdk/jsx';
+import { Box, Section, Text } from '@metamask/snaps-sdk/jsx';
import type { PermissionDetail } from '../../core/existingpermissions/permissionFormatter';
@@ -23,18 +23,24 @@ export const PermissionCard = ({
index,
}: PermissionCardProps): JSX.Element => {
return (
-
- {index > 0 && }
- {Object.entries(detail).map(([label, value]) => (
-
- {label}:
- {value}
-
- ))}
-
+
+ {Object.entries(detail).map(([key, item]) =>
+ key === 'justification' ? (
+
+ {item.label}:
+ {item.value}
+
+ ) : (
+
+ {item.label}:
+ {item.value}
+
+ ),
+ )}
+
);
};
diff --git a/packages/gator-permissions-snap/test/core/confirmation.test.ts b/packages/gator-permissions-snap/test/core/confirmation.test.ts
index 69a07336..bf1fa223 100644
--- a/packages/gator-permissions-snap/test/core/confirmation.test.ts
+++ b/packages/gator-permissions-snap/test/core/confirmation.test.ts
@@ -324,7 +324,6 @@ describe('ConfirmationDialog', () => {
await confirmationDialog.updateContent({
ui: updatedUi,
- isGrantDisabled: false,
});
expect(mockSnapsProvider.request).toHaveBeenCalledWith({
diff --git a/packages/gator-permissions-snap/test/core/existingPermissions/existingPermissionsService.test.ts b/packages/gator-permissions-snap/test/core/existingPermissions/existingPermissionsService.test.ts
index c0c2fd20..dbf40719 100644
--- a/packages/gator-permissions-snap/test/core/existingPermissions/existingPermissionsService.test.ts
+++ b/packages/gator-permissions-snap/test/core/existingPermissions/existingPermissionsService.test.ts
@@ -1,11 +1,22 @@
-import { describe, it, beforeEach, expect, jest } from '@jest/globals';
-import { UserInputEventType } from '@metamask/snaps-sdk';
+import {
+ describe,
+ it,
+ beforeEach,
+ afterEach,
+ expect,
+ jest,
+} from '@jest/globals';
+import type { Permission } from '@metamask/7715-permissions-shared/types';
+import { logger } from '@metamask/7715-permissions-shared/utils';
import type { Hex } from '@metamask/utils';
import { bytesToHex } from '@metamask/utils';
import type { TokenBalanceAndMetadata } from '../../../src/clients/types';
import type { DialogInterface } from '../../../src/core/dialogInterface';
-import { ExistingPermissionsService } from '../../../src/core/existingpermissions/existingPermissionsService';
+import {
+ ExistingPermissionsService,
+ ExistingPermissionsState,
+} from '../../../src/core/existingpermissions/existingPermissionsService';
import type {
ProfileSyncManager,
StoredGrantedPermission,
@@ -14,7 +25,6 @@ import type {
TokenMetadata,
TokenMetadataService,
} from '../../../src/services/tokenMetadataService';
-import type { UserEventDispatcher } from '../../../src/userEventDispatcher';
// Helper to generate random addresses
const randomAddress = (): Hex => {
@@ -55,26 +65,17 @@ const createMockStoredPermission = (
describe('ExistingPermissionsService', () => {
let service: ExistingPermissionsService;
let mockProfileSyncManager: jest.Mocked;
- let mockUserEventDispatcher: jest.Mocked;
let mockTokenMetadataService: jest.Mocked;
- let mockDialogInterface: jest.Mocked;
beforeEach(() => {
jest.clearAllMocks();
+ jest.spyOn(logger, 'error').mockImplementation(() => undefined);
// Setup mock ProfileSyncManager
mockProfileSyncManager = {
getAllGrantedPermissions: jest.fn(),
} as unknown as jest.Mocked;
- // Setup mock UserEventDispatcher
- mockUserEventDispatcher = {
- on: jest.fn().mockReturnValue({
- unbind: jest.fn(),
- dispatcher: {} as any,
- }),
- } as unknown as jest.Mocked;
-
// Setup mock TokenMetadataService - formatPermissionWithTokenMetadata uses getTokenMetadata
const tokenMetadata: TokenMetadata = { decimals: 18, symbol: 'ETH' };
mockTokenMetadataService = {
@@ -89,20 +90,17 @@ describe('ExistingPermissionsService', () => {
}),
} as unknown as jest.Mocked;
- // Setup mock DialogInterface
- mockDialogInterface = {
- show: jest.fn(async (_ui: any, _onClose?: () => void) => 'interface-id'),
- interfaceId: 'interface-id',
- } as unknown as jest.Mocked;
-
// Create service with mocks
service = new ExistingPermissionsService({
profileSyncManager: mockProfileSyncManager,
- userEventDispatcher: mockUserEventDispatcher,
tokenMetadataService: mockTokenMetadataService,
});
});
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
describe('getExistingPermissions()', () => {
it('should return all permissions for origin across all chains', async () => {
// Setup: permissions on chains A, B, C
@@ -188,205 +186,300 @@ describe('ExistingPermissionsService', () => {
// Assert: returns empty array instead of throwing
expect(result).toStrictEqual([]);
+ expect(logger.error).toHaveBeenCalledWith(
+ 'ExistingPermissionsService.getExistingPermissions() failed',
+ expect.objectContaining({
+ siteOrigin: 'https://example.com',
+ error: 'Storage error',
+ }),
+ );
});
});
- describe('showExistingPermissions()', () => {
- it('should return early if no existing permissions', async () => {
- // Action
- const result = await service.showExistingPermissions({
- dialogInterface: mockDialogInterface,
- existingPermissions: undefined,
- });
+ describe('getExistingPermissionsStatus()', () => {
+ it('should return None when no existing permissions exist', async () => {
+ mockProfileSyncManager.getAllGrantedPermissions.mockResolvedValue([]);
- // Assert
- expect(result).toStrictEqual({ wasCancelled: false });
- expect(mockDialogInterface.show).not.toHaveBeenCalled();
- });
+ const requestedPermission: Permission = {
+ type: 'erc20-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- it('should return early if empty permission array', async () => {
- // Action
- const result = await service.showExistingPermissions({
- dialogInterface: mockDialogInterface,
- existingPermissions: [],
- });
+ const status = await service.getExistingPermissionsStatus(
+ 'https://example.com',
+ requestedPermission,
+ );
- // Assert
- expect(result).toStrictEqual({ wasCancelled: false });
- expect(mockDialogInterface.show).not.toHaveBeenCalled();
+ expect(status).toBe(ExistingPermissionsState.None);
});
- it('should return early if all permissions are invalid', async () => {
- const invalidPermission1 = createMockStoredPermission();
- invalidPermission1.permissionResponse.from = undefined as any;
+ it('should return SimilarPermissions when matching categories exist', async () => {
+ const permission = createMockStoredPermission();
+ permission.permissionResponse.permission = {
+ type: 'erc20-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
+
+ mockProfileSyncManager.getAllGrantedPermissions.mockResolvedValue([
+ permission,
+ ]);
- const invalidPermission2 = createMockStoredPermission();
- invalidPermission2.permissionResponse.chainId = undefined as any;
+ const requestedPermission: Permission = {
+ type: 'native-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- // Action
- const result = await service.showExistingPermissions({
- dialogInterface: mockDialogInterface,
- existingPermissions: [invalidPermission1, invalidPermission2],
- });
-
- // Assert: returns early without showing dialog
- expect(result).toStrictEqual({ wasCancelled: false });
- expect(mockDialogInterface.show).not.toHaveBeenCalled();
+ const status = await service.getExistingPermissionsStatus(
+ 'https://example.com',
+ requestedPermission,
+ );
+
+ expect(status).toBe(ExistingPermissionsState.SimilarPermissions);
});
- it('should show skeleton immediately then update with real content', async () => {
+ it('should return DissimilarPermissions when categories do not match', async () => {
const permission = createMockStoredPermission();
+ permission.permissionResponse.permission = {
+ type: 'erc20-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- // Mock dialog.show to capture the callback and resolve immediately
- mockDialogInterface.show.mockImplementation(async (_ui, _onClose) => {
- return 'interface-id';
- });
+ mockProfileSyncManager.getAllGrantedPermissions.mockResolvedValue([
+ permission,
+ ]);
- // Action
- const resultPromise = service.showExistingPermissions({
- dialogInterface: mockDialogInterface,
- existingPermissions: [permission],
- });
+ const requestedPermission: Permission = {
+ type: 'native-token-periodic',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
+
+ const status = await service.getExistingPermissionsStatus(
+ 'https://example.com',
+ requestedPermission,
+ );
- // Allow async operations to complete
- await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(status).toBe(ExistingPermissionsState.DissimilarPermissions);
+ });
- // Assert: skeleton should be shown, then updated with real content
- expect(mockDialogInterface.show).toHaveBeenCalledTimes(2);
+ it('should not treat types that merely contain "stream" as stream category', async () => {
+ const permission = createMockStoredPermission();
+ permission.permissionResponse.permission = {
+ type: 'custom-streaming-permission',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- // Get both calls
- const firstCall = mockDialogInterface.show.mock.calls[0];
- const secondCall = mockDialogInterface.show.mock.calls[1];
+ mockProfileSyncManager.getAllGrantedPermissions.mockResolvedValue([
+ permission,
+ ]);
- expect(firstCall).toBeDefined();
- expect(secondCall).toBeDefined();
+ const requestedPermission: Permission = {
+ type: 'native-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- // Clean up: trigger dialog close to resolve the promise
- const onCloseCallback = firstCall?.[1];
- onCloseCallback?.();
+ const status = await service.getExistingPermissionsStatus(
+ 'https://example.com',
+ requestedPermission,
+ );
- await resultPromise;
+ expect(status).toBe(ExistingPermissionsState.DissimilarPermissions);
});
- it('should call getTokenMetadata when permission has token amount fields', async () => {
- const permission = createMockStoredPermission();
- permission.permissionResponse.permission = {
+ it('should return None on error', async () => {
+ mockProfileSyncManager.getAllGrantedPermissions.mockRejectedValue(
+ new Error('Storage error'),
+ );
+
+ const requestedPermission: Permission = {
type: 'erc20-token-stream',
- data: {
- maxAmount: '0xde0b6b3a7640000' as Hex, // 1 ETH in hex
- startTime: Math.floor(Date.now() / 1000),
- },
+ data: {},
isAdjustmentAllowed: true,
};
- mockDialogInterface.show.mockResolvedValue('interface-id');
+ const status = await service.getExistingPermissionsStatus(
+ 'https://example.com',
+ requestedPermission,
+ );
- const resultPromise = service.showExistingPermissions({
- dialogInterface: mockDialogInterface,
- existingPermissions: [permission],
- });
+ expect(status).toBe(ExistingPermissionsState.None);
+ });
- await new Promise((resolve) => setTimeout(resolve, 50));
+ it('should call profile sync only once', async () => {
+ mockProfileSyncManager.getAllGrantedPermissions.mockResolvedValue([]);
- expect(mockTokenMetadataService.getTokenMetadata).toHaveBeenCalledWith(
- expect.objectContaining({
- chainId: 1,
- account: permission.permissionResponse.from,
- }),
- );
+ const requestedPermission: Permission = {
+ type: 'erc20-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- const onCloseCallback = mockDialogInterface.show.mock.calls[0]?.[1];
- onCloseCallback?.();
+ await service.getExistingPermissionsStatus(
+ 'https://example.com',
+ requestedPermission,
+ );
- await resultPromise;
+ expect(
+ mockProfileSyncManager.getAllGrantedPermissions,
+ ).toHaveBeenCalledTimes(1);
});
+ });
- it('should filter out invalid permissions from display', async () => {
- const validPermission = createMockStoredPermission();
- const invalidPermissionNoFrom = { ...createMockStoredPermission() };
- invalidPermissionNoFrom.permissionResponse.from = undefined as any;
+ describe('getExistingPermissionsStatusFromList()', () => {
+ it('returns None when list is empty', () => {
+ const requestedPermission: Permission = {
+ type: 'erc20-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- mockDialogInterface.show.mockResolvedValue('interface-id');
+ expect(
+ service.getExistingPermissionsStatusFromList([], requestedPermission),
+ ).toBe(ExistingPermissionsState.None);
+ });
- // Action
- const resultPromise = service.showExistingPermissions({
- dialogInterface: mockDialogInterface,
- existingPermissions: [validPermission, invalidPermissionNoFrom],
- });
+ it('returns SimilarPermissions when matching categories exist', () => {
+ const permission = createMockStoredPermission();
+ permission.permissionResponse.permission = {
+ type: 'erc20-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- // Allow async operations
- await new Promise((resolve) => setTimeout(resolve, 50));
+ const requestedPermission: Permission = {
+ type: 'native-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- // Assert: dialog was shown (skeleton + real content attempt)
- expect(mockDialogInterface.show).toHaveBeenCalled();
+ expect(
+ service.getExistingPermissionsStatusFromList(
+ [permission],
+ requestedPermission,
+ ),
+ ).toBe(ExistingPermissionsState.SimilarPermissions);
+ });
- // Trigger dialog close
- const onCloseCallback = mockDialogInterface.show.mock.calls[0]?.[1];
- onCloseCallback?.();
+ it('returns DissimilarPermissions when categories do not match', () => {
+ const permission = createMockStoredPermission();
+ permission.permissionResponse.permission = {
+ type: 'erc20-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- await resultPromise;
+ const requestedPermission: Permission = {
+ type: 'native-token-periodic',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
+
+ expect(
+ service.getExistingPermissionsStatusFromList(
+ [permission],
+ requestedPermission,
+ ),
+ ).toBe(ExistingPermissionsState.DissimilarPermissions);
});
- it('should register button handler for user confirmation', async () => {
+ it('does not treat types that merely contain "stream" as stream category', () => {
const permission = createMockStoredPermission();
+ permission.permissionResponse.permission = {
+ type: 'custom-streaming-permission',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- mockDialogInterface.show.mockResolvedValue('interface-id');
+ const requestedPermission: Permission = {
+ type: 'native-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ };
- // Action
- const resultPromise = service.showExistingPermissions({
- dialogInterface: mockDialogInterface,
- existingPermissions: [permission],
- });
+ expect(
+ service.getExistingPermissionsStatusFromList(
+ [permission],
+ requestedPermission,
+ ),
+ ).toBe(ExistingPermissionsState.DissimilarPermissions);
+ });
+ });
- // Allow async operations
- await new Promise((resolve) => setTimeout(resolve, 50));
+ describe('showExistingPermissions()', () => {
+ it('shows skeleton then list without calling profile sync', async () => {
+ const show = jest.fn().mockResolvedValue(undefined);
+ const dialogInterface = { show } as unknown as DialogInterface;
- // Assert: button handler was registered
- expect(mockUserEventDispatcher.on).toHaveBeenCalledWith(
- expect.objectContaining({
- eventType: UserInputEventType.ButtonClickEvent,
- }),
- );
+ await service.showExistingPermissions(dialogInterface, [
+ createMockStoredPermission(),
+ ]);
- // Clean up
- const onCloseCallback = mockDialogInterface.show.mock.calls[0]?.[1];
- onCloseCallback?.();
+ expect(show).toHaveBeenCalledTimes(2);
+ expect(
+ mockProfileSyncManager.getAllGrantedPermissions,
+ ).not.toHaveBeenCalled();
+ });
+
+ it('shows fallback UI when formatting fails', async () => {
+ const show = jest.fn().mockResolvedValue(undefined);
+ const dialogInterface = { show } as unknown as DialogInterface;
+ const spy = jest
+ .spyOn(service, 'createExistingPermissionsContent')
+ .mockRejectedValueOnce(new Error('format failed'));
+
+ await service.showExistingPermissions(dialogInterface, [
+ createMockStoredPermission(),
+ ]);
- await resultPromise;
+ expect(show).toHaveBeenCalledTimes(2);
+ expect(logger.error).toHaveBeenCalled();
+ spy.mockRestore();
});
+ });
- it('should handle errors gracefully without crashing', async () => {
+ describe('createExistingPermissionsContent()', () => {
+ it('should format permissions and build content', async () => {
const permission = createMockStoredPermission();
+
+ const content = await service.createExistingPermissionsContent([
+ permission,
+ ]);
+
+ expect(content).toBeDefined();
+ expect(mockTokenMetadataService.getTokenMetadata).not.toHaveBeenCalled();
+ });
+
+ it('should call getTokenMetadata when permission has token amount fields', async () => {
+ const from = randomAddress();
+ const permission = createMockStoredPermission('0x1', from);
permission.permissionResponse.permission = {
type: 'erc20-token-stream',
data: {
- maxAmount: '0xde0b6b3a7640000' as Hex,
- startTime: Math.floor(Date.now() / 1000),
+ maxAmount: '0xde0b6b3a7640000',
+ tokenAddress: randomAddress(),
+ justification: 'test',
},
isAdjustmentAllowed: true,
};
- mockTokenMetadataService.getTokenMetadata.mockRejectedValue(
- new Error('Token metadata fetch failed'),
- );
- mockDialogInterface.show.mockResolvedValue('interface-id');
-
- // Action - should not throw when getTokenMetadata fails (dialog stays with skeleton)
- const resultPromise = service.showExistingPermissions({
- dialogInterface: mockDialogInterface,
- existingPermissions: [permission],
- });
+ const content = await service.createExistingPermissionsContent([
+ permission,
+ ]);
- // Allow async operations
- await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(content).toBeDefined();
+ expect(mockTokenMetadataService.getTokenMetadata).toHaveBeenCalled();
+ });
- // Trigger dialog close
- const onCloseCallback = mockDialogInterface.show.mock.calls[0]?.[1];
- onCloseCallback?.();
+ it('should handle empty permission array', async () => {
+ const content = await service.createExistingPermissionsContent([]);
- // Assert: should complete without throwing
- const result = await resultPromise;
- expect(result).toStrictEqual(expect.objectContaining({}));
+ expect(content).toBeDefined();
});
});
});
diff --git a/packages/gator-permissions-snap/test/core/existingPermissions/permissionFormatter.test.ts b/packages/gator-permissions-snap/test/core/existingPermissions/permissionFormatter.test.ts
new file mode 100644
index 00000000..a3b7396e
--- /dev/null
+++ b/packages/gator-permissions-snap/test/core/existingPermissions/permissionFormatter.test.ts
@@ -0,0 +1,211 @@
+import {
+ describe,
+ it,
+ expect,
+ jest,
+ beforeEach,
+ afterEach,
+} from '@jest/globals';
+import type { PermissionResponse } from '@metamask/7715-permissions-shared/types';
+import { logger } from '@metamask/7715-permissions-shared/utils';
+import type { Hex } from '@metamask/utils';
+
+import {
+ formatPermissionWithTokenMetadata,
+ groupPermissionsByFromAddress,
+} from '../../../src/core/existingpermissions/permissionFormatter';
+import { DEFAULT_MAX_AMOUNT } from '../../../src/permissions/erc20TokenStream/context';
+import type { TokenMetadataService } from '../../../src/services/tokenMetadataService';
+
+const fromA = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as Hex;
+const fromB = '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' as Hex;
+const chainId = '0x1' as Hex;
+
+const basePermission = (
+ overrides: Partial,
+): PermissionResponse =>
+ ({
+ chainId,
+ from: fromA,
+ to: '0xcccccccccccccccccccccccccccccccccccccccc' as Hex,
+ context: '0x',
+ dependencies: [],
+ delegationManager: '0xdddddddddddddddddddddddddddddddddddddddd' as Hex,
+ permission: {
+ type: 'erc20-token-stream',
+ data: {},
+ isAdjustmentAllowed: true,
+ },
+ rules: [],
+ ...overrides,
+ }) as PermissionResponse;
+
+describe('groupPermissionsByFromAddress', () => {
+ it('groups permissions by CAIP-10 from address', () => {
+ const p1 = basePermission({
+ permission: {
+ type: 'erc20-token-stream',
+ data: { justification: 'a' },
+ isAdjustmentAllowed: true,
+ },
+ });
+ const p2 = basePermission({
+ from: fromB,
+ permission: {
+ type: 'native-token-stream',
+ data: { justification: 'b' },
+ isAdjustmentAllowed: true,
+ },
+ });
+ const p3 = basePermission({
+ permission: {
+ type: 'erc20-token-periodic',
+ data: { justification: 'c' },
+ isAdjustmentAllowed: true,
+ },
+ });
+
+ const grouped = groupPermissionsByFromAddress([p1, p2, p3]);
+
+ expect(Object.keys(grouped)).toHaveLength(2);
+ expect(grouped[fromA]).toHaveLength(2);
+ expect(grouped[fromB]).toHaveLength(1);
+ });
+
+ it('skips entries without from or chainId', () => {
+ const valid = basePermission({});
+ const missingFrom = basePermission({ from: undefined as unknown as Hex });
+ const missingChain = basePermission({
+ chainId: undefined as unknown as Hex,
+ });
+
+ const grouped = groupPermissionsByFromAddress([
+ valid,
+ missingFrom,
+ missingChain,
+ ]);
+
+ expect(Object.keys(grouped)).toStrictEqual([fromA]);
+ });
+});
+
+describe('formatPermissionWithTokenMetadata', () => {
+ let mockTokenMetadataService: jest.Mocked<
+ Pick
+ >;
+
+ beforeEach(() => {
+ jest.spyOn(logger, 'debug').mockImplementation(() => undefined);
+ const getTokenMetadata = jest
+ .fn()
+ .mockResolvedValue({ decimals: 18, symbol: 'ETH' });
+ mockTokenMetadataService = {
+ getTokenMetadata: getTokenMetadata as jest.MockedFunction<
+ TokenMetadataService['getTokenMetadata']
+ >,
+ };
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('returns the permission unchanged when data has no token amount fields', async () => {
+ const permission = basePermission({
+ permission: {
+ type: 'erc20-token-revocation',
+ data: { justification: 'x' },
+ isAdjustmentAllowed: true,
+ },
+ });
+
+ const result = await formatPermissionWithTokenMetadata(
+ permission,
+ mockTokenMetadataService as unknown as TokenMetadataService,
+ );
+
+ expect(result).toStrictEqual(permission);
+ expect(mockTokenMetadataService.getTokenMetadata).not.toHaveBeenCalled();
+ });
+
+ it('formats maxAmount using token metadata', async () => {
+ const permission = basePermission({
+ permission: {
+ type: 'erc20-token-stream',
+ data: {
+ maxAmount: '0x38d7ea4c68000',
+ tokenAddress: '0x0000000000000000000000000000000000000001',
+ justification: 'j',
+ },
+ isAdjustmentAllowed: true,
+ },
+ });
+
+ const result = await formatPermissionWithTokenMetadata(
+ permission,
+ mockTokenMetadataService as unknown as TokenMetadataService,
+ );
+
+ expect(mockTokenMetadataService.getTokenMetadata).toHaveBeenCalledWith(
+ expect.objectContaining({
+ chainId: 1,
+ account: fromA,
+ assetAddress: '0x0000000000000000000000000000000000000001',
+ }),
+ );
+ expect(result.permission.data).toMatchObject({
+ maxAmount: expect.stringContaining('ETH') as unknown,
+ });
+ });
+
+ it('maps DEFAULT_MAX_AMOUNT maxAmount to unlimited label', async () => {
+ const permission = basePermission({
+ permission: {
+ type: 'erc20-token-stream',
+ data: {
+ maxAmount: DEFAULT_MAX_AMOUNT,
+ tokenAddress: '0x0000000000000000000000000000000000000001',
+ justification: 'j',
+ },
+ isAdjustmentAllowed: true,
+ },
+ });
+
+ const result = await formatPermissionWithTokenMetadata(
+ permission,
+ mockTokenMetadataService as unknown as TokenMetadataService,
+ );
+
+ expect(result.permission.data).toMatchObject({
+ maxAmount: expect.any(String) as unknown,
+ });
+ const { maxAmount } = result.permission.data as { maxAmount: string };
+ expect(maxAmount.toLowerCase().startsWith('0x')).toBe(false);
+ });
+
+ it('returns original permission when metadata fetch fails', async () => {
+ mockTokenMetadataService.getTokenMetadata.mockRejectedValue(
+ new Error('rpc down'),
+ );
+
+ const permission = basePermission({
+ permission: {
+ type: 'erc20-token-stream',
+ data: {
+ maxAmount: '0x1',
+ tokenAddress: '0x0000000000000000000000000000000000000001',
+ justification: 'j',
+ },
+ isAdjustmentAllowed: true,
+ },
+ });
+
+ const result = await formatPermissionWithTokenMetadata(
+ permission,
+ mockTokenMetadataService as unknown as TokenMetadataService,
+ );
+
+ expect(result).toStrictEqual(permission);
+ expect(logger.debug).toHaveBeenCalled();
+ });
+});
diff --git a/packages/gator-permissions-snap/test/core/permissionHandler.test.ts b/packages/gator-permissions-snap/test/core/permissionHandler.test.ts
index ffa1f26b..b48fe451 100644
--- a/packages/gator-permissions-snap/test/core/permissionHandler.test.ts
+++ b/packages/gator-permissions-snap/test/core/permissionHandler.test.ts
@@ -5,6 +5,7 @@ import { UserInputEventType } from '@metamask/snaps-sdk';
import { AddressScanResultType } from '../../src/clients/trustSignalsClient';
import type { TokenBalanceAndMetadata } from '../../src/clients/types';
import type { AccountController } from '../../src/core/accountController';
+import { ExistingPermissionsState } from '../../src/core/existingpermissions/existingPermissionsState';
import { PermissionHandler } from '../../src/core/permissionHandler';
import type { PermissionRequestLifecycleOrchestrator } from '../../src/core/permissionRequestLifecycleOrchestrator';
import type {
@@ -414,6 +415,8 @@ describe('PermissionHandler', () => {
chainId: 1,
scanDappUrlResult: null,
scanAddressResult: null,
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
expect(dependencies.createConfirmationContent).toHaveBeenCalledWith({
@@ -436,10 +439,12 @@ describe('PermissionHandler', () => {
chainId: 1,
scanDappUrlResult: null,
scanAddressResult: null,
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
expect(result).toBeDefined();
- expect(result.type).toBe('Box');
+ expect(result.type).toBe('Container');
});
it('uses translated fallback for address warning when scanAddressResult.label is empty', async () => {
@@ -459,6 +464,8 @@ describe('PermissionHandler', () => {
resultType: AddressScanResultType.Malicious,
label: '',
},
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
// When label is empty, permissionHandlerContent should use t('maliciousAddressLabel')
@@ -495,7 +502,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: true,
});
const accountSelectorBoundEvent = getBoundEvent({
@@ -530,7 +536,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: true,
});
expect(
@@ -558,7 +563,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: true,
});
const accountSelectorChangeHandler = getBoundEvent({
@@ -607,7 +611,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: true,
});
const accountSelectorChangeHandler = getBoundEvent({
@@ -654,7 +657,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: true,
});
const confirmationContent =
@@ -665,212 +667,215 @@ describe('PermissionHandler', () => {
chainId: 1,
scanDappUrlResult: null,
scanAddressResult: null,
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
expect(confirmationContent).toMatchInlineSnapshot(`
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
- "center": true,
"children": [
{
"key": null,
"props": {
- "children": "Permission request",
- "size": "lg",
+ "center": true,
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Permission request",
+ "size": "lg",
+ },
+ "type": "Heading",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "This site wants permissions to spend your tokens.",
+ },
+ "type": "Text",
+ },
+ ],
},
- "type": "Heading",
+ "type": "Box",
},
{
"key": null,
"props": {
- "children": "This site wants permissions to spend your tokens.",
- },
- "type": "Text",
- },
- ],
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
+ "children": {
"key": null,
"props": {
- "alignment": "space-between",
- "children": {
- "key": null,
- "props": {
- "children": [
- {
- "key": null,
- "props": {
- "children": "Account",
- },
- "type": "Text",
- },
- {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "alignment": "space-between",
+ "children": {
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Account",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The account from which the permission is being granted.",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The account from which the permission is being granted.",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
- ],
- "direction": "horizontal",
+ "direction": "horizontal",
+ },
+ "type": "Box",
},
- "type": "Box",
- },
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "chainIds": [
- "eip155:1",
- ],
- "name": "account-selector",
- "switchGlobalAccount": false,
- "value": "eip155:1:0x1234567890123456789012345678901234567890",
- },
- "type": "AccountSelector",
- },
- false,
- {
- "key": null,
- "props": {
- "alignment": "end",
- "children": [
{
"key": null,
- "props": {},
- "type": "Skeleton",
+ "props": {
+ "chainIds": [
+ "eip155:1",
+ ],
+ "name": "account-selector",
+ "switchGlobalAccount": false,
+ "value": "eip155:1:0x1234567890123456789012345678901234567890",
+ },
+ "type": "AccountSelector",
},
+ false,
{
"key": null,
- "props": {},
- "type": "Skeleton",
+ "props": {
+ "alignment": "end",
+ "children": [
+ {
+ "key": null,
+ "props": {},
+ "type": "Skeleton",
+ },
+ {
+ "key": null,
+ "props": {},
+ "type": "Skeleton",
+ },
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
},
],
- "direction": "horizontal",
+ "direction": "vertical",
},
"type": "Box",
},
- ],
- "direction": "vertical",
+ },
+ "type": "Section",
},
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
- {
+ false,
+ false,
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Justification",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Justification",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "Justification given by the recipient for requesting this permission.",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "Justification given by the recipient for requesting this permission.",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
{
"key": null,
"props": {
- "children": "Test justification text that is longer than twenty characters",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Test justification text that is longer than twenty characters",
+ },
+ "type": "Text",
+ },
+ null,
+ ],
+ "direction": "vertical",
},
- "type": "Text",
+ "type": "Box",
},
- null,
],
"direction": "vertical",
},
"type": "Box",
},
- ],
- "direction": "vertical",
+ },
+ "type": "Section",
},
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": [
{
"key": null,
"props": {
- "alignment": "space-between",
"children": [
{
"key": null,
@@ -880,75 +885,75 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Request from",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Request from",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
{
"key": null,
"props": {
- "alignment": "end",
- "children": "https://example.com",
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "https://example.com",
+ },
+ "type": "Text",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -957,33 +962,68 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Recipient",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Recipient",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
{
"key": null,
"props": {
"children": {
"key": null,
"props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
+ "children": "0x12345...67890",
},
"type": "Text",
},
+ "content": "0x1234567890123456789012345678901234567890",
},
"type": "Tooltip",
},
@@ -992,46 +1032,11 @@ describe('PermissionHandler', () => {
},
"type": "Box",
},
- null,
],
"direction": "horizontal",
},
"type": "Box",
},
- {
- "key": null,
- "props": {
- "children": [
- null,
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": "0x12345...67890",
- },
- "type": "Text",
- },
- "content": "0x1234567890123456789012345678901234567890",
- },
- "type": "Tooltip",
- },
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -1040,75 +1045,75 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Network",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Network",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The network on which the permission is being requested",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The network on which the permission is being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
{
"key": null,
"props": {
- "alignment": "end",
- "children": "Ethereum Mainnet",
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "Ethereum Mainnet",
+ },
+ "type": "Text",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -1117,66 +1122,76 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Token",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Token",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The token being requested",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The token being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "children": "ETH",
- "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "children": "ETH",
+ "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
+ },
+ "type": "Link",
+ },
+ "content": "0x38c4A...611F3",
+ },
+ "type": "Tooltip",
},
- "type": "Link",
- },
- "content": "0x38c4A...611F3",
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
],
"direction": "horizontal",
@@ -1184,22 +1199,48 @@ describe('PermissionHandler', () => {
"type": "Box",
},
],
- "direction": "horizontal",
},
- "type": "Box",
+ "type": "Section",
},
+ undefined,
],
+ "direction": "vertical",
},
- "type": "Section",
+ "type": "Box",
},
- undefined,
- ],
- "direction": "vertical",
+ },
+ "type": "Box",
},
- "type": "Box",
- },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Cancel",
+ "name": "cancel-button",
+ "variant": "destructive",
+ },
+ "type": "Button",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "Grant",
+ "disabled": false,
+ "name": "grant-button",
+ "variant": "primary",
+ },
+ "type": "Button",
+ },
+ ],
+ },
+ "type": "Footer",
+ },
+ ],
},
- "type": "Box",
+ "type": "Container",
}
`);
});
@@ -1221,7 +1262,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: true,
});
const accountSelectorChangeHandler = getBoundEvent({
@@ -1253,227 +1293,230 @@ describe('PermissionHandler', () => {
chainId: 1,
scanDappUrlResult: null,
scanAddressResult: null,
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
expect(confirmationContent).toMatchInlineSnapshot(`
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
- "center": true,
"children": [
{
"key": null,
"props": {
- "children": "Permission request",
- "size": "lg",
+ "center": true,
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Permission request",
+ "size": "lg",
+ },
+ "type": "Heading",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "This site wants permissions to spend your tokens.",
+ },
+ "type": "Text",
+ },
+ ],
},
- "type": "Heading",
+ "type": "Box",
},
{
"key": null,
"props": {
- "children": "This site wants permissions to spend your tokens.",
- },
- "type": "Text",
- },
- ],
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
+ "children": {
"key": null,
"props": {
- "alignment": "space-between",
- "children": {
- "key": null,
- "props": {
- "children": [
- {
- "key": null,
- "props": {
- "children": "Account",
- },
- "type": "Text",
- },
- {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "alignment": "space-between",
+ "children": {
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Account",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The account from which the permission is being granted.",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The account from which the permission is being granted.",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "chainIds": [
- "eip155:1",
- ],
- "name": "account-selector",
- "switchGlobalAccount": false,
- "value": "eip155:1:0x1234567890123456789012345678901234567890",
- },
- "type": "AccountSelector",
- },
- {
- "key": null,
- "props": {
- "children": "This account will be upgraded to a smart account to complete this permission.",
- "color": "warning",
- "size": "sm",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "alignment": "end",
- "children": [
- {
- "key": null,
- "props": {},
- "type": "Skeleton",
+ "direction": "horizontal",
+ },
+ "type": "Box",
},
{
"key": null,
"props": {
- "children": [
- "1",
- " ",
- "available",
+ "chainIds": [
+ "eip155:1",
],
+ "name": "account-selector",
+ "switchGlobalAccount": false,
+ "value": "eip155:1:0x1234567890123456789012345678901234567890",
},
- "type": "Text",
+ "type": "AccountSelector",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- ],
- "direction": "vertical",
- },
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
- {
- "key": null,
- "props": {
- "children": [
{
"key": null,
"props": {
- "children": "Justification",
+ "children": "This account will be upgraded to a smart account to complete this permission.",
+ "color": "warning",
+ "size": "sm",
},
"type": "Text",
},
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "alignment": "end",
+ "children": [
+ {
+ "key": null,
+ "props": {},
+ "type": "Skeleton",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "Justification given by the recipient for requesting this permission.",
+ {
+ "key": null,
+ "props": {
+ "children": [
+ "1",
+ " ",
+ "available",
+ ],
+ },
+ "type": "Text",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
],
- "direction": "horizontal",
+ "direction": "vertical",
},
"type": "Box",
},
- {
+ },
+ "type": "Section",
+ },
+ false,
+ false,
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Test justification text that is longer than twenty characters",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Justification",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "Justification given by the recipient for requesting this permission.",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Test justification text that is longer than twenty characters",
+ },
+ "type": "Text",
+ },
+ null,
+ ],
+ "direction": "vertical",
},
- "type": "Text",
+ "type": "Box",
},
- null,
],
"direction": "vertical",
},
"type": "Box",
},
- ],
- "direction": "vertical",
+ },
+ "type": "Section",
},
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": [
{
"key": null,
"props": {
- "alignment": "space-between",
"children": [
{
"key": null,
@@ -1483,75 +1526,75 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Request from",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Request from",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
{
"key": null,
"props": {
- "alignment": "end",
- "children": "https://example.com",
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "https://example.com",
+ },
+ "type": "Text",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -1560,116 +1603,68 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Recipient",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Recipient",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": "0x12345...67890",
- },
- "type": "Text",
- },
- "content": "0x1234567890123456789012345678901234567890",
- },
- "type": "Tooltip",
- },
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
"children": [
+ null,
{
"key": null,
"props": {
- "children": "Network",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
- },
- "content": {
+ "children": {
"key": null,
"props": {
- "children": "The network on which the permission is being requested",
+ "children": "0x12345...67890",
},
"type": "Text",
},
+ "content": "0x1234567890123456789012345678901234567890",
},
"type": "Tooltip",
},
@@ -1678,7 +1673,6 @@ describe('PermissionHandler', () => {
},
"type": "Box",
},
- null,
],
"direction": "horizontal",
},
@@ -1687,31 +1681,80 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
- null,
{
"key": null,
"props": {
- "alignment": "end",
- "children": "Ethereum Mainnet",
+ "alignment": "space-between",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Network",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The network on which the permission is being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "Ethereum Mainnet",
+ },
+ "type": "Text",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -1720,33 +1763,69 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Token",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Token",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The token being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
{
"key": null,
"props": {
"children": {
"key": null,
"props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": "ETH",
+ "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The token being requested",
- },
- "type": "Text",
+ "type": "Link",
},
+ "content": "0x38c4A...611F3",
},
"type": "Tooltip",
},
@@ -1755,54 +1834,54 @@ describe('PermissionHandler', () => {
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": "ETH",
- "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
- },
- "type": "Link",
- },
- "content": "0x38c4A...611F3",
- },
- "type": "Tooltip",
- },
],
"direction": "horizontal",
},
"type": "Box",
},
],
- "direction": "horizontal",
},
- "type": "Box",
+ "type": "Section",
},
+ undefined,
],
+ "direction": "vertical",
},
- "type": "Section",
+ "type": "Box",
},
- undefined,
- ],
- "direction": "vertical",
+ },
+ "type": "Box",
},
- "type": "Box",
- },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Cancel",
+ "name": "cancel-button",
+ "variant": "destructive",
+ },
+ "type": "Button",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "Grant",
+ "disabled": false,
+ "name": "grant-button",
+ "variant": "primary",
+ },
+ "type": "Button",
+ },
+ ],
+ },
+ "type": "Footer",
+ },
+ ],
},
- "type": "Box",
+ "type": "Container",
}
`);
});
@@ -1849,7 +1928,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: true,
});
const accountSelectorChangeHandler = getBoundEvent({
@@ -1890,6 +1968,8 @@ describe('PermissionHandler', () => {
chainId: 1,
scanDappUrlResult: null,
scanAddressResult: null,
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
// skeletons in place of both the token balance and fiat balance
@@ -1897,292 +1977,216 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
- "center": true,
"children": [
{
"key": null,
"props": {
- "children": "Permission request",
- "size": "lg",
+ "center": true,
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Permission request",
+ "size": "lg",
+ },
+ "type": "Heading",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "This site wants permissions to spend your tokens.",
+ },
+ "type": "Text",
+ },
+ ],
},
- "type": "Heading",
+ "type": "Box",
},
{
"key": null,
"props": {
- "children": "This site wants permissions to spend your tokens.",
- },
- "type": "Text",
- },
- ],
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
+ "children": {
"key": null,
"props": {
- "alignment": "space-between",
- "children": {
- "key": null,
- "props": {
- "children": [
- {
- "key": null,
- "props": {
- "children": "Account",
- },
- "type": "Text",
- },
- {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "alignment": "space-between",
+ "children": {
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Account",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The account from which the permission is being granted.",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The account from which the permission is being granted.",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "chainIds": [
- "eip155:1",
- ],
- "name": "account-selector",
- "switchGlobalAccount": false,
- "value": "eip155:1:0x1234567890123456789012345678901234567890",
- },
- "type": "AccountSelector",
- },
- {
- "key": null,
- "props": {
- "children": "This account will be upgraded to a smart account to complete this permission.",
- "color": "warning",
- "size": "sm",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "alignment": "end",
- "children": [
- {
- "key": null,
- "props": {},
- "type": "Skeleton",
+ "direction": "horizontal",
+ },
+ "type": "Box",
},
{
"key": null,
- "props": {},
- "type": "Skeleton",
+ "props": {
+ "chainIds": [
+ "eip155:1",
+ ],
+ "name": "account-selector",
+ "switchGlobalAccount": false,
+ "value": "eip155:1:0x1234567890123456789012345678901234567890",
+ },
+ "type": "AccountSelector",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- ],
- "direction": "vertical",
- },
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
- {
- "key": null,
- "props": {
- "children": [
{
"key": null,
"props": {
- "children": "Justification",
+ "children": "This account will be upgraded to a smart account to complete this permission.",
+ "color": "warning",
+ "size": "sm",
},
"type": "Text",
},
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "alignment": "end",
+ "children": [
+ {
+ "key": null,
+ "props": {},
+ "type": "Skeleton",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "Justification given by the recipient for requesting this permission.",
+ {
+ "key": null,
+ "props": {},
+ "type": "Skeleton",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
],
- "direction": "horizontal",
+ "direction": "vertical",
},
"type": "Box",
},
- {
+ },
+ "type": "Section",
+ },
+ false,
+ false,
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Test justification text that is longer than twenty characters",
- },
- "type": "Text",
- },
- null,
- ],
- "direction": "vertical",
- },
- "type": "Box",
- },
- ],
- "direction": "vertical",
- },
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": [
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
- {
- "key": null,
- "props": {
- "children": [
- {
- "key": null,
- "props": {
- "children": "Request from",
- },
- "type": "Text",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Justification",
},
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
},
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
- },
- "type": "Text",
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "Justification given by the recipient for requesting this permission.",
},
+ "type": "Text",
},
- "type": "Tooltip",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
- {
- "key": null,
- "props": {
- "alignment": "end",
- "children": "https://example.com",
- },
- "type": "Text",
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Test justification text that is longer than twenty characters",
+ },
+ "type": "Text",
+ },
+ null,
+ ],
+ "direction": "vertical",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
+ "type": "Box",
+ },
+ ],
+ "direction": "vertical",
},
- ],
- "direction": "horizontal",
+ "type": "Box",
+ },
},
- "type": "Box",
+ "type": "Section",
},
{
"key": null,
"props": {
- "alignment": "space-between",
"children": [
{
"key": null,
@@ -2192,81 +2196,75 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Recipient",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Request from",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "children": "0x12345...67890",
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "https://example.com",
+ },
+ "type": "Text",
},
- "type": "Text",
- },
- "content": "0x1234567890123456789012345678901234567890",
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -2275,33 +2273,68 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Network",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Recipient",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
{
"key": null,
"props": {
"children": {
"key": null,
"props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The network on which the permission is being requested",
+ "children": "0x12345...67890",
},
"type": "Text",
},
+ "content": "0x1234567890123456789012345678901234567890",
},
"type": "Tooltip",
},
@@ -2310,7 +2343,6 @@ describe('PermissionHandler', () => {
},
"type": "Box",
},
- null,
],
"direction": "horizontal",
},
@@ -2319,31 +2351,80 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
- null,
{
"key": null,
"props": {
- "alignment": "end",
- "children": "Ethereum Mainnet",
+ "alignment": "space-between",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Network",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The network on which the permission is being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "Ethereum Mainnet",
+ },
+ "type": "Text",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -2352,33 +2433,69 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Token",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Token",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The token being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
{
"key": null,
"props": {
"children": {
"key": null,
"props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The token being requested",
+ "children": "ETH",
+ "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
},
- "type": "Text",
+ "type": "Link",
},
+ "content": "0x38c4A...611F3",
},
"type": "Tooltip",
},
@@ -2387,54 +2504,54 @@ describe('PermissionHandler', () => {
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": "ETH",
- "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
- },
- "type": "Link",
- },
- "content": "0x38c4A...611F3",
- },
- "type": "Tooltip",
- },
],
"direction": "horizontal",
},
"type": "Box",
},
],
- "direction": "horizontal",
},
- "type": "Box",
+ "type": "Section",
},
+ undefined,
],
+ "direction": "vertical",
},
- "type": "Section",
+ "type": "Box",
},
- undefined,
- ],
- "direction": "vertical",
+ },
+ "type": "Box",
},
- "type": "Box",
- },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Cancel",
+ "name": "cancel-button",
+ "variant": "destructive",
+ },
+ "type": "Button",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "Grant",
+ "disabled": false,
+ "name": "grant-button",
+ "variant": "primary",
+ },
+ "type": "Button",
+ },
+ ],
+ },
+ "type": "Footer",
+ },
+ ],
},
- "type": "Box",
+ "type": "Container",
}
`);
@@ -2452,6 +2569,8 @@ describe('PermissionHandler', () => {
chainId: 1,
scanDappUrlResult: null,
scanAddressResult: null,
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
// concrete token balance, skeleton for fiat balance
@@ -2459,221 +2578,222 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
- "center": true,
"children": [
{
"key": null,
"props": {
- "children": "Permission request",
- "size": "lg",
+ "center": true,
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Permission request",
+ "size": "lg",
+ },
+ "type": "Heading",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "This site wants permissions to spend your tokens.",
+ },
+ "type": "Text",
+ },
+ ],
},
- "type": "Heading",
+ "type": "Box",
},
{
"key": null,
"props": {
- "children": "This site wants permissions to spend your tokens.",
- },
- "type": "Text",
- },
- ],
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
+ "children": {
"key": null,
"props": {
- "alignment": "space-between",
- "children": {
- "key": null,
- "props": {
- "children": [
- {
- "key": null,
- "props": {
- "children": "Account",
- },
- "type": "Text",
- },
- {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "alignment": "space-between",
+ "children": {
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Account",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The account from which the permission is being granted.",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The account from which the permission is being granted.",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
- ],
- "direction": "horizontal",
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "chainIds": [
+ "eip155:1",
+ ],
+ "name": "account-selector",
+ "switchGlobalAccount": false,
+ "value": "eip155:1:0x1234567890123456789012345678901234567890",
+ },
+ "type": "AccountSelector",
},
- "type": "Box",
- },
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "chainIds": [
- "eip155:1",
- ],
- "name": "account-selector",
- "switchGlobalAccount": false,
- "value": "eip155:1:0x1234567890123456789012345678901234567890",
- },
- "type": "AccountSelector",
- },
- {
- "key": null,
- "props": {
- "children": "This account will be upgraded to a smart account to complete this permission.",
- "color": "warning",
- "size": "sm",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "alignment": "end",
- "children": [
{
"key": null,
- "props": {},
- "type": "Skeleton",
+ "props": {
+ "children": "This account will be upgraded to a smart account to complete this permission.",
+ "color": "warning",
+ "size": "sm",
+ },
+ "type": "Text",
},
{
"key": null,
"props": {
+ "alignment": "end",
"children": [
- "1",
- " ",
- "available",
+ {
+ "key": null,
+ "props": {},
+ "type": "Skeleton",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ "1",
+ " ",
+ "available",
+ ],
+ },
+ "type": "Text",
+ },
],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
- "direction": "horizontal",
+ "direction": "vertical",
},
"type": "Box",
},
- ],
- "direction": "vertical",
+ },
+ "type": "Section",
},
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
- {
+ false,
+ false,
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Justification",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Justification",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "Justification given by the recipient for requesting this permission.",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "Justification given by the recipient for requesting this permission.",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
{
"key": null,
"props": {
- "children": "Test justification text that is longer than twenty characters",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Test justification text that is longer than twenty characters",
+ },
+ "type": "Text",
+ },
+ null,
+ ],
+ "direction": "vertical",
},
- "type": "Text",
+ "type": "Box",
},
- null,
],
"direction": "vertical",
},
"type": "Box",
},
- ],
- "direction": "vertical",
+ },
+ "type": "Section",
},
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": [
{
"key": null,
"props": {
- "alignment": "space-between",
"children": [
{
"key": null,
@@ -2683,75 +2803,75 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Request from",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Request from",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
{
"key": null,
"props": {
- "alignment": "end",
- "children": "https://example.com",
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "https://example.com",
+ },
+ "type": "Text",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -2760,33 +2880,68 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Recipient",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Recipient",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
{
"key": null,
"props": {
"children": {
"key": null,
"props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
+ "children": "0x12345...67890",
},
"type": "Text",
},
+ "content": "0x1234567890123456789012345678901234567890",
},
"type": "Tooltip",
},
@@ -2795,46 +2950,11 @@ describe('PermissionHandler', () => {
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": "0x12345...67890",
- },
- "type": "Text",
- },
- "content": "0x1234567890123456789012345678901234567890",
- },
- "type": "Tooltip",
- },
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -2843,75 +2963,75 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Network",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Network",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The network on which the permission is being requested",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The network on which the permission is being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
{
"key": null,
"props": {
- "alignment": "end",
- "children": "Ethereum Mainnet",
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "Ethereum Mainnet",
+ },
+ "type": "Text",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -2920,33 +3040,69 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Token",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Token",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The token being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
{
"key": null,
"props": {
"children": {
"key": null,
"props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The token being requested",
+ "children": "ETH",
+ "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
},
- "type": "Text",
+ "type": "Link",
},
+ "content": "0x38c4A...611F3",
},
"type": "Tooltip",
},
@@ -2955,54 +3111,54 @@ describe('PermissionHandler', () => {
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": "ETH",
- "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
- },
- "type": "Link",
- },
- "content": "0x38c4A...611F3",
- },
- "type": "Tooltip",
- },
],
"direction": "horizontal",
},
"type": "Box",
},
],
- "direction": "horizontal",
},
- "type": "Box",
+ "type": "Section",
},
+ undefined,
],
+ "direction": "vertical",
},
- "type": "Section",
+ "type": "Box",
},
- undefined,
- ],
- "direction": "vertical",
+ },
+ "type": "Box",
},
- "type": "Box",
- },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Cancel",
+ "name": "cancel-button",
+ "variant": "destructive",
+ },
+ "type": "Button",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "Grant",
+ "disabled": false,
+ "name": "grant-button",
+ "variant": "primary",
+ },
+ "type": "Button",
+ },
+ ],
+ },
+ "type": "Footer",
+ },
+ ],
},
- "type": "Box",
+ "type": "Container",
}
`);
@@ -3020,6 +3176,8 @@ describe('PermissionHandler', () => {
chainId: 1,
scanDappUrlResult: null,
scanAddressResult: null,
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
// concrete token balance, concrete fiat balance
@@ -3027,223 +3185,224 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
- "center": true,
"children": [
{
"key": null,
"props": {
- "children": "Permission request",
- "size": "lg",
+ "center": true,
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Permission request",
+ "size": "lg",
+ },
+ "type": "Heading",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "This site wants permissions to spend your tokens.",
+ },
+ "type": "Text",
+ },
+ ],
},
- "type": "Heading",
+ "type": "Box",
},
{
"key": null,
"props": {
- "children": "This site wants permissions to spend your tokens.",
- },
- "type": "Text",
- },
- ],
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": [
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": {
- "key": null,
- "props": {
- "children": [
- {
- "key": null,
- "props": {
- "children": "Account",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The account from which the permission is being granted.",
- },
- "type": "Text",
- },
- },
- "type": "Tooltip",
- },
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "chainIds": [
- "eip155:1",
- ],
- "name": "account-selector",
- "switchGlobalAccount": false,
- "value": "eip155:1:0x1234567890123456789012345678901234567890",
- },
- "type": "AccountSelector",
- },
- {
- "key": null,
- "props": {
- "children": "This account will be upgraded to a smart account to complete this permission.",
- "color": "warning",
- "size": "sm",
- },
- "type": "Text",
- },
- {
+ "children": {
"key": null,
"props": {
- "alignment": "end",
"children": [
{
"key": null,
"props": {
- "children": "$1000",
+ "alignment": "space-between",
+ "children": {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Account",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The account from which the permission is being granted.",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
{
"key": null,
"props": {
- "children": [
- "1",
- " ",
- "available",
+ "chainIds": [
+ "eip155:1",
],
+ "name": "account-selector",
+ "switchGlobalAccount": false,
+ "value": "eip155:1:0x1234567890123456789012345678901234567890",
},
- "type": "Text",
+ "type": "AccountSelector",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- ],
- "direction": "vertical",
- },
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
- {
- "key": null,
- "props": {
- "children": [
{
"key": null,
"props": {
- "children": "Justification",
+ "children": "This account will be upgraded to a smart account to complete this permission.",
+ "color": "warning",
+ "size": "sm",
},
"type": "Text",
},
{
"key": null,
"props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "alignment": "end",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "$1000",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "Justification given by the recipient for requesting this permission.",
+ {
+ "key": null,
+ "props": {
+ "children": [
+ "1",
+ " ",
+ "available",
+ ],
+ },
+ "type": "Text",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
],
- "direction": "horizontal",
+ "direction": "vertical",
},
"type": "Box",
},
- {
+ },
+ "type": "Section",
+ },
+ false,
+ false,
+ {
+ "key": null,
+ "props": {
+ "children": {
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Test justification text that is longer than twenty characters",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Justification",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "Justification given by the recipient for requesting this permission.",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Test justification text that is longer than twenty characters",
+ },
+ "type": "Text",
+ },
+ null,
+ ],
+ "direction": "vertical",
},
- "type": "Text",
+ "type": "Box",
},
- null,
],
"direction": "vertical",
},
"type": "Box",
},
- ],
- "direction": "vertical",
+ },
+ "type": "Section",
},
- "type": "Box",
- },
- },
- "type": "Section",
- },
- {
- "key": null,
- "props": {
- "children": [
{
"key": null,
"props": {
- "alignment": "space-between",
"children": [
{
"key": null,
@@ -3253,75 +3412,75 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Request from",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Request from",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
{
"key": null,
"props": {
- "alignment": "end",
- "children": "https://example.com",
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "https://example.com",
+ },
+ "type": "Text",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -3330,33 +3489,68 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Recipient",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Recipient",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The site requesting the permission",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
{
"key": null,
"props": {
"children": {
"key": null,
"props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The site requesting the permission",
+ "children": "0x12345...67890",
},
"type": "Text",
},
+ "content": "0x1234567890123456789012345678901234567890",
},
"type": "Tooltip",
},
@@ -3365,46 +3559,11 @@ describe('PermissionHandler', () => {
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": "0x12345...67890",
- },
- "type": "Text",
- },
- "content": "0x1234567890123456789012345678901234567890",
- },
- "type": "Tooltip",
- },
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -3413,75 +3572,75 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Network",
- },
- "type": "Text",
- },
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Network",
+ },
+ "type": "Text",
},
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The network on which the permission is being requested",
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The network on which the permission is being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
},
- "type": "Text",
- },
+ ],
+ "direction": "horizontal",
},
- "type": "Tooltip",
+ "type": "Box",
},
+ null,
],
"direction": "horizontal",
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
{
"key": null,
"props": {
- "alignment": "end",
- "children": "Ethereum Mainnet",
+ "children": [
+ null,
+ {
+ "key": null,
+ "props": {
+ "alignment": "end",
+ "children": "Ethereum Mainnet",
+ },
+ "type": "Text",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
],
"direction": "horizontal",
},
"type": "Box",
},
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "alignment": "space-between",
- "children": [
{
"key": null,
"props": {
@@ -3490,33 +3649,69 @@ describe('PermissionHandler', () => {
{
"key": null,
"props": {
+ "alignment": "space-between",
"children": [
{
"key": null,
"props": {
- "children": "Token",
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Token",
+ },
+ "type": "Text",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": {
+ "key": null,
+ "props": {
+ "color": "muted",
+ "name": "question",
+ "size": "inherit",
+ },
+ "type": "Icon",
+ },
+ "content": {
+ "key": null,
+ "props": {
+ "children": "The token being requested",
+ },
+ "type": "Text",
+ },
+ },
+ "type": "Tooltip",
+ },
+ ],
+ "direction": "horizontal",
},
- "type": "Text",
+ "type": "Box",
},
+ null,
+ ],
+ "direction": "horizontal",
+ },
+ "type": "Box",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ null,
{
"key": null,
"props": {
"children": {
"key": null,
"props": {
- "color": "muted",
- "name": "question",
- "size": "inherit",
- },
- "type": "Icon",
- },
- "content": {
- "key": null,
- "props": {
- "children": "The token being requested",
+ "children": "ETH",
+ "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
},
- "type": "Text",
+ "type": "Link",
},
+ "content": "0x38c4A...611F3",
},
"type": "Tooltip",
},
@@ -3525,54 +3720,54 @@ describe('PermissionHandler', () => {
},
"type": "Box",
},
- null,
- ],
- "direction": "horizontal",
- },
- "type": "Box",
- },
- {
- "key": null,
- "props": {
- "children": [
- null,
- {
- "key": null,
- "props": {
- "children": {
- "key": null,
- "props": {
- "children": "ETH",
- "href": "https://etherscan.io/address/0x38c4A4F071d33d6Cf83e2e81F12D9B5D30E611F3",
- },
- "type": "Link",
- },
- "content": "0x38c4A...611F3",
- },
- "type": "Tooltip",
- },
],
"direction": "horizontal",
},
"type": "Box",
},
],
- "direction": "horizontal",
},
- "type": "Box",
+ "type": "Section",
},
+ undefined,
],
+ "direction": "vertical",
},
- "type": "Section",
+ "type": "Box",
},
- undefined,
- ],
- "direction": "vertical",
+ },
+ "type": "Box",
},
- "type": "Box",
- },
+ {
+ "key": null,
+ "props": {
+ "children": [
+ {
+ "key": null,
+ "props": {
+ "children": "Cancel",
+ "name": "cancel-button",
+ "variant": "destructive",
+ },
+ "type": "Button",
+ },
+ {
+ "key": null,
+ "props": {
+ "children": "Grant",
+ "disabled": false,
+ "name": "grant-button",
+ "variant": "primary",
+ },
+ "type": "Button",
+ },
+ ],
+ },
+ "type": "Footer",
+ },
+ ],
},
- "type": "Box",
+ "type": "Container",
}
`);
});
@@ -3608,7 +3803,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: true,
});
const accountSelectorChangeHandler = getBoundEvent({
@@ -3662,7 +3856,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: true,
});
const accountSelectorBoundEvent = getBoundEvent({
@@ -3725,7 +3918,6 @@ describe('PermissionHandler', () => {
interfaceId: mockInterfaceId,
initialContext: mockContext,
updateContext,
- isAdjustmentAllowed: false, // Adjustment not allowed
});
// Try to get a rule input handler - it should still be bound
diff --git a/packages/gator-permissions-snap/test/core/permissionRequestLifecycleOrchestrator.test.ts b/packages/gator-permissions-snap/test/core/permissionRequestLifecycleOrchestrator.test.ts
index 94223561..9ff46353 100644
--- a/packages/gator-permissions-snap/test/core/permissionRequestLifecycleOrchestrator.test.ts
+++ b/packages/gator-permissions-snap/test/core/permissionRequestLifecycleOrchestrator.test.ts
@@ -20,12 +20,17 @@ import { getChainMetadata } from '../../src/core/chainMetadata';
import type { ConfirmationDialog } from '../../src/core/confirmation';
import type { ConfirmationDialogFactory } from '../../src/core/confirmationFactory';
import type { DialogInterfaceFactory } from '../../src/core/dialogInterfaceFactory';
+import {
+ ExistingPermissionsService,
+ ExistingPermissionsState,
+} from '../../src/core/existingpermissions/existingPermissionsService';
import type { PermissionIntroductionService } from '../../src/core/permissionIntroduction';
import { PermissionRequestLifecycleOrchestrator } from '../../src/core/permissionRequestLifecycleOrchestrator';
import type { BaseContext } from '../../src/core/types';
+import type { ProfileSyncManager } from '../../src/profileSync/profileSync';
+import type { NonceCaveatService } from '../../src/services/nonceCaveatService';
import type { SnapsMetricsService } from '../../src/services/snapsMetricsService';
-import { ExistingPermissionsService } from 'src/core/existingpermissions/existingPermissionsService';
-import type { NonceCaveatService } from 'src/services/nonceCaveatService';
+import type { TokenMetadataService } from '../../src/services/tokenMetadataService';
const randomAddress = (): Hex => {
const randomBytes = new Uint8Array(20);
@@ -136,11 +141,32 @@ const mockPermissionIntroductionService = {
showIntroduction: jest.fn().mockResolvedValue({ wasCancelled: false }),
} as unknown as jest.Mocked;
+const existingPermissionsStatusHelper = new ExistingPermissionsService({
+ profileSyncManager: {
+ getAllGrantedPermissions: jest.fn(),
+ } as unknown as ProfileSyncManager,
+ tokenMetadataService: {} as unknown as TokenMetadataService,
+});
+
const mockExistingPermissionsService = {
- getExistingPermissions: jest.fn().mockResolvedValue([]),
- showExistingPermissions: jest.fn().mockResolvedValue({ wasCancelled: false }),
+ getExistingPermissions: jest.fn(),
+ getExistingPermissionsStatusFromList: jest.fn(),
+ showExistingPermissions: jest.fn(),
} as unknown as jest.Mocked;
+// Set default mock implementations for existing permissions service
+mockExistingPermissionsService.getExistingPermissions.mockResolvedValue([]);
+mockExistingPermissionsService.getExistingPermissionsStatusFromList.mockImplementation(
+ (list, perm) =>
+ existingPermissionsStatusHelper.getExistingPermissionsStatusFromList(
+ list,
+ perm,
+ ),
+);
+mockExistingPermissionsService.showExistingPermissions.mockResolvedValue(
+ undefined,
+);
+
const mockScanAddressResult: FetchAddressScanResult = {
resultType: AddressScanResultType.Benign,
label: '',
@@ -171,6 +197,19 @@ describe('PermissionRequestLifecycleOrchestrator', () => {
beforeEach(() => {
jest.clearAllMocks();
+ // Reset existing permissions service mocks after clearing
+ mockExistingPermissionsService.getExistingPermissions.mockResolvedValue([]);
+ mockExistingPermissionsService.getExistingPermissionsStatusFromList.mockImplementation(
+ (list, perm) =>
+ existingPermissionsStatusHelper.getExistingPermissionsStatusFromList(
+ list,
+ perm,
+ ),
+ );
+ mockExistingPermissionsService.showExistingPermissions.mockResolvedValue(
+ undefined,
+ );
+
lifecycleHandlerMocks = {
parseAndValidatePermission: jest.fn().mockImplementation((req) => req),
buildContext: jest.fn().mockResolvedValue(mockContext),
@@ -277,6 +316,21 @@ describe('PermissionRequestLifecycleOrchestrator', () => {
});
});
+ it('loads existing permissions from profile sync only once per request', async () => {
+ await permissionRequestLifecycleOrchestrator.orchestrate(
+ 'test-origin',
+ mockPermissionRequest,
+ lifecycleHandlerMocks,
+ );
+
+ expect(
+ mockExistingPermissionsService.getExistingPermissions,
+ ).toHaveBeenCalledTimes(1);
+ expect(
+ mockExistingPermissionsService.getExistingPermissions,
+ ).toHaveBeenCalledWith('test-origin');
+ });
+
it('creates a skeleton confirmation before the context is resolved', async () => {
// this never resolves, because we are testing the behavior _before_ the context is returned.
const contextPromise = new Promise((_resolve) => {
@@ -345,7 +399,6 @@ describe('PermissionRequestLifecycleOrchestrator', () => {
expect(mockConfirmationDialog.updateContent).toHaveBeenCalledWith({
ui: mockUiContent,
- isGrantDisabled: false,
});
});
@@ -738,7 +791,7 @@ describe('PermissionRequestLifecycleOrchestrator', () => {
});
it('serializes consecutive updateConfirmation calls so they run in order and do not overwrite each other', async () => {
- // updateConfirmation is called with only newContext and isGrantDisabled; scan results are read from closure variables.
+ // updateConfirmation is called with optional newContext; scan results are read from closure variables.
mockTrustSignalsClient.scanDappUrl.mockImplementationOnce(
async (): Promise =>
new Promise(() => {}),
@@ -1007,6 +1060,8 @@ describe('PermissionRequestLifecycleOrchestrator', () => {
chainId: 1,
scanDappUrlResult: null,
scanAddressResult: null,
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
// Final call has both scan results from closure (after both background scans resolve)
@@ -1019,6 +1074,8 @@ describe('PermissionRequestLifecycleOrchestrator', () => {
chainId: 1,
scanDappUrlResult: { isComplete: false },
scanAddressResult: mockScanAddressResult,
+ existingPermissionsStatus: ExistingPermissionsState.None,
+ isGrantDisabled: false,
});
expect(mockTrustSignalsClient.fetchAddressScan).toHaveBeenCalledWith(
@@ -1028,7 +1085,6 @@ describe('PermissionRequestLifecycleOrchestrator', () => {
expect(mockConfirmationDialog.updateContent).toHaveBeenCalledWith({
ui: mockUiContent,
- isGrantDisabled: false,
});
});