forked from tracychen/coinbase-onchainkit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new OnchainName component (coinbase#49)
- Loading branch information
1 parent
dce6f67
commit 8af8505
Showing
19 changed files
with
1,558 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Jest is an amazing test runner and has some awesome assertion APIs | ||
// built in by default. However, there are times when having more | ||
// specific matchers (assertions) would be far more convenient. | ||
// https://jest-extended.jestcommunity.dev/docs/matchers/ | ||
import 'jest-extended'; | ||
// Enable jest-dom functions | ||
import '@testing-library/jest-dom'; | ||
|
||
import { TextEncoder, TextDecoder } from 'util'; | ||
|
||
global.TextEncoder = TextEncoder; | ||
global.TextDecoder = TextDecoder as typeof global.TextDecoder; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import { OnchainName } from './OnchainName'; | ||
import { useOnchainName } from '../hooks/useOnchainName'; | ||
import { getSlicedAddress } from '../core/address'; | ||
|
||
// Mocking the hooks and utilities | ||
jest.mock('../hooks/useOnchainName', () => ({ | ||
useOnchainName: jest.fn(), | ||
})); | ||
jest.mock('../core/address', () => ({ | ||
getSlicedAddress: jest.fn(), | ||
})); | ||
|
||
const mockSliceAddress = (addr: string) => addr.slice(0, 6) + '...' + addr.slice(-4); | ||
|
||
describe('OnchainAddress', () => { | ||
const testAddress = '0x1234567890abcdef1234567890abcdef12345678'; | ||
const testName = 'testname.eth'; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
(getSlicedAddress as jest.Mock).mockImplementation(mockSliceAddress); | ||
}); | ||
|
||
it('displays ENS name when available', () => { | ||
(useOnchainName as jest.Mock).mockReturnValue({ ensName: testName, isLoading: false }); | ||
|
||
render(<OnchainName address={testAddress} />); | ||
|
||
expect(screen.getByText(testName)).toBeInTheDocument(); | ||
expect(getSlicedAddress).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it('displays sliced address when ENS name is not available and sliced is true as default', () => { | ||
(useOnchainName as jest.Mock).mockReturnValue({ ensName: null, isLoading: false }); | ||
|
||
render(<OnchainName address={testAddress} />); | ||
|
||
expect(screen.getByText(mockSliceAddress(testAddress))).toBeInTheDocument(); | ||
}); | ||
|
||
it('displays empty when ens still fetching', () => { | ||
(useOnchainName as jest.Mock).mockReturnValue({ ensName: null, isLoading: true }); | ||
|
||
render(<OnchainName address={testAddress} />); | ||
|
||
expect(screen.queryByText(mockSliceAddress(testAddress))).not.toBeInTheDocument(); | ||
expect(getSlicedAddress).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it('displays full address when ENS name is not available and sliced is false', () => { | ||
(useOnchainName as jest.Mock).mockReturnValue({ ensName: null, isLoading: false }); | ||
|
||
render(<OnchainName address={testAddress} sliced={false} />); | ||
|
||
expect(screen.getByText(testAddress)).toBeInTheDocument(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import React, { useMemo } from 'react'; | ||
import { getSlicedAddress } from '../core/address'; | ||
import { useOnchainName } from '../hooks/useOnchainName'; | ||
import type { Address } from 'viem'; | ||
|
||
type OnchainNameProps = { | ||
address: Address; | ||
className?: string; | ||
sliced?: boolean; | ||
props?: React.HTMLAttributes<HTMLSpanElement>; | ||
}; | ||
|
||
/** | ||
* OnchainName is a React component that renders the user name from an Ethereum address. | ||
* It displays the ENS name if available; otherwise, it shows either a sliced version of the address | ||
* or the full address, based on the 'sliced' prop. By default, 'sliced' is set to true. | ||
* | ||
* @param {Address} address - Ethereum address to be displayed. | ||
* @param {string} [className] - Optional CSS class for custom styling. | ||
* @param {boolean} [sliced=true] - Determines if the address should be sliced when no ENS name is available. | ||
* @param {React.HTMLAttributes<HTMLSpanElement>} [props] - Additional HTML attributes for the span element. | ||
*/ | ||
export function OnchainName({ address, className, sliced = true, props }: OnchainNameProps) { | ||
const { ensName, isLoading } = useOnchainName(address); | ||
|
||
// wrapped in useMemo to prevent unnecessary recalculations. | ||
const normalizedAddress = useMemo(() => { | ||
if (!ensName && !isLoading && sliced) { | ||
return getSlicedAddress(address); | ||
} | ||
return address; | ||
}, [address, isLoading]); | ||
|
||
if (isLoading) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<span className={className} {...props}> | ||
{ensName ?? normalizedAddress} | ||
</span> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { getSlicedAddress } from './address'; | ||
|
||
describe('getSlicedAddress', () => { | ||
it('should return a string of class names', () => { | ||
const address = getSlicedAddress('0x1234567890123456789012345678901234567890'); | ||
expect(address).toEqual('0x123...7890'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* getSlicedAddress returns the first 5 and last 4 characters of an address. | ||
*/ | ||
export const getSlicedAddress = (address: `0x${string}` | undefined) => { | ||
if (!address) { | ||
return ''; | ||
} | ||
return `${address.slice(0, 5)}...${address.slice(-4)}`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/** | ||
* @jest-environment jsdom | ||
*/ | ||
|
||
import { renderHook, waitFor } from '@testing-library/react'; | ||
import { useOnchainActionWithCache } from './useOnchainActionWithCache'; | ||
import { InMemoryStorage } from '../store/inMemoryStorageService'; | ||
|
||
jest.mock('../store/inMemoryStorageService', () => ({ | ||
InMemoryStorage: { | ||
getData: jest.fn(), | ||
setData: jest.fn(), | ||
}, | ||
})); | ||
|
||
describe('useOnchainActionWithCache', () => { | ||
const mockAction = jest.fn(); | ||
const actionKey = 'testKey'; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('initializes with loading state and undefined data', () => { | ||
const { result } = renderHook(() => useOnchainActionWithCache(mockAction, actionKey)); | ||
|
||
expect(result.current.isLoading).toBe(true); | ||
expect(result.current.data).toBeUndefined(); | ||
}); | ||
|
||
it('fetches data and updates state', async () => { | ||
const testData = 'testData'; | ||
mockAction.mockResolvedValue(testData); | ||
|
||
const { result } = renderHook(() => useOnchainActionWithCache(mockAction, actionKey)); | ||
|
||
await waitFor(() => { | ||
expect(mockAction).toHaveBeenCalled(); | ||
expect(result.current.data).toBe(testData); | ||
expect(result.current.isLoading).toBe(false); | ||
}); | ||
}); | ||
|
||
it('caches data when an actionKey is provided', async () => { | ||
const testData = 'testData'; | ||
mockAction.mockResolvedValue(testData); | ||
|
||
renderHook(() => useOnchainActionWithCache(mockAction, actionKey)); | ||
|
||
await waitFor(() => { | ||
expect(InMemoryStorage.setData).toHaveBeenCalledWith(actionKey, testData); | ||
}); | ||
}); | ||
|
||
it('does not cache data when actionKey is empty', async () => { | ||
const testData = 'testData'; | ||
mockAction.mockResolvedValue(testData); | ||
|
||
renderHook(() => useOnchainActionWithCache(mockAction, '')); | ||
|
||
await waitFor(() => { | ||
expect(InMemoryStorage.setData).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { useEffect, useState } from 'react'; | ||
import { ActionFunction, ActionKey, StorageValue } from '../types'; | ||
import { InMemoryStorage } from '../store/inMemoryStorageService'; // Adjust the import path as needed | ||
|
||
type ExtractStorageValue<T> = T extends StorageValue ? T : never; | ||
|
||
/** | ||
* A generic hook to fetch and store data using a specified storage service. | ||
* It fetches data based on the given dependencies and stores it using the provided storage service. | ||
* @param action - The action function to fetch data. | ||
* @param actionKey - A key associated with the action for caching purposes. | ||
* @returns The data fetched by the action function and a boolean indicating whether the data is being fetched. | ||
*/ | ||
export function useOnchainActionWithCache<T>(action: ActionFunction<T>, actionKey: ActionKey) { | ||
const [data, setData] = useState<StorageValue>(undefined); | ||
const [isLoading, setIsLoading] = useState(true); | ||
|
||
useEffect(() => { | ||
let isSubscribed = true; | ||
|
||
const callAction = async () => { | ||
let fetchedData: StorageValue; | ||
// Use cache only if actionKey is not empty | ||
if (actionKey) { | ||
fetchedData = await InMemoryStorage.getData(actionKey); | ||
} | ||
|
||
// If no cached data or actionKey is empty, fetch new data | ||
if (!fetchedData) { | ||
fetchedData = (await action()) as ExtractStorageValue<T>; | ||
// Cache the data only if actionKey is not empty | ||
if (actionKey) { | ||
await InMemoryStorage.setData(actionKey, fetchedData); | ||
} | ||
} | ||
|
||
if (isSubscribed) { | ||
setData(fetchedData); | ||
setIsLoading(false); | ||
} | ||
}; | ||
|
||
void callAction(); | ||
|
||
return () => { | ||
isSubscribed = false; | ||
}; | ||
}, [actionKey, action]); | ||
|
||
return { data, isLoading }; | ||
} |
Oops, something went wrong.