Skip to content

Commit fc9ab36

Browse files
feat: Add DateTimePicker component (#3698)
Adds a new `DateTimePicker` component <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces a new `DateTimePicker` JSX component (with ISO 8601 validation), adds simulation/Jest support via `pickDateTime`, integrates in controllers/state, and showcases it in the interactive UI example. > > - **SDK / JSX**: > - Add `DateTimePicker` component (`jsx/components/form/DateTimePicker.ts`) with tests and validation (`DateTimePickerStruct`). > - Introduce `ISO8601DateStruct` in `internals/time` (using `luxon`) and export via SDK; update validation to use it. > - Update `Field` and form typings/exports to include `DateTimePicker`. > - Add `luxon` dependency and types. > - **Controllers**: > - Treat `DateTimePicker` as stateful in interface utils; support default/state construction in forms and root. > - **Simulation / Jest**: > - Add `pickDateTime` action across simulation API and handler interfaces; wire events/state updates; extensive tests. > - Expose `pickDateTime` in Jest helpers and mock interface responses. > - **RPC Methods**: > - Extend `snap_createInterface` validation/error messages to allow `DateTimePicker`. > - **Examples**: > - Update interactive UI example to include Date/Time/DateTime pickers and tests asserting ISO strings. > - **Execution Environments**: > - Update LavaMoat policies to allow `@metamask/snaps-sdk>luxon` and `Intl`. > - **Utils**: > - Move ISO date validation to SDK; `snaps-utils/time` now re-exports `ISO8601DateStruct` and retains duration utilities. > - **Manifests/Lockfile**: > - Update example snap shasum; lockfile reflects new `luxon` and types. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 85563a0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Frederik Bolding <[email protected]>
1 parent 01afb52 commit fc9ab36

File tree

31 files changed

+811
-71
lines changed

31 files changed

+811
-71
lines changed

packages/examples/packages/interactive-ui/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/snaps.git"
88
},
99
"source": {
10-
"shasum": "lQJxp8tdELhy1AOIMHwlI4P9XDuQk/HGd6sRn17aHAs=",
10+
"shasum": "IN9G5gIS/Du5i6iLckWggkisIlE0A7w6QzvlGyB7Z38=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",

packages/examples/packages/interactive-ui/src/components/InteractiveForm.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Checkbox,
1717
Container,
1818
Footer,
19+
DateTimePicker,
1920
} from '@metamask/snaps-sdk/jsx';
2021

2122
/**
@@ -46,6 +47,21 @@ export type InteractiveFormState = {
4647
* The value of the example Selector.
4748
*/
4849
'example-selector': string;
50+
51+
/**
52+
* The value of the example DateTimePicker.
53+
*/
54+
'example-datetime'?: string;
55+
56+
/**
57+
* The value of the example DatePicker.
58+
*/
59+
'example-date'?: string;
60+
61+
/**
62+
* The value of the example TimePicker.
63+
*/
64+
'example-time'?: string;
4965
};
5066

5167
export const InteractiveForm: SnapComponent<{ disabled?: boolean }> = ({
@@ -102,6 +118,31 @@ export const InteractiveForm: SnapComponent<{ disabled?: boolean }> = ({
102118
</SelectorOption>
103119
</Selector>
104120
</Field>
121+
<Field label="Example DateTimePicker">
122+
<DateTimePicker
123+
name="example-datetime"
124+
placeholder="Select a date and time"
125+
disabled={disabled}
126+
/>
127+
</Field>
128+
<Field label="Example DatePicker">
129+
<DateTimePicker
130+
name="example-date"
131+
type="date"
132+
placeholder="Select a date"
133+
disableFuture={true}
134+
disabled={disabled}
135+
/>
136+
</Field>
137+
<Field label="Example TimePicker">
138+
<DateTimePicker
139+
name="example-time"
140+
type="time"
141+
placeholder="Select a time"
142+
disablePast={true}
143+
disabled={disabled}
144+
/>
145+
</Field>
105146
</Form>
106147
</Box>
107148
<Footer>

packages/examples/packages/interactive-ui/src/index.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ describe('onRpcRequest', () => {
3535
method: 'dialog',
3636
});
3737

38+
const presentDate = new Date();
39+
40+
const futureDate = new Date();
41+
futureDate.setFullYear(futureDate.getFullYear() + 1);
42+
43+
const pastDate = new Date();
44+
pastDate.setFullYear(pastDate.getFullYear() - 1);
45+
3846
const formScreen = await response.getInterface();
3947

4048
expect(formScreen).toRender(<InteractiveForm />);
@@ -49,6 +57,12 @@ describe('onRpcRequest', () => {
4957

5058
await formScreen.clickElement('example-checkbox');
5159

60+
await formScreen.pickDateTime('example-datetime', presentDate);
61+
62+
await formScreen.pickDateTime('example-date', pastDate);
63+
64+
await formScreen.pickDateTime('example-time', futureDate);
65+
5266
await formScreen.clickElement('submit');
5367

5468
const resultScreen = await response.getInterface();
@@ -62,6 +76,9 @@ describe('onRpcRequest', () => {
6276
'example-radiogroup': 'option3',
6377
'example-checkbox': true,
6478
'example-selector': 'option2',
79+
'example-datetime': presentDate.toISOString(),
80+
'example-date': pastDate.toISOString(),
81+
'example-time': futureDate.toISOString(),
6582
}}
6683
/>,
6784
);
@@ -94,6 +111,9 @@ describe('onRpcRequest', () => {
94111
'example-radiogroup': 'option1',
95112
'example-checkbox': false,
96113
'example-selector': 'option1',
114+
'example-datetime': '',
115+
'example-date': '',
116+
'example-time': '',
97117
}}
98118
/>,
99119
);
@@ -106,6 +126,14 @@ describe('onRpcRequest', () => {
106126

107127
describe('onHomePage', () => {
108128
it('returns custom UI', async () => {
129+
const presentDate = new Date();
130+
131+
const futureDate = new Date();
132+
futureDate.setFullYear(futureDate.getFullYear() + 1);
133+
134+
const pastDate = new Date();
135+
pastDate.setFullYear(pastDate.getFullYear() - 1);
136+
109137
const { onHomePage } = await installSnap();
110138

111139
const response = await onHomePage();
@@ -122,6 +150,12 @@ describe('onHomePage', () => {
122150

123151
await formScreen.selectFromSelector('example-selector', 'option2');
124152

153+
await formScreen.pickDateTime('example-datetime', presentDate);
154+
155+
await formScreen.pickDateTime('example-date', pastDate);
156+
157+
await formScreen.pickDateTime('example-time', futureDate);
158+
125159
await formScreen.clickElement('submit');
126160

127161
const resultScreen = response.getInterface();
@@ -134,6 +168,9 @@ describe('onHomePage', () => {
134168
'example-radiogroup': 'option3',
135169
'example-checkbox': false,
136170
'example-selector': 'option2',
171+
'example-datetime': presentDate.toISOString(),
172+
'example-date': pastDate.toISOString(),
173+
'example-time': futureDate.toISOString(),
137174
}}
138175
/>,
139176
);

packages/snaps-controllers/src/interface/utils.test.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
AssetSelector,
1919
AddressInput,
2020
AccountSelector,
21+
DateTimePicker,
2122
} from '@metamask/snaps-sdk/jsx';
2223
import type { CaipAccountId } from '@metamask/utils';
2324
import { parseCaipAccountId } from '@metamask/utils';
@@ -352,6 +353,66 @@ describe('constructState', () => {
352353
});
353354
});
354355

356+
it('sets default value for root level DateTimePicker', () => {
357+
const element = (
358+
<Box>
359+
<DateTimePicker name="foo" />
360+
</Box>
361+
);
362+
363+
const result = constructState({}, element, elementDataGetters);
364+
expect(result).toStrictEqual({
365+
foo: null,
366+
});
367+
});
368+
369+
it('supports root level DateTimePicker', () => {
370+
const element = (
371+
<Box>
372+
<DateTimePicker name="foo" value="2022-01-01T00:00:00Z" />
373+
</Box>
374+
);
375+
376+
const result = constructState({}, element, elementDataGetters);
377+
expect(result).toStrictEqual({
378+
foo: '2022-01-01T00:00:00Z',
379+
});
380+
});
381+
382+
it('sets default value for DateTimePicker in forms', () => {
383+
const element = (
384+
<Box>
385+
<Form name="form">
386+
<Field label="foo">
387+
<DateTimePicker name="foo" />
388+
</Field>
389+
</Form>
390+
</Box>
391+
);
392+
393+
const result = constructState({}, element, elementDataGetters);
394+
expect(result).toStrictEqual({
395+
form: { foo: null },
396+
});
397+
});
398+
399+
it('supports DateTimePicker in forms', () => {
400+
const element = (
401+
<Box>
402+
<Form name="form">
403+
<Field label="foo">
404+
<DateTimePicker name="foo" value="2022-01-01T00:00:00Z" />
405+
</Field>
406+
</Form>
407+
</Box>
408+
);
409+
410+
const result = constructState({}, element, elementDataGetters);
411+
expect(result).toStrictEqual({
412+
form: { foo: '2022-01-01T00:00:00Z' },
413+
});
414+
});
415+
355416
it('sets default value for root level dropdown', () => {
356417
const element = (
357418
<Box>

packages/snaps-controllers/src/interface/utils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
AssetSelectorElement,
2424
AddressInputElement,
2525
AccountSelectorElement,
26+
DateTimePickerElement,
2627
} from '@metamask/snaps-sdk/jsx';
2728
import { isJSXElementUnsafe } from '@metamask/snaps-sdk/jsx';
2829
import type { InternalAccount } from '@metamask/snaps-utils';
@@ -57,6 +58,7 @@ const STATEFUL_COMPONENT_TYPES = [
5758
'AssetSelector',
5859
'AddressInput',
5960
'AccountSelector',
61+
'DateTimePicker',
6062
] as const;
6163

6264
/**
@@ -342,7 +344,8 @@ function constructComponentSpecificDefaultState(
342344
| SelectorElement
343345
| AssetSelectorElement
344346
| AddressInputElement
345-
| AccountSelectorElement,
347+
| AccountSelectorElement
348+
| DateTimePickerElement,
346349
elementDataGetters: ElementDataGetters,
347350
) {
348351
switch (element.type) {
@@ -459,7 +462,8 @@ function getComponentStateValue(
459462
| SelectorElement
460463
| AssetSelectorElement
461464
| AddressInputElement
462-
| AccountSelectorElement,
465+
| AccountSelectorElement
466+
| DateTimePickerElement,
463467
elementDataGetters: ElementDataGetters,
464468
) {
465469
switch (element.type) {
@@ -510,7 +514,8 @@ function constructInputState(
510514
| SelectorElement
511515
| AssetSelectorElement
512516
| AddressInputElement
513-
| AccountSelectorElement,
517+
| AccountSelectorElement
518+
| DateTimePickerElement,
514519
elementDataGetters: ElementDataGetters,
515520
form?: string,
516521
) {

packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
"@metamask/snaps-sdk": {
7575
"packages": {
7676
"@metamask/superstruct": true,
77-
"@metamask/utils": true
77+
"@metamask/utils": true,
78+
"@metamask/snaps-sdk>luxon": true
7879
}
7980
},
8081
"@metamask/snaps-utils": {
@@ -143,6 +144,11 @@
143144
"define": true
144145
}
145146
},
147+
"@metamask/snaps-sdk>luxon": {
148+
"globals": {
149+
"Intl": true
150+
}
151+
},
146152
"@metamask/object-multiplex>once": {
147153
"packages": {
148154
"@metamask/object-multiplex>once>wrappy": true

packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"@metamask/snaps-sdk": {
8282
"packages": {
8383
"@metamask/superstruct": true,
84-
"@metamask/utils": true
84+
"@metamask/utils": true,
85+
"@metamask/snaps-sdk>luxon": true
8586
}
8687
},
8788
"@metamask/snaps-utils": {
@@ -156,6 +157,11 @@
156157
"define": true
157158
}
158159
},
160+
"@metamask/snaps-sdk>luxon": {
161+
"globals": {
162+
"Intl": true
163+
}
164+
},
159165
"@metamask/object-multiplex>once": {
160166
"packages": {
161167
"@metamask/object-multiplex>once>wrappy": true

packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"@metamask/snaps-sdk": {
8282
"packages": {
8383
"@metamask/superstruct": true,
84-
"@metamask/utils": true
84+
"@metamask/utils": true,
85+
"@metamask/snaps-sdk>luxon": true
8586
}
8687
},
8788
"@metamask/snaps-utils": {
@@ -156,6 +157,11 @@
156157
"define": true
157158
}
158159
},
160+
"@metamask/snaps-sdk>luxon": {
161+
"globals": {
162+
"Intl": true
163+
}
164+
},
159165
"@metamask/object-multiplex>once": {
160166
"packages": {
161167
"@metamask/object-multiplex>once>wrappy": true

packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
"@metamask/snaps-sdk": {
7575
"packages": {
7676
"@metamask/superstruct": true,
77-
"@metamask/utils": true
77+
"@metamask/utils": true,
78+
"@metamask/snaps-sdk>luxon": true
7879
}
7980
},
8081
"@metamask/snaps-utils": {
@@ -143,6 +144,11 @@
143144
"define": true
144145
}
145146
},
147+
"@metamask/snaps-sdk>luxon": {
148+
"globals": {
149+
"Intl": true
150+
}
151+
},
146152
"@metamask/object-multiplex>once": {
147153
"packages": {
148154
"@metamask/object-multiplex>once>wrappy": true

packages/snaps-jest/src/helpers.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ describe('installSnap', () => {
411411
type: DialogType.Prompt,
412412
content: <Text>Hello, world!</Text>,
413413
clickElement: expect.any(Function),
414+
pickDateTime: expect.any(Function),
414415
typeInField: expect.any(Function),
415416
selectInDropdown: expect.any(Function),
416417
selectFromRadioGroup: expect.any(Function),
@@ -475,6 +476,7 @@ describe('installSnap', () => {
475476
type: DialogType.Confirmation,
476477
content: <Text>Hello, world!</Text>,
477478
clickElement: expect.any(Function),
479+
pickDateTime: expect.any(Function),
478480
typeInField: expect.any(Function),
479481
selectInDropdown: expect.any(Function),
480482
selectFromRadioGroup: expect.any(Function),
@@ -539,6 +541,7 @@ describe('installSnap', () => {
539541
type: DialogType.Alert,
540542
content: <Text>Hello, world!</Text>,
541543
clickElement: expect.any(Function),
544+
pickDateTime: expect.any(Function),
542545
typeInField: expect.any(Function),
543546
selectInDropdown: expect.any(Function),
544547
selectFromRadioGroup: expect.any(Function),

0 commit comments

Comments
 (0)