Skip to content

Commit 575a62d

Browse files
committed
Decompose specific permission form out of site/pages/index.tsx to allow rapid addition of new permissions
1 parent e70def8 commit 575a62d

10 files changed

Lines changed: 941 additions & 478 deletions

File tree

docs/addingNewPermissionTypes.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,108 @@ export const DEFAULT_OFFERS: GatorPermission[] = [
194194
];
195195
```
196196

197+
### 9. Add the Permission to the demo dapp
198+
199+
### 10. Implement Permission Form Component
200+
201+
Create a new form component in `packages/site/src/components/permissions/` for your permission type. For example, `YourPermissionForm.tsx`:
202+
203+
```typescript
204+
import { useCallback, useEffect, useState } from 'react';
205+
import { StyledForm } from './styles';
206+
import type { YourPermissionRequest } from './types';
207+
208+
type YourPermissionFormProps = {
209+
onChange: (request: YourPermissionRequest) => void;
210+
}
211+
212+
export const YourPermissionForm = ({
213+
onChange,
214+
}: YourPermissionFormProps) => {
215+
// 1. Define state for each form field
216+
const [field1, setField1] = useState(initialValue1);
217+
const [field2, setField2] = useState(initialValue2);
218+
// ... add more state as needed
219+
220+
// 2. Implement change handlers for each field
221+
const handleField1Change = useCallback(
222+
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
223+
setField1(value);
224+
},
225+
[],
226+
);
227+
228+
// 3. Use useEffect to emit changes to parent
229+
useEffect(() => {
230+
onChange({
231+
type: 'your-permission-type',
232+
field1,
233+
field2,
234+
// ... include all fields
235+
});
236+
}, [onChange, field1, field2 /* ... */]);
237+
238+
// 4. Render form fields
239+
return (
240+
<StyledForm>
241+
<div>
242+
<label htmlFor="field1">Field 1:</label>
243+
<input
244+
type="text"
245+
id="field1"
246+
name="field1"
247+
value={field1}
248+
onChange={handleField1Change}
249+
placeholder="Enter field 1"
250+
/>
251+
</div>
252+
{/* Add more form fields */}
253+
</StyledForm>
254+
);
255+
};
256+
```
257+
258+
Then, add your form to the permission type selector in `packages/site/src/pages/index.tsx`:
259+
260+
1. Import your form component:
261+
```typescript
262+
import { YourPermissionForm } from '../components/permissions';
263+
```
264+
265+
2. Add your permission type to the select options:
266+
```typescript
267+
<select
268+
id="permissionType"
269+
name="permissionType"
270+
value={permissionType}
271+
onChange={handlePermissionTypeChange}
272+
>
273+
{/* ... existing options ... */}
274+
<option value="your-permission-type">Your Permission Type</option>
275+
</select>
276+
```
277+
278+
3. Add the form component to the conditional rendering:
279+
```typescript
280+
{permissionType === 'your-permission-type' && (
281+
<YourPermissionForm
282+
onChange={(request: YourPermissionRequest) => {
283+
setPermissionRequest(request);
284+
}}
285+
/>
286+
)}
287+
```
288+
289+
Key points for implementing permission forms:
290+
1. Use the `StyledForm` component for consistent styling
291+
2. Implement proper type safety with TypeScript
292+
3. Use controlled form components with React state
293+
4. Emit changes to parent component using the `onChange` prop
294+
5. Include all required fields from your permission type definition
295+
6. Add appropriate validation and error handling
296+
7. Follow the existing pattern of using `useCallback` for event handlers
297+
8. Use `useEffect` to emit form changes to the parent
298+
197299
## Best Practices
198300

199301
1. **Type Safety**: Use TypeScript and Zod for robust type checking and validation.

packages/permissions-kernel-snap/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snap-7715-permissions.git"
88
},
99
"source": {
10-
"shasum": "eRjmPTGFoDn0TmSiFtw7F6sYLP+OInKhcDoSPgOK2Wg=",
10+
"shasum": "S0ov2EamT+yz9yJnmK+pFZ5q8knPzSppoKBYMKfHSX8=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
import { parseUnits, toHex, type Hex } from 'viem';
3+
import { StyledForm } from './styles';
4+
import type { ERC20TokenStreamPermissionRequest } from './types';
5+
6+
type ERC20TokenStreamFormProps = {
7+
onChange: (request: ERC20TokenStreamPermissionRequest) => void;
8+
};
9+
10+
export const ERC20TokenStreamForm = ({
11+
onChange,
12+
}: ERC20TokenStreamFormProps) => {
13+
const [initialAmount, setInitialAmount] = useState<bigint | null>(
14+
BigInt(toHex(parseUnits('.5', 18))),
15+
);
16+
const [amountPerSecond, setAmountPerSecond] = useState(
17+
BigInt(toHex(parseUnits('.5', 18))),
18+
);
19+
const [maxAmount, setMaxAmount] = useState<bigint | null>(
20+
BigInt(toHex(parseUnits('2.5', 18))),
21+
);
22+
const [startTime, setStartTime] = useState(Math.floor(Date.now() / 1000));
23+
const [expiry, setExpiry] = useState(
24+
Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 30 days from now
25+
);
26+
const [justification, setJustification] = useState(
27+
'This is a very important request for streaming allowance for some very important thing',
28+
);
29+
const [isAdjustmentAllowed, setIsAdjustmentAllowed] = useState(true);
30+
const [tokenAddress, setTokenAddress] = useState<Hex>(
31+
'0x616553f076c6f66739a99fef373c6b4ae1b22a7a', // Consensys USDC
32+
);
33+
34+
const handleInitialAmountChange = useCallback(
35+
({
36+
target: { value: inputValue },
37+
}: React.ChangeEvent<HTMLInputElement>) => {
38+
if (inputValue.trim() === '') {
39+
setInitialAmount(null);
40+
} else {
41+
setInitialAmount(BigInt(inputValue));
42+
}
43+
},
44+
[],
45+
);
46+
47+
const handleAmountPerSecondChange = useCallback(
48+
({
49+
target: { value: inputValue },
50+
}: React.ChangeEvent<HTMLInputElement>) => {
51+
setAmountPerSecond(BigInt(inputValue));
52+
},
53+
[],
54+
);
55+
56+
const handleMaxAmountChange = useCallback(
57+
({
58+
target: { value: inputValue },
59+
}: React.ChangeEvent<HTMLInputElement>) => {
60+
if (inputValue.trim() === '') {
61+
setMaxAmount(null);
62+
} else {
63+
setMaxAmount(BigInt(inputValue));
64+
}
65+
},
66+
[],
67+
);
68+
69+
const handleStartTimeChange = useCallback(
70+
({
71+
target: { value: inputValue },
72+
}: React.ChangeEvent<HTMLInputElement>) => {
73+
setStartTime(Number(inputValue));
74+
},
75+
[],
76+
);
77+
78+
const handleJustificationChange = useCallback(
79+
({
80+
target: { value: inputValue },
81+
}: React.ChangeEvent<HTMLTextAreaElement>) => {
82+
setJustification(inputValue);
83+
},
84+
[],
85+
);
86+
87+
const handleExpiryChange = useCallback(
88+
({
89+
target: { value: inputValue },
90+
}: React.ChangeEvent<HTMLInputElement>) => {
91+
setExpiry(Number(inputValue));
92+
},
93+
[],
94+
);
95+
96+
const handleIsAdjustmentAllowedChange = useCallback(
97+
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
98+
setIsAdjustmentAllowed(checked);
99+
},
100+
[],
101+
);
102+
103+
const handleTokenAddressChange = useCallback(
104+
({
105+
target: { value: inputValue },
106+
}: React.ChangeEvent<HTMLInputElement>) => {
107+
setTokenAddress(inputValue as Hex);
108+
},
109+
[],
110+
);
111+
112+
useEffect(() => {
113+
onChange({
114+
type: 'erc20-token-stream',
115+
initialAmount: initialAmount ? toHex(initialAmount) : null,
116+
amountPerSecond: toHex(amountPerSecond),
117+
maxAmount: maxAmount ? toHex(maxAmount) : null,
118+
startTime,
119+
expiry,
120+
justification,
121+
isAdjustmentAllowed,
122+
tokenAddress,
123+
});
124+
}, [
125+
onChange,
126+
initialAmount,
127+
amountPerSecond,
128+
maxAmount,
129+
startTime,
130+
expiry,
131+
justification,
132+
isAdjustmentAllowed,
133+
tokenAddress,
134+
]);
135+
136+
return (
137+
<StyledForm>
138+
<div>
139+
<label htmlFor="tokenAddress">Token Address:</label>
140+
<input
141+
type="text"
142+
id="tokenAddress"
143+
name="tokenAddress"
144+
value={tokenAddress}
145+
onChange={handleTokenAddressChange}
146+
placeholder="0x..."
147+
/>
148+
</div>
149+
<div>
150+
<label htmlFor="initialAmount">Initial Amount:</label>
151+
<input
152+
type="text"
153+
id="initialAmount"
154+
name="initialAmount"
155+
value={initialAmount?.toString()}
156+
onChange={handleInitialAmountChange}
157+
/>
158+
</div>
159+
<div>
160+
<label htmlFor="amountPerSecond">Amount Per Second:</label>
161+
<input
162+
type="text"
163+
id="amountPerSecond"
164+
name="amountPerSecond"
165+
value={amountPerSecond.toString()}
166+
onChange={handleAmountPerSecondChange}
167+
/>
168+
</div>
169+
<div>
170+
<label htmlFor="maxAmount">Max Amount:</label>
171+
<input
172+
type="text"
173+
id="maxAmount"
174+
name="maxAmount"
175+
value={maxAmount?.toString()}
176+
onChange={handleMaxAmountChange}
177+
/>
178+
</div>
179+
<div>
180+
<label htmlFor="startTime">Start Time:</label>
181+
<input
182+
type="number"
183+
id="startTime"
184+
name="startTime"
185+
value={startTime}
186+
onChange={handleStartTimeChange}
187+
/>
188+
</div>
189+
<div>
190+
<label htmlFor="justification">Justification:</label>
191+
<textarea
192+
id="justification"
193+
name="justification"
194+
rows={3}
195+
value={justification}
196+
onChange={handleJustificationChange}
197+
></textarea>
198+
</div>
199+
<div>
200+
<label htmlFor="expiry">Expiry:</label>
201+
<input
202+
type="number"
203+
id="expiry"
204+
name="expiry"
205+
value={expiry}
206+
onChange={handleExpiryChange}
207+
/>
208+
</div>
209+
<div style={{ display: 'flex', alignItems: 'center' }}>
210+
<label htmlFor="isAdjustmentAllowed">Allow Adjustments:</label>
211+
<input
212+
type="checkbox"
213+
id="isAdjustmentAllowed"
214+
name="isAdjustmentAllowed"
215+
checked={isAdjustmentAllowed}
216+
onChange={handleIsAdjustmentAllowedChange}
217+
style={{ width: 'auto', marginLeft: '1rem' }}
218+
/>
219+
</div>
220+
</StyledForm>
221+
);
222+
};

0 commit comments

Comments
 (0)