Skip to content

Commit 83856eb

Browse files
authored
Refactor snap site architecture and delegated execution error handling (#340)
* feat: add PermissionResponsePanel and RedeemPermissionPanel components - Implemented PermissionResponsePanel for displaying permission responses with copy functionality. - Created RedeemPermissionPanel for handling permission redemption with user operation receipt display. - Updated RedemptionForm to handle new nonce encoding. - Introduced utility functions for decoding permission contexts and formatting delegated execution errors. - Added support for chain configuration and improved clipboard copy functionality across components. - Refactored index page to utilize new components and hooks for better state management and user experience. * feat: update dependencies and add new permission types for improved functionality * feat: add ErrorAlert component and error display utility functions * fix: update @metamask/smart-accounts-kit to version 1.6.0 and adjust dependencies * refactor: simplify bundler client and delegate account logic, improve error handling
1 parent 59a8e32 commit 83856eb

23 files changed

Lines changed: 892 additions & 457 deletions

packages/site/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"dependencies": {
3131
"@metamask/7715-permission-types": "0.7.1",
3232
"@metamask/providers": "22.1.1",
33-
"@metamask/smart-accounts-kit": "^1.5.0",
33+
"@metamask/smart-accounts-kit": "^1.6.0",
3434
"@metamask/utils": "11.11.0",
3535
"react": "19.2.3",
3636
"react-dom": "19.2.3",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useMemo, useState } from 'react';
2+
import styled from 'styled-components';
3+
4+
import { ErrorMessage } from '../styles';
5+
import { getErrorDisplay } from '../utils';
6+
7+
type ErrorAlertProps = {
8+
error: unknown;
9+
};
10+
11+
const ErrorSummary = styled.p`
12+
margin: 0;
13+
`;
14+
15+
const DetailsButton = styled.button`
16+
background-color: transparent;
17+
border-color: ${({ theme }) => theme.colors.error?.default};
18+
color: ${({ theme }) => theme.colors.error?.alternative};
19+
margin-top: 1.2rem;
20+
min-height: 3.6rem;
21+
padding: 0.7rem 1rem;
22+
23+
&:hover {
24+
background-color: ${({ theme }) => theme.colors.error?.muted};
25+
border-color: ${({ theme }) => theme.colors.error?.default};
26+
color: ${({ theme }) => theme.colors.error?.alternative};
27+
}
28+
`;
29+
30+
const ErrorDetails = styled.pre`
31+
background-color: ${({ theme }) => theme.colors.background?.alternative};
32+
border: 1px solid ${({ theme }) => theme.colors.border?.default};
33+
border-radius: ${({ theme }) => theme.radii.button};
34+
color: ${({ theme }) => theme.colors.text?.default};
35+
font-family: ${({ theme }) => theme.fonts.code};
36+
font-size: ${({ theme }) => theme.fontSizes.small};
37+
line-height: 1.5;
38+
margin: 1.2rem 0 0;
39+
max-height: 28rem;
40+
overflow: auto;
41+
padding: 1.2rem;
42+
white-space: pre-wrap;
43+
word-break: break-word;
44+
`;
45+
46+
export const ErrorAlert = ({ error }: ErrorAlertProps) => {
47+
const [isExpanded, setIsExpanded] = useState(false);
48+
const { summary, details } = useMemo(() => getErrorDisplay(error), [error]);
49+
const hasDetails = details !== summary;
50+
51+
return (
52+
<ErrorMessage role="alert">
53+
<ErrorSummary>
54+
<b>An error happened:</b> {summary}
55+
</ErrorSummary>
56+
{hasDetails && (
57+
<>
58+
<DetailsButton
59+
aria-expanded={isExpanded}
60+
onClick={() => setIsExpanded((currentValue) => !currentValue)}
61+
type="button"
62+
>
63+
{isExpanded ? 'Hide details' : 'Show details'}
64+
</DetailsButton>
65+
{isExpanded && <ErrorDetails>{details}</ErrorDetails>}
66+
</>
67+
)}
68+
</ErrorMessage>
69+
);
70+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ConnectButton, InstallMetaMaskButton } from './Buttons';
2+
import { Card } from './Card';
3+
4+
type SnapConnectionCardsProps = {
5+
isGatorSnapReady: boolean;
6+
isKernelSnapReady: boolean;
7+
isMetaMaskReady: boolean;
8+
onRequestKernelSnap: () => Promise<void> | void;
9+
onRequestPermissionSnap: () => Promise<void> | void;
10+
};
11+
12+
export const SnapConnectionCards = ({
13+
isGatorSnapReady,
14+
isKernelSnapReady,
15+
isMetaMaskReady,
16+
onRequestKernelSnap,
17+
onRequestPermissionSnap,
18+
}: SnapConnectionCardsProps) => (
19+
<>
20+
{!isMetaMaskReady && (
21+
<Card
22+
content={{
23+
title: 'Install',
24+
description:
25+
'Snaps requires MetaMask to be installed. Install MetaMask to get started.',
26+
button: <InstallMetaMaskButton />,
27+
}}
28+
fullWidth
29+
/>
30+
)}
31+
32+
<Card
33+
content={{
34+
title: `${isKernelSnapReady ? 'Reconnect' : 'Connect'}(kernel)`,
35+
description:
36+
'Get started by connecting to and installing the kernel snap.',
37+
button: (
38+
<ConnectButton
39+
onClick={onRequestKernelSnap}
40+
disabled={!isMetaMaskReady}
41+
$isReconnect={isKernelSnapReady}
42+
/>
43+
),
44+
}}
45+
disabled={!isMetaMaskReady}
46+
/>
47+
48+
<Card
49+
content={{
50+
title: `${isGatorSnapReady ? 'Reconnect' : 'Connect'}(provider)`,
51+
description:
52+
'Get started by connecting to and installing the permission provider snap.',
53+
button: (
54+
<ConnectButton
55+
onClick={onRequestPermissionSnap}
56+
disabled={!isMetaMaskReady}
57+
$isReconnect={isGatorSnapReady}
58+
/>
59+
),
60+
}}
61+
disabled={!isMetaMaskReady}
62+
/>
63+
</>
64+
);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
export * from './Buttons';
22
export * from './Card';
3+
export * from './ErrorAlert';
34
export * from './Footer';
45
export * from './Header';
56
export * from './MetaMask';
67
export * from './PoweredBy';
78
export * from './SnapLogo';
9+
export * from './SnapConnectionCards';
810
export * from './Toggle';
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Box, CopyButton, ResponseContainer } from '../../styles';
2+
import { stringifyWithBigInt } from '../../utils';
3+
import { CustomMessageButton } from '../Buttons';
4+
import { Title } from '../Card';
5+
6+
type PermissionQueriesPanelProps = {
7+
grantedIsCopied: boolean;
8+
grantedPermissionsResponse: unknown;
9+
onCopyGrantedPermissions: () => void;
10+
onCopySupportedPermissions: () => void;
11+
onGetGrantedPermissions: () => void;
12+
onGetSupportedPermissions: () => void;
13+
supportedIsCopied: boolean;
14+
supportedPermissionsResponse: unknown;
15+
};
16+
17+
const permissionQueryActionsStyle = {
18+
display: 'flex',
19+
gap: '1rem',
20+
marginTop: '1rem',
21+
marginBottom: '1rem',
22+
};
23+
24+
export const PermissionQueriesPanel = ({
25+
grantedIsCopied,
26+
grantedPermissionsResponse,
27+
onCopyGrantedPermissions,
28+
onCopySupportedPermissions,
29+
onGetGrantedPermissions,
30+
onGetSupportedPermissions,
31+
supportedIsCopied,
32+
supportedPermissionsResponse,
33+
}: PermissionQueriesPanelProps) => {
34+
const hasSupportedPermissionsResponse = Boolean(supportedPermissionsResponse);
35+
const hasGrantedPermissionsResponse = Boolean(grantedPermissionsResponse);
36+
37+
return (
38+
<Box>
39+
<Title>Permission Queries</Title>
40+
<div style={permissionQueryActionsStyle}>
41+
<CustomMessageButton
42+
$text="Get Supported Permissions"
43+
onClick={onGetSupportedPermissions}
44+
/>
45+
<CustomMessageButton
46+
$text="Get Granted Permissions"
47+
onClick={onGetGrantedPermissions}
48+
/>
49+
</div>
50+
{hasSupportedPermissionsResponse && (
51+
<ResponseContainer>
52+
<Title>Supported Permissions</Title>
53+
<CopyButton
54+
onClick={onCopySupportedPermissions}
55+
title={'Copy to clipboard'}
56+
>
57+
{supportedIsCopied ? '✅' : '📝'}
58+
</CopyButton>
59+
<pre>{stringifyWithBigInt(supportedPermissionsResponse)}</pre>
60+
</ResponseContainer>
61+
)}
62+
{hasGrantedPermissionsResponse && (
63+
<ResponseContainer style={{ marginTop: '1rem' }}>
64+
<Title>Granted Permissions</Title>
65+
<CopyButton
66+
onClick={onCopyGrantedPermissions}
67+
title={'Copy to clipboard'}
68+
>
69+
{grantedIsCopied ? '✅' : '📝'}
70+
</CopyButton>
71+
<pre>{stringifyWithBigInt(grantedPermissionsResponse)}</pre>
72+
</ResponseContainer>
73+
)}
74+
</Box>
75+
);
76+
};
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { ChangeEvent } from 'react';
2+
import type { Chain } from 'viem';
3+
4+
import { ERC20TokenAllowanceForm } from './ERC20TokenAllowanceForm';
5+
import { ERC20TokenPeriodicForm } from './ERC20TokenPeriodicForm';
6+
import { ERC20TokenStreamForm } from './ERC20TokenStreamForm';
7+
import { NativeTokenAllowanceForm } from './NativeTokenAllowanceForm';
8+
import { NativeTokenPeriodicForm } from './NativeTokenPeriodicForm';
9+
import { NativeTokenStreamForm } from './NativeTokenStreamForm';
10+
import { TokenApprovalRevocationForm } from './TokenApprovalRevocationForm';
11+
import type { PermissionRequest } from './types';
12+
import { Box, StyledForm } from '../../styles';
13+
import { CustomMessageButton } from '../Buttons';
14+
15+
type PermissionRequestPanelProps = {
16+
onChainChange: (event: ChangeEvent<HTMLSelectElement>) => void;
17+
onFormChange: (request: PermissionRequest) => void;
18+
onGrantPermissions: () => void;
19+
onPermissionTypeChange: (event: ChangeEvent<HTMLSelectElement>) => void;
20+
permissionType: PermissionRequest['type'];
21+
selectedChain: Chain;
22+
supportedChains: Chain[];
23+
};
24+
25+
const selectStyle = {
26+
padding: '0.8rem',
27+
border: '1px solid',
28+
borderRadius: '0.3rem',
29+
flexGrow: 1,
30+
};
31+
32+
const renderPermissionForm = (
33+
permissionType: PermissionRequest['type'],
34+
onFormChange: (request: PermissionRequest) => void,
35+
) => {
36+
switch (permissionType) {
37+
case 'native-token-stream':
38+
return <NativeTokenStreamForm onChange={onFormChange} />;
39+
case 'erc20-token-stream':
40+
return <ERC20TokenStreamForm onChange={onFormChange} />;
41+
case 'native-token-periodic':
42+
return <NativeTokenPeriodicForm onChange={onFormChange} />;
43+
case 'erc20-token-periodic':
44+
return <ERC20TokenPeriodicForm onChange={onFormChange} />;
45+
case 'native-token-allowance':
46+
return <NativeTokenAllowanceForm onChange={onFormChange} />;
47+
case 'erc20-token-allowance':
48+
return <ERC20TokenAllowanceForm onChange={onFormChange} />;
49+
case 'token-approval-revocation':
50+
return <TokenApprovalRevocationForm onChange={onFormChange} />;
51+
default:
52+
return null;
53+
}
54+
};
55+
56+
export const PermissionRequestPanel = ({
57+
onChainChange,
58+
onFormChange,
59+
onGrantPermissions,
60+
onPermissionTypeChange,
61+
permissionType,
62+
selectedChain,
63+
supportedChains,
64+
}: PermissionRequestPanelProps) => (
65+
<Box>
66+
<StyledForm>
67+
<div>
68+
<label htmlFor="chainSelector">Chain:</label>
69+
<select
70+
id="chainSelector"
71+
name="chainSelector"
72+
value={selectedChain.id}
73+
onChange={onChainChange}
74+
style={selectStyle}
75+
>
76+
{supportedChains.map((chain) => (
77+
<option key={chain.id} value={chain.id}>
78+
{chain.name}
79+
</option>
80+
))}
81+
</select>
82+
</div>
83+
84+
<div>
85+
<label htmlFor="permissionType">Permission Type:</label>
86+
<select
87+
id="permissionType"
88+
name="permissionType"
89+
value={permissionType}
90+
onChange={onPermissionTypeChange}
91+
style={selectStyle}
92+
>
93+
<option value="native-token-stream">Native Token Stream</option>
94+
<option value="erc20-token-stream">ERC20 Token Stream</option>
95+
<option value="native-token-periodic">Native Token Periodic</option>
96+
<option value="native-token-allowance">Native Token Allowance</option>
97+
<option value="erc20-token-periodic">ERC20 Token Periodic</option>
98+
<option value="erc20-token-allowance">ERC20 Token Allowance</option>
99+
<option value="token-approval-revocation">
100+
Token Approval Revocation
101+
</option>
102+
</select>
103+
</div>
104+
105+
{renderPermissionForm(permissionType, onFormChange)}
106+
</StyledForm>
107+
<CustomMessageButton
108+
$text="Grant Permission"
109+
onClick={onGrantPermissions}
110+
/>
111+
</Box>
112+
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Box, CopyButton, ResponseContainer } from '../../styles';
2+
import { stringifyWithBigInt } from '../../utils';
3+
import { Title } from '../Card';
4+
5+
type PermissionResponsePanelProps = {
6+
decodedIsCopied: boolean;
7+
decodedPermissionContext: unknown;
8+
isCopied: boolean;
9+
onCopyDecodedPermissionContext: () => void;
10+
onCopyPermissionResponse: () => void;
11+
permissionResponse: unknown;
12+
};
13+
14+
export const PermissionResponsePanel = ({
15+
decodedIsCopied,
16+
decodedPermissionContext,
17+
isCopied,
18+
onCopyDecodedPermissionContext,
19+
onCopyPermissionResponse,
20+
permissionResponse,
21+
}: PermissionResponsePanelProps) => {
22+
const hasDecodedPermissionContext = Boolean(decodedPermissionContext);
23+
24+
return (
25+
<Box style={{ position: 'relative' }}>
26+
<ResponseContainer>
27+
<Title>Permission Response</Title>
28+
<CopyButton
29+
onClick={onCopyPermissionResponse}
30+
title={'Copy to clipboard'}
31+
>
32+
{isCopied ? '✅' : '📝'}
33+
</CopyButton>
34+
<pre>{stringifyWithBigInt(permissionResponse)}</pre>
35+
{hasDecodedPermissionContext && (
36+
<details style={{ marginTop: '1rem' }}>
37+
<summary>Decoded Permission Context</summary>
38+
<CopyButton
39+
onClick={onCopyDecodedPermissionContext}
40+
title={'Copy to clipboard'}
41+
style={{ position: 'relative', float: 'right' }}
42+
>
43+
{decodedIsCopied ? '✅' : '📝'}
44+
</CopyButton>
45+
<pre>{stringifyWithBigInt(decodedPermissionContext)}</pre>
46+
</details>
47+
)}
48+
</ResponseContainer>
49+
</Box>
50+
);
51+
};

0 commit comments

Comments
 (0)