diff --git a/packages/react-native/lib/documentsHooks.test.js b/packages/react-native/lib/documentsHooks.test.js new file mode 100644 index 00000000..ee2a7b5e --- /dev/null +++ b/packages/react-native/lib/documentsHooks.test.js @@ -0,0 +1,146 @@ +import { useDocument, useDocuments } from './documentsHooks'; +import { WalletEvents } from '@docknetwork/wallet-sdk-wasm/src/modules/wallet'; +import { getWallet } from './wallet'; +import { act, renderHook } from '@testing-library/react-hooks'; + +const mockDocument = { id: 'mock-document-id' }; + +const mockWallet = { + getDocumentById: jest.fn(() => Promise.resolve(mockDocument)), + getDocumentsByType: jest.fn(() => Promise.resolve([])), + eventManager: { + on: jest.fn(), + removeListener: jest.fn(), + }, +}; + +jest.mock('./wallet', () => ({ + getWallet: jest.fn(() => mockWallet), +})); + +describe('useDocument', () => { + + beforeEach(() => { + getWallet.mockReturnValue(mockWallet); + mockWallet.getDocumentById.mockReset(); + mockWallet.eventManager.on.mockReset(); + mockWallet.eventManager.removeListener.mockReset(); + }); + + it('should fetch the document by documentId', async () => { + const documentId = 'document-id'; + const document = { id: documentId, content: 'content' }; + mockWallet.getDocumentById.mockResolvedValue(document); + + const { result, waitFor } = renderHook(() => + useDocument(documentId), + ); + + await waitFor(() => expect(getWallet).toHaveBeenCalled()) + expect(mockWallet.getDocumentById).toHaveBeenCalledWith(documentId); + expect(result.current).toEqual(document); + }); + + it('should fetch the document when documentUpdated event is emitted', async () => { + const documentId = 'document-id'; + const initialDocument = { id: documentId, content: 'content' }; + const updatedDocument = { id: documentId, content: 'updated content' }; + mockWallet.getDocumentById + .mockResolvedValueOnce(initialDocument) + .mockResolvedValueOnce(updatedDocument); + + const { result, waitFor } = renderHook(() => + useDocument(documentId), + ); + + act(() => { + mockWallet.eventManager.on.mock.calls[0][1](updatedDocument); + }); + + await waitFor(() => expect(mockWallet.getDocumentById).toHaveBeenCalledTimes(2)); + expect(result.current).toEqual(updatedDocument); + }); + + it('should fetch the document when documentAdded event is emitted', async () => { + const documentId = 'document-id'; + const initialDocument = null; + const newDocument = { id: documentId, content: 'added content' }; + mockWallet.getDocumentById + .mockResolvedValueOnce(initialDocument) + .mockResolvedValueOnce(newDocument); + + const { result, waitFor } = renderHook(() => + useDocument(documentId), + ); + + act(() => { + mockWallet.eventManager.on.mock.calls[0][1](newDocument); + }); + + await waitFor(() => expect(mockWallet.getDocumentById).toHaveBeenCalledTimes(2)); + expect(result.current).toEqual(newDocument); + }); + + it('should fetch the document when documentRemoved event is emitted', async () => { + const documentId = 'document-id'; + const initialDocument = { id: documentId, content: 'content' }; + const newDocument = { id: documentId, content: 'updated content' }; + mockWallet.getDocumentById + .mockResolvedValueOnce(initialDocument) + .mockResolvedValueOnce(newDocument); + + const { result, waitFor } = renderHook(() => + useDocument(documentId), + ); + + act(() => { + mockWallet.eventManager.on.mock.calls[0][1](newDocument); + }); + + await waitFor(() => expect(mockWallet.getDocumentById).toHaveBeenCalledTimes(2)); + expect(result.current).toEqual(newDocument); + }); + +}); + +describe('useDocuments', () => { + beforeEach(() => { + getWallet.mockReturnValue(mockWallet); + mockWallet.getDocumentById.mockReset(); + mockWallet.eventManager.on.mockReset(); + mockWallet.eventManager.removeListener.mockReset(); + }); + + it('should fetch the document correctly', async () => { + const type = 'type1'; + const documents = [ + { id: 'doc1', type }, + { id: 'doc2', type }, + ]; + mockWallet.getDocumentsByType.mockResolvedValue(documents); + + const { result, waitFor } = renderHook(() => useDocuments({ type })); + + await waitFor(() => expect(mockWallet.getDocumentsByType).toHaveBeenCalledWith(type)); + expect(result.current.documents).toEqual(documents); + expect(result.current.loading).toEqual(false); + }); + it('should refetch documents on networkUpdated event', async () => { + const { waitFor } = renderHook(() => + useDocuments({ type: 'mockType' }), + ); + await mockWallet.eventManager.on.mock.calls[2][1](); + + expect(mockWallet.getDocumentsByType).toHaveBeenCalledWith('mockType'); + }); + + it('should force refetch documents on networkUpdated event if type is not set', async () => { + const { waitFor } = renderHook(() => + useDocuments(), + ); + mockWallet.getDocumentsByType.mockReset(); + await mockWallet.eventManager.on.mock.calls[3][1](); + + expect(mockWallet.getDocumentsByType).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/react-native/lib/documentsHooks.ts b/packages/react-native/lib/documentsHooks.ts new file mode 100644 index 00000000..6def3b03 --- /dev/null +++ b/packages/react-native/lib/documentsHooks.ts @@ -0,0 +1,77 @@ +import {WalletEvents} from '@docknetwork/wallet-sdk-wasm/src/modules/wallet'; +import {useCallback, useEffect, useState} from 'react'; +import {getWallet} from './wallet'; + +const useEventListener = (eventManager, eventNames, listener) => { + useEffect(() => { + eventNames.forEach(eventName => eventManager.on(eventName, listener)); + return () => + eventNames.forEach(eventName => + eventManager.removeListener(eventName, listener), + ); + }, [eventManager, eventNames, listener]); +}; + +const events = [ + WalletEvents.documentAdded, + WalletEvents.documentRemoved, + WalletEvents.documentUpdated, +]; + +export function useDocument(id) { + const [document, setDocument] = useState(null); + + const refetchDocument = useCallback( + async updatedDoc => { + if (updatedDoc.id !== id) return; + const doc = await getWallet().getDocumentById(id); + setDocument(doc); + }, + [id], + ); + + useEffect(() => { + getWallet().getDocumentById(id).then(setDocument); + }, [id]); + + useEventListener(getWallet().eventManager, events, refetchDocument); + + return document; +} + +export function useDocuments({type = null} = {}) { + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchDocuments = useCallback( + async (updatedDoc, forceFetch = false) => { + console.log('fetching documents', updatedDoc, forceFetch); + if ( + forceFetch || + updatedDoc?.type === type || + updatedDoc?.type?.includes(type) + ) { + const docs = await getWallet().getDocumentsByType(type); + setDocuments(docs); + setLoading(false); + } + }, + [type], + ); + + useEffect(() => { + fetchDocuments(null, true); + }, [fetchDocuments, setLoading]); + + useEventListener(getWallet().eventManager, events, fetchDocuments); + useEventListener( + getWallet().eventManager, + [WalletEvents.networkUpdated], + async () => fetchDocuments(null, true), + ); + + return { + documents, + loading, + }; +} diff --git a/packages/react-native/lib/index.tsx b/packages/react-native/lib/index.tsx index 460a2a29..93944e75 100644 --- a/packages/react-native/lib/index.tsx +++ b/packages/react-native/lib/index.tsx @@ -53,9 +53,12 @@ export const WalletSDKContext = React.createContext({ }); setStorage(AsyncStorage); + export {useAccounts}; export {useDIDManagement}; export {useCredentialUtils, useCredentialStatus}; +export {useDocument, useDocuments} from './documentsHooks'; + export function getStorage() { return AsyncStorage; } @@ -103,75 +106,6 @@ export function useAccount(address) { onDelete, }; } - -const useEventListener = (eventManager, eventNames, listener) => { - useEffect(() => { - eventNames.forEach(eventName => eventManager.on(eventName, listener)); - return () => - eventNames.forEach(eventName => - eventManager.removeListener(eventName, listener), - ); - }, [eventManager, eventNames, listener]); -}; - -const events = [ - WalletEvents.documentAdded, - WalletEvents.documentRemoved, - WalletEvents.documentUpdated, -]; - -export function useDocument(id) { - const [document, setDocument] = useState(null); - - const refetchDocument = useCallback( - async updatedDoc => { - if (updatedDoc.id !== id) return; - const doc = await getWallet().getDocumentById(id); - setDocument(doc); - }, - [id], - ); - - useEffect(() => { - getWallet().getDocumentById(id).then(setDocument); - }, [id]); - - useEventListener(getWallet().eventManager, events, refetchDocument); - - return document; -} - -export function useDocuments({type}) { - const [documents, setDocuments] = useState([]); - const [loading, setLoading] = useState(true); - - const fetchDocuments = useCallback( - async (updatedDoc, forceFetch = false) => { - if ( - forceFetch || - updatedDoc?.type === type || - updatedDoc?.type?.includes(type) - ) { - const docs = await getWallet().getDocumentsByType(type); - setDocuments(docs); - setLoading(false); - } - }, - [type], - ); - - useEffect(() => { - fetchDocuments(null, true); - }, [fetchDocuments, setLoading]); - - useEventListener(getWallet().eventManager, events, fetchDocuments); - - return { - documents, - loading, - }; -} - export function useWallet() { return useContext(WalletSDKContext); } diff --git a/packages/react-native/package.json b/packages/react-native/package.json index d1f1424f..d29c2ab5 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -36,7 +36,7 @@ "ts-node": "^10.9.1", "typescript": "^5.0.4", "@testing-library/react-hooks": "^8.0.0", - "react-test-renderer": "17.0.2", + "react-test-renderer": "18.2.0", "react": "^18.2.0", "@types/react": "^18.0.24", "react-dom": "^17.0.2", diff --git a/yarn.lock b/yarn.lock index 74972423..ae9186cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13201,11 +13201,16 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1, react-is@^17.0.2: +react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + react-native-keychain@^8.0.0: version "8.1.2" resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-8.1.2.tgz#34291ae472878e5124d081211af5ede7d810e64f" @@ -13226,7 +13231,7 @@ react-native-webview@^11.4.3: escape-string-regexp "2.0.0" invariant "2.2.4" -react-shallow-renderer@^16.13.1: +react-shallow-renderer@^16.15.0: version "16.15.0" resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== @@ -13234,15 +13239,14 @@ react-shallow-renderer@^16.13.1: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0 || ^18.0.0" -react-test-renderer@17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" - integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== +react-test-renderer@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-18.2.0.tgz#1dd912bd908ff26da5b9fca4fd1c489b9523d37e" + integrity sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA== dependencies: - object-assign "^4.1.1" - react-is "^17.0.2" - react-shallow-renderer "^16.13.1" - scheduler "^0.20.2" + react-is "^18.2.0" + react-shallow-renderer "^16.15.0" + scheduler "^0.23.0" react@^17.0.2: version "17.0.2" @@ -13904,6 +13908,13 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" +scheduler@^0.23.0: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + schema-utils@^2.6.5: version "2.7.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"