Skip to content

Commit

Permalink
feat: ConnectWallet component (coinbase#720)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyhyco authored Jun 26, 2024
1 parent 19a7f6f commit 7dbdc50
Show file tree
Hide file tree
Showing 35 changed files with 594 additions and 260 deletions.
4 changes: 2 additions & 2 deletions src/identity/components/Address.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Address } from './Address';
import { useIdentityContext } from '../context';
import { useIdentityContext } from './IdentityProvider';
import { getSlicedAddress } from '../getSlicedAddress';

jest.mock('../context', () => ({
jest.mock('./IdentityProvider', () => ({
useIdentityContext: jest.fn(),
}));

Expand Down
2 changes: 1 addition & 1 deletion src/identity/components/Address.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useIdentityContext } from '../context';
import { useIdentityContext } from './IdentityProvider';
import { getSlicedAddress } from '../getSlicedAddress';
import { cn, text } from '../../styles/theme';
import type { AddressReact } from '../types';
Expand Down
2 changes: 1 addition & 1 deletion src/identity/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo, Children } from 'react';
import { useIdentityContext } from '../context';
import { useIdentityContext } from './IdentityProvider';
import { useAvatar } from '../hooks/useAvatar';
import { useName } from '../hooks/useName';
import type { AvatarReact } from '../types';
Expand Down
6 changes: 3 additions & 3 deletions src/identity/components/DisplayBadge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import { DisplayBadge } from './DisplayBadge';
import { Badge } from './Badge';
import { useOnchainKit } from '../../useOnchainKit';
import { useAttestations } from '../hooks/useAttestations';
import { useIdentityContext } from '../context';
import { useIdentityContext } from './IdentityProvider';

jest.mock('../../useOnchainKit', () => ({
useOnchainKit: jest.fn(),
}));
jest.mock('../hooks/useAttestations', () => ({
useAttestations: jest.fn(),
}));
jest.mock('../context', () => ({
jest.mock('./IdentityProvider', () => ({
useIdentityContext: jest.fn(),
}));

Expand All @@ -43,7 +43,7 @@ describe('DisplayBadge', () => {
</DisplayBadge>,
),
).toThrow(
'Name: a SchemaId must be provided to the Identity or Avatar component.',
'Name: a SchemaId must be provided to the OnchainKitProvider or Identity component.',
);
});

Expand Down
4 changes: 2 additions & 2 deletions src/identity/components/DisplayBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Address } from 'viem';
import type { ReactNode } from 'react';
import { useOnchainKit } from '../../useOnchainKit';
import { useAttestations } from '../hooks/useAttestations';
import { useIdentityContext } from '../context';
import { useIdentityContext } from './IdentityProvider';

type DisplayBadgeReact = {
children: ReactNode;
Expand All @@ -15,7 +15,7 @@ export function DisplayBadge({ children, address }: DisplayBadgeReact) {
useIdentityContext();
if (!contextSchemaId && !schemaId) {
throw new Error(
'Name: a SchemaId must be provided to the Identity or Avatar component.',
'Name: a SchemaId must be provided to the OnchainKitProvider or Identity component.',
);
}
const attestations = useAttestations({
Expand Down
46 changes: 5 additions & 41 deletions src/identity/components/Identity.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,16 @@
import { Children, useMemo } from 'react';
import { IdentityContext } from '../context';
import { Avatar } from './Avatar';
import { Name } from './Name';
import { Address } from './Address';
import type { IdentityReact } from '../types';
import { background, cn } from '../../styles/theme';
import { IdentityProvider } from './IdentityProvider';
import { IdentityLayout } from './IdentityLayout';

export function Identity({
address,
children,
className,
schemaId,
}: IdentityReact) {
const value = useMemo(() => {
return {
address,
schemaId,
};
}, [address, schemaId]);

const { avatar, name, addressComponent } = useMemo(() => {
const childrenArray = Children.toArray(children);
return {
// @ts-ignore
avatar: childrenArray.find(({ type }) => type === Avatar),
// @ts-ignore
name: childrenArray.find(({ type }) => type === Name),
// @ts-ignore
addressComponent: childrenArray.find(({ type }) => type === Address),
};
}, [children]);

return (
<IdentityContext.Provider value={value}>
<div
className={cn(
background.default,
'flex h-14 items-center space-x-4 px-2 py-1',
className,
)}
data-testid="ockIdentity_container"
>
{avatar}
<div className="flex flex-col">
{name}
{addressComponent}
</div>
</div>
</IdentityContext.Provider>
<IdentityProvider address={address} schemaId={schemaId}>
<IdentityLayout className={className}>{children}</IdentityLayout>
</IdentityProvider>
);
}
41 changes: 41 additions & 0 deletions src/identity/components/IdentityLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Children, useMemo, type ReactNode } from 'react';
import { Avatar } from './Avatar';
import { Name } from './Name';
import { Address } from './Address';
import { background, cn } from '../../styles/theme';

type IdentityLayoutReact = {
children: ReactNode;
className?: string;
};

export function IdentityLayout({ children, className }: IdentityLayoutReact) {
const { avatar, name, addressComponent } = useMemo(() => {
const childrenArray = Children.toArray(children);
return {
// @ts-ignore
avatar: childrenArray.find(({ type }) => type === Avatar),
// @ts-ignore
name: childrenArray.find(({ type }) => type === Name),
// @ts-ignore
addressComponent: childrenArray.find(({ type }) => type === Address),
};
}, [children]);

return (
<div
className={cn(
background.default,
'flex items-center space-x-4 px-2 py-1',
className,
)}
data-testid="ockIdentity_container"
>
{avatar}
<div className="flex flex-col">
{name}
{addressComponent}
</div>
</div>
);
}
39 changes: 39 additions & 0 deletions src/identity/components/IdentityProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @jest-environment jsdom
*/
import React from 'react';
import '@testing-library/jest-dom';
import { render, renderHook } from '@testing-library/react';
import type { Address } from 'viem';
import { IdentityProvider, useIdentityContext } from './IdentityProvider';

describe('IdentityProvider', () => {
it('provides context values from props', () => {
const address: Address = '0x1234567890abcdef1234567890abcdef12345678';
const schemaId: Address = '0xabcdefabcdefabcdefabcdefabcdefabcdef';

const { result } = renderHook(() => useIdentityContext(), {
wrapper: ({ children }) => (
<IdentityProvider address={address} schemaId={schemaId}>
{children}
</IdentityProvider>
),
});
expect(result.current.address).toEqual(address);
expect(result.current.schemaId).toEqual(schemaId);
});

it('should return default context when no props are passed', () => {
render(
<IdentityProvider>
<div />
</IdentityProvider>,
);

const { result } = renderHook(() => useIdentityContext(), {
wrapper: IdentityProvider,
});
expect(result.current.address).toEqual('');
expect(result.current.schemaId).toEqual(undefined);
});
});
34 changes: 34 additions & 0 deletions src/identity/components/IdentityProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { type ReactNode, useState, createContext, useContext } from 'react';
import type { Address } from 'viem';
import { useValue } from '../../internal/hooks/useValue';
import type { IdentityContextType } from '../types';

const emptyContext = {} as IdentityContextType;

export const IdentityContext = createContext<IdentityContextType>(emptyContext);

export function useIdentityContext() {
return useContext(IdentityContext);
}

type IdentityProvider = {
address?: Address;
children: ReactNode;
schemaId?: Address | null;
};

export function IdentityProvider(props: IdentityProvider) {
const [address, setAddress] = useState(props.address ?? ('' as Address));

const value = useValue({
address,
schemaId: props.schemaId,
setAddress,
});

return (
<IdentityContext.Provider value={value}>
{props.children}
</IdentityContext.Provider>
);
}
4 changes: 2 additions & 2 deletions src/identity/components/Name.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useName } from '../hooks/useName';
import { useAttestations } from '../hooks/useAttestations';
import { Name } from './Name';
import { Badge } from './Badge';
import { useIdentityContext } from '../context';
import { useIdentityContext } from './IdentityProvider';

jest.mock('../hooks/useName', () => ({
useName: jest.fn(),
Expand All @@ -19,7 +19,7 @@ jest.mock('../getSlicedAddress', () => ({
getSlicedAddress: jest.fn(),
}));

jest.mock('../context', () => ({
jest.mock('./IdentityProvider', () => ({
useIdentityContext: jest.fn(),
}));

Expand Down
2 changes: 1 addition & 1 deletion src/identity/components/Name.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Children, useMemo } from 'react';
import { useIdentityContext } from '../context';
import { useIdentityContext } from './IdentityProvider';
import { getSlicedAddress } from '../getSlicedAddress';
import { useName } from '../hooks/useName';
import type { NameReact } from '../types';
Expand Down
21 changes: 0 additions & 21 deletions src/identity/context.test.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions src/identity/context.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/identity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { Avatar } from './components/Avatar';
export { Badge } from './components/Badge';
export { Name } from './components/Name';
export { Identity } from './components/Identity';
export { IdentityLayout } from './components/IdentityLayout';
export { getAvatar } from './core/getAvatar';
export { getName } from './core/getName';
export { useAvatar } from './hooks/useAvatar';
Expand Down
9 changes: 8 additions & 1 deletion src/identity/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { HTMLAttributes, ImgHTMLAttributes, ReactNode } from 'react';
import type {
Dispatch,
HTMLAttributes,
ImgHTMLAttributes,
ReactNode,
SetStateAction,
} from 'react';
import type { Address, Chain } from 'viem';

/**
Expand Down Expand Up @@ -107,6 +113,7 @@ export type GetNameReturnType = string | null;
export type IdentityContextType = {
address: Address; // The Ethereum address to fetch the avatar and name for.
schemaId?: Address | null; // The Ethereum address of the schema to use for EAS attestation.
setAddress: Dispatch<SetStateAction<Address>>;
};

/**
Expand Down
5 changes: 5 additions & 0 deletions src/internal/hooks/useValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useMemo } from 'react';

export function useValue<T>(object: T): T {
return useMemo(() => object, [object]);
}
2 changes: 1 addition & 1 deletion src/internal/loading/Spinner.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('Spinner component', () => {

const spinner = spinnerContainer.firstChild;
expect(spinner).toHaveClass(
'animate-spin border-4 border-gray-200 border-t-3 rounded-full border-t-blue-500 px-2.5 py-2.5',
'animate-spin border-2 border-gray-200 border-t-3 rounded-full border-t-blue-500 px-2.5 py-2.5',
);
});
});
2 changes: 1 addition & 1 deletion src/internal/loading/Spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function Spinner() {
>
<div
className={cn(
'animate-spin border-4 border-gray-200 border-t-3',
'animate-spin border-2 border-gray-200 border-t-3',
'rounded-full border-t-blue-500 px-2.5 py-2.5',
)}
/>
Expand Down
Loading

0 comments on commit 7dbdc50

Please sign in to comment.