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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions packages/gator-permissions-snap/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
43 changes: 8 additions & 35 deletions packages/gator-permissions-snap/src/core/confirmation.tsx
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -25,8 +23,6 @@ export class ConfirmationDialog {

#ui: SnapElement;

#isGrantDisabled = true;

#timeout: Timeout | undefined;

#hasTimedOut = false;
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -184,41 +180,18 @@ export class ConfirmationDialog {
}

#buildConfirmation(): JSX.Element {
return (
<Container>
{this.#ui}
<Footer>
<Button name={ConfirmationDialog.#cancelButton} variant="destructive">
{t('cancelButton')}
</Button>
<Button
name={ConfirmationDialog.#grantButton}
variant="primary"
disabled={this.#isGrantDisabled}
>
{t('grantButton')}
</Button>
</Footer>
</Container>
);
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<void> {
async updateContent({ ui }: { ui: SnapElement }): Promise<void> {
this.#ui = ui;
this.#isGrantDisabled = isGrantDisabled;

await this.#dialogInterface.show(this.#buildConfirmation());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -54,14 +49,12 @@ export function buildExistingPermissionsSkeletonContent(
<Text fontWeight="bold">{t('accountLabel')}</Text>
<Skeleton />
</Box>
<Divider />
{/* Show 2 skeleton permission cards */}
{[0, 1].map((permIndex) => (
<Box
key={`skeleton-permission-${permIndex}`}
direction="vertical"
>
{permIndex > 0 && <Divider />}
<Box direction="vertical">
<Skeleton />
<Skeleton />
Expand All @@ -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 (
<Container>
<Box direction="vertical">
<Box center={true}>
<Heading size="lg">{t(title)}</Heading>
<Text>{t(description)}</Text>
<Text>{t('existingPermissionsLoadError')}</Text>
</Box>
</Box>
<Footer>
<Button name={EXISTING_PERMISSIONS_CONFIRM_BUTTON}>
{t(buttonLabel)}
</Button>
</Footer>
</Container>
);
}

/**
* 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.
Expand All @@ -105,42 +130,20 @@ export function buildExistingPermissionsContent(
</Box>

{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 (
<Section key={`account-${accountAddress}`}>
<Box direction="vertical">
<Box direction="horizontal" alignment="space-between">
<Text fontWeight="bold">{t('accountLabel')}</Text>
<Address address={accountAddress as Hex} displayName={true} />
</Box>
<Divider />
{displayedPermissions.map((detail, index) => (
<PermissionCard
key={`permission-${index}`}
detail={detail}
index={index}
/>
))}
{hasMorePermissions && (
<Box direction="vertical">
<Divider />
<Text>
{moreCount === 1
? t('morePermissionsCountSingle')
: t('morePermissionsCountPlural', [String(moreCount)])}
<Bold>{t('dappConnectionsLink')}</Bold>
</Text>
</Box>
)}
</Box>
</Section>
<Box key={`account-${accountAddress}`} direction="vertical">
<Section direction="horizontal" alignment="space-between">
<Text fontWeight="bold">{t('accountLabel')}</Text>
<Address address={accountAddress as Hex} displayName={true} />
</Section>
{permissions.map((detail, index) => (
<PermissionCard
key={`${accountAddress}-${index}`}
detail={detail}
index={index}
/>
))}
</Box>
);
})}
</Box>
Expand Down
Loading
Loading