Skip to content

Commit

Permalink
feat: Add Avatar component (#2820)
Browse files Browse the repository at this point in the history
Add `Avatar` component, that lets Snaps show an avatar for an address
without showing the full address.

Progresses #2776
  • Loading branch information
FrederikBolding authored Oct 9, 2024
1 parent 173cf1b commit fae7855
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "4a1znWfS1zEU7F+KvELAq39Q31sOaFMrxy40yNz4B0s=",
"shasum": "HEAbfXBUqw5fNP+sJVyvUpXucZy6CwiCFXJi36CEfUw=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/packages/browserify/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snaps.git"
},
"source": {
"shasum": "t0meIgWvqOjHA02oXh9UmcfDmGoiklTgVud+eqyKbRE=",
"shasum": "mCoDlMSdhDJAXd9zT74ST7jHysifHdQ8r0++b8uPbOs=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe('snap_createInterface', () => {
error: {
code: -32602,
message:
'Invalid params: At path: ui -- Expected type to be one of: "Address", "Bold", "Box", "Button", "Copyable", "Divider", "Dropdown", "RadioGroup", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", "Container", but received: undefined.',
'Invalid params: At path: ui -- Expected type to be one of: "Address", "Bold", "Box", "Button", "Copyable", "Divider", "Dropdown", "RadioGroup", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", "Avatar", "Container", but received: undefined.',
stack: expect.any(String),
},
id: 1,
Expand Down Expand Up @@ -190,7 +190,7 @@ describe('snap_createInterface', () => {
error: {
code: -32602,
message:
'Invalid params: At path: ui.props.children -- Expected type to be one of: "Address", "Bold", "Box", "Button", "Copyable", "Divider", "Dropdown", "RadioGroup", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", but received: "Field".',
'Invalid params: At path: ui.props.children -- Expected type to be one of: "Address", "Bold", "Box", "Button", "Copyable", "Divider", "Dropdown", "RadioGroup", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", "Avatar", but received: "Field".',
stack: expect.any(String),
},
id: 1,
Expand Down
35 changes: 35 additions & 0 deletions packages/snaps-sdk/src/jsx/components/Avatar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Avatar } from './Avatar';

describe('Avatar', () => {
it('renders an avatar', () => {
const result = (
<Avatar address="eip155:1:0x1234567890123456789012345678901234567890" />
);

expect(result).toStrictEqual({
type: 'Avatar',
key: null,
props: {
address: 'eip155:1:0x1234567890123456789012345678901234567890',
},
});
});

it('renders an avatar of a certain size', () => {
const result = (
<Avatar
address="eip155:1:0x1234567890123456789012345678901234567890"
size="lg"
/>
);

expect(result).toStrictEqual({
type: 'Avatar',
key: null,
props: {
address: 'eip155:1:0x1234567890123456789012345678901234567890',
size: 'lg',
},
});
});
});
37 changes: 37 additions & 0 deletions packages/snaps-sdk/src/jsx/components/Avatar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { CaipAccountId } from '@metamask/utils';

import { createSnapComponent } from '../component';

/**
* The props of the {@link Avatar} component.
*
* @property address - The address to display. This should be a valid CAIP-10 address.
*/
export type AvatarProps = {
address: CaipAccountId;
size?: 'sm' | 'md' | 'lg' | undefined;
};

const TYPE = 'Avatar';

/**
* An avatar component, which is used to display an avatar for a CAIP-10 address.
*
* This component does not accept any children.
*
* @param props - The props of the component.
* @param props.address - The address to display. This should be a valid CAIP-10 address.
* @returns An avatar element.
* @example
* <Avatar address="eip155:1:0x1234567890123456789012345678901234567890" />
* @example
* <Avatar address="bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6" />
*/
export const Avatar = createSnapComponent<AvatarProps, typeof TYPE>(TYPE);

/**
* An avatar element.
*
* @see Avatar
*/
export type AvatarElement = ReturnType<typeof Avatar>;
3 changes: 3 additions & 0 deletions packages/snaps-sdk/src/jsx/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AddressElement } from './Address';
import type { AvatarElement } from './Avatar';
import type { BoxElement } from './Box';
import type { CardElement } from './Card';
import type { ContainerElement } from './Container';
Expand All @@ -21,6 +22,7 @@ import type { ValueElement } from './Value';
export * from './form';
export * from './formatting';
export * from './Address';
export * from './Avatar';
export * from './Box';
export * from './Card';
export * from './Copyable';
Expand All @@ -44,6 +46,7 @@ export * from './Section';
export type JSXElement =
| StandardFormElement
| StandardFormattingElement
| AvatarElement
| AddressElement
| BoxElement
| CardElement
Expand Down
50 changes: 50 additions & 0 deletions packages/snaps-sdk/src/jsx/validation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
Selector,
SelectorOption,
Section,
Avatar,
} from './components';
import {
AddressStruct,
Expand Down Expand Up @@ -69,6 +70,7 @@ import {
SelectorStruct,
SectionStruct,
NotificationComponentsStruct,
AvatarStruct,
} from './validation';

describe('KeyStruct', () => {
Expand Down Expand Up @@ -432,6 +434,54 @@ describe('ItalicStruct', () => {
});
});

describe('AvatarStruct', () => {
it.each([
<Avatar address="eip155:1:0x1234567890abcdef1234567890abcdef12345678" />,
<Avatar address="bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6" />,
<Avatar address="cosmos:cosmoshub-3:cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0" />,
<Avatar
address="eip155:1:0x1234567890abcdef1234567890abcdef12345678"
size="lg"
/>,
])('validates an avatar element', (value) => {
expect(is(value, AvatarStruct)).toBe(true);
});

it.each([
'foo',
42,
null,
undefined,
{},
[],
// @ts-expect-error - Invalid props.
<Avatar />,
// @ts-expect-error - Invalid props.
<Avatar>
<Text>foo</Text>
</Avatar>,
// @ts-expect-error - Invalid props.
<Avatar address="0x1234567890abcdef1234567890abcdef12345678" />,
// @ts-expect-error - Invalid props.
<Avatar address="0x1234" />,
<Avatar
address="eip155:1:0x1234567890abcdef1234567890abcdef12345678"
// @ts-expect-error - Invalid props.
size="foo"
/>,
<Avatar address="a:b:0x1234" />,
<Text>foo</Text>,
<Box>
<Text>foo</Text>
</Box>,
<Row label="label">
<Image src="<svg />" alt="alt" />
</Row>,
])('does not validate "%p"', (value) => {
expect(is(value, AvatarStruct)).toBe(false);
});
});

describe('AddressStruct', () => {
it.each([
<Address address="0x1234567890abcdef1234567890abcdef12345678" />,
Expand Down
11 changes: 11 additions & 0 deletions packages/snaps-sdk/src/jsx/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import type {
SnapsChildren,
StringElement,
} from './component';
import type { AvatarElement } from './components';
import {
type AddressElement,
type BoldElement,
Expand Down Expand Up @@ -473,6 +474,14 @@ export const AddressStruct: Describe<AddressElement> = element('Address', {
address: nullUnion([HexChecksumAddressStruct, CaipAccountIdStruct]),
});

/**
* A struct for the {@link AvatarElement} type.
*/
export const AvatarStruct = element('Avatar', {
address: CaipAccountIdStruct,
size: optional(nullUnion([literal('sm'), literal('md'), literal('lg')])),
}) as unknown as Struct<AvatarElement, null>;

export const BoxChildrenStruct = children(
// eslint-disable-next-line @typescript-eslint/no-use-before-define
[lazy(() => BoxChildStruct)],
Expand Down Expand Up @@ -745,6 +754,7 @@ export const BoxChildStruct = typedUnion([
IconStruct,
SelectorStruct,
SectionStruct,
AvatarStruct,
]);

export const NotificationComponentsStruct = typedUnion([
Expand Down Expand Up @@ -824,6 +834,7 @@ export const JSXElementStruct: Describe<JSXElement> = typedUnion([
SelectorStruct,
SelectorOptionStruct,
SectionStruct,
AvatarStruct,
]);

/**
Expand Down

0 comments on commit fae7855

Please sign in to comment.