Skip to content

Commit

Permalink
Merge pull request #193 from 0xsequence/paginated-variant-of-useListC…
Browse files Browse the repository at this point in the history
…ollectibles

Paginated variant of use list collectibles
  • Loading branch information
AlexanderKolberg authored Feb 25, 2025
2 parents 7a18121 + 767620a commit 5c6fc9b
Show file tree
Hide file tree
Showing 8 changed files with 673 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { zeroAddress } from 'viem';
import { describe, expect, it } from 'vitest';
import {
mockCollectibleOrder,
mockMarketplaceEndpoint,
} from '../../_internal/api/__mocks__/marketplace.msw';
import { OrderSide } from '../../_internal/api/marketplace.gen';
import { renderHook } from '../../_internal/test-utils';
import { server } from '../../_internal/test/setup';
import { useListCollectiblesPaginated } from '../useListCollectiblesPaginated';
import type { UseListCollectiblesPaginatedArgs } from '../useListCollectiblesPaginated';

describe('useListCollectiblesPaginated', () => {
const defaultArgs: UseListCollectiblesPaginatedArgs = {
chainId: '1',
collectionAddress: zeroAddress,
side: OrderSide.listing,
query: {
enabled: true,
page: 1,
pageSize: 30,
},
};

it('should fetch collectibles successfully', async () => {
// Set up mock response
server.use(
http.post(mockMarketplaceEndpoint('ListCollectibles'), () => {
return HttpResponse.json({
collectibles: [mockCollectibleOrder],
page: { page: 1, pageSize: 30, more: false },
});
}),
);

const { result } = renderHook(() =>
useListCollectiblesPaginated(defaultArgs),
);

// Wait for the query to complete
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

// Verify the data
expect(result.current.data?.collectibles).toEqual([mockCollectibleOrder]);
expect(result.current.data?.page).toEqual({
page: 1,
pageSize: 30,
more: false,
});
});

it('should handle error states', async () => {
// Override the handler to return an error
server.use(
http.post(mockMarketplaceEndpoint('ListCollectibles'), () => {
return HttpResponse.json(
{ error: { message: 'Failed to fetch collectibles' } },
{ status: 500 },
);
}),
);

const { result } = renderHook(() =>
useListCollectiblesPaginated(defaultArgs),
);

// Wait for the error state
await waitFor(() => {
expect(result.current.isError).toBe(true);
});

expect(result.current.error).toBeDefined();
expect(result.current.data).toBeUndefined();
});

it('should handle pagination correctly', async () => {
// Set up mock response for page 2
const modifiedCollectibleOrder = {
...mockCollectibleOrder,
metadata: {
...mockCollectibleOrder.metadata,
tokenId: '2',
},
};

server.use(
http.post(mockMarketplaceEndpoint('ListCollectibles'), () => {
return HttpResponse.json({
collectibles: [modifiedCollectibleOrder],
page: { page: 2, pageSize: 20, more: false },
});
}),
);

const paginatedArgs: UseListCollectiblesPaginatedArgs = {
...defaultArgs,
query: {
enabled: true,
page: 2,
pageSize: 20,
},
};

const { result } = renderHook(() =>
useListCollectiblesPaginated(paginatedArgs),
);

// Wait for the query to complete
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

// Verify the data for page 2
expect(result.current.data?.collectibles[0].metadata.tokenId).toBe('2');
expect(result.current.data?.page).toEqual({
page: 2,
pageSize: 20,
more: false,
});
});

it('should refetch when args change', async () => {
// Set up initial mock response
server.use(
http.post(mockMarketplaceEndpoint('ListCollectibles'), () => {
return HttpResponse.json({
collectibles: [mockCollectibleOrder],
page: { page: 1, pageSize: 30, more: true },
});
}),
);

let currentArgs = defaultArgs;
const { result, rerender } = renderHook(() =>
useListCollectiblesPaginated(currentArgs),
);

// Wait for initial data
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

// Change args and rerender
currentArgs = {
...defaultArgs,
query: {
...defaultArgs.query,
page: 2,
},
};

// Set up mock response for page 2
const modifiedCollectibleOrder = {
...mockCollectibleOrder,
metadata: {
...mockCollectibleOrder.metadata,
tokenId: '2',
},
};

server.use(
http.post(mockMarketplaceEndpoint('ListCollectibles'), () => {
return HttpResponse.json({
collectibles: [modifiedCollectibleOrder],
page: { page: 2, pageSize: 30, more: false },
});
}),
);

rerender();

// Wait for new data
await waitFor(() => {
expect(result.current.data?.page?.page).toBe(2);
});

// Verify new data
expect(result.current.data?.collectibles[0].metadata.tokenId).toBe('2');
});

it('should not fetch when enabled is false', async () => {
// Set up a mock handler to ensure no requests are made
let requestMade = false;
server.use(
http.post(mockMarketplaceEndpoint('ListCollectibles'), () => {
requestMade = true;
return HttpResponse.json({
collectibles: [mockCollectibleOrder],
page: { page: 1, pageSize: 30, more: false },
});
}),
);

const disabledArgs: UseListCollectiblesPaginatedArgs = {
...defaultArgs,
query: {
enabled: false,
page: 1,
pageSize: 30,
},
};

const { result } = renderHook(() =>
useListCollectiblesPaginated(disabledArgs),
);

// For disabled queries, we expect no loading state and no data
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.error).toBeNull();
expect(requestMade).toBe(false);
});
});
1 change: 1 addition & 0 deletions packages/sdk/src/react/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from './useHighestOffer';
export * from './useListBalances';
export * from './useListCollectibleActivities';
export * from './useListCollectibles';
export * from './useListCollectiblesPaginated';
export * from './useListCollectionActivities';
export * from './useListOffersForCollectible';
export * from './useCountOffersForCollectible';
Expand Down
78 changes: 78 additions & 0 deletions packages/sdk/src/react/hooks/useListCollectiblesPaginated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { queryOptions, useQuery } from '@tanstack/react-query';
import { z } from 'zod';
import type { Page, SdkConfig } from '../../types';
import {
AddressSchema,
ChainIdSchema,
type ListCollectiblesArgs,
collectableKeys,
getMarketplaceClient,
} from '../_internal';
import { listCollectiblesArgsSchema } from '../_internal/api/zod-schema';
import { useConfig } from './useConfig';

const UseListCollectiblesPaginatedArgsSchema = listCollectiblesArgsSchema
.omit({
contractAddress: true,
})
.extend({
collectionAddress: AddressSchema,
chainId: ChainIdSchema.pipe(z.coerce.string()),
query: z
.object({
enabled: z.boolean().optional(),
page: z.number().optional().default(1),
pageSize: z.number().optional().default(30),
})
.optional()
.default({}),
});

export type UseListCollectiblesPaginatedArgs = z.infer<
typeof UseListCollectiblesPaginatedArgsSchema
>;

export type UseListCollectiblesPaginatedReturn = Awaited<
ReturnType<typeof fetchCollectiblesPaginated>
>;

const fetchCollectiblesPaginated = async (
args: UseListCollectiblesPaginatedArgs,
marketplaceClient: Awaited<ReturnType<typeof getMarketplaceClient>>,
) => {
const parsedArgs = UseListCollectiblesPaginatedArgsSchema.parse(args);
const page: Page = {
page: parsedArgs.query?.page ?? 1,
pageSize: parsedArgs.query?.pageSize ?? 30,
};

const arg = {
...parsedArgs,
contractAddress: parsedArgs.collectionAddress,
page,
} as ListCollectiblesArgs;

return marketplaceClient.listCollectibles(arg);
};

export const listCollectiblesPaginatedOptions = (
args: UseListCollectiblesPaginatedArgs,
config: SdkConfig,
) => {
const marketplaceClient = getMarketplaceClient(
args.chainId as string,
config,
);
return queryOptions({
queryKey: [...collectableKeys.lists, 'paginated', args],
queryFn: () => fetchCollectiblesPaginated(args, marketplaceClient),
enabled: args.query?.enabled ?? true,
});
};

export const useListCollectiblesPaginated = (
args: UseListCollectiblesPaginatedArgs,
) => {
const config = useConfig();
return useQuery(listCollectiblesPaginatedOptions(args, config));
};
12 changes: 12 additions & 0 deletions playgrounds/react-vite/src/lib/MarketplaceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import type { Hex } from 'viem';

export type Tab = 'collections' | 'collectibles' | 'collectible';
export type PaginationMode = 'paginated' | 'infinite';

interface MarketplaceContextType {
collectionAddress: Hex;
Expand All @@ -31,6 +32,8 @@ interface MarketplaceContextType {
setIsEmbeddedWalletEnabled: (enabled: boolean) => void;
orderbookKind: OrderbookKind | undefined;
setOrderbookKind: (kind: OrderbookKind | undefined) => void;
paginationMode: PaginationMode;
setPaginationMode: (mode: PaginationMode) => void;
}

const MarketplaceContext = createContext<MarketplaceContextType | undefined>(
Expand Down Expand Up @@ -73,6 +76,7 @@ interface StoredSettings {
projectId: string;
isEmbeddedWalletEnabled: boolean;
orderbookKind: OrderbookKind | undefined;
paginationMode: PaginationMode;
}

function loadStoredSettings(): Partial<StoredSettings> {
Expand Down Expand Up @@ -133,6 +137,10 @@ export function MarketplaceProvider({ children }: { children: ReactNode }) {
OrderbookKind | undefined
>();

const [paginationMode, setPaginationMode] = useState<PaginationMode>(
stored.paginationMode ?? 'infinite',
);

// Save settings whenever they change
useEffect(() => {
saveSettings({
Expand All @@ -142,6 +150,7 @@ export function MarketplaceProvider({ children }: { children: ReactNode }) {
projectId,
isEmbeddedWalletEnabled,
orderbookKind,
paginationMode,
});
}, [
collectionAddress,
Expand All @@ -150,6 +159,7 @@ export function MarketplaceProvider({ children }: { children: ReactNode }) {
projectId,
isEmbeddedWalletEnabled,
orderbookKind,
paginationMode,
]);

const waasConfigKey =
Expand Down Expand Up @@ -185,6 +195,8 @@ export function MarketplaceProvider({ children }: { children: ReactNode }) {
setIsEmbeddedWalletEnabled,
orderbookKind,
setOrderbookKind,
paginationMode,
setPaginationMode,
sdkConfig: {
projectId,
projectAccessKey,
Expand Down
Loading

0 comments on commit 5c6fc9b

Please sign in to comment.