diff --git a/app/src/hooks/useRecoveryPrompts.ts b/app/src/hooks/useRecoveryPrompts.ts index 61bcf3f45..abe85947c 100644 --- a/app/src/hooks/useRecoveryPrompts.ts +++ b/app/src/hooks/useRecoveryPrompts.ts @@ -3,12 +3,14 @@ import { useEffect } from 'react'; import { navigationRef } from '../navigation'; +import { usePassport } from '../providers/passportDataProvider'; import { useSettingStore } from '../stores/settingStore'; import { useModal } from './useModal'; export default function useRecoveryPrompts() { const { loginCount, cloudBackupEnabled, hasViewedRecoveryPhrase } = useSettingStore(); + const { getAllDocuments } = usePassport(); const { showModal, visible } = useModal({ titleText: 'Protect your account', bodyText: @@ -25,17 +27,36 @@ export default function useRecoveryPrompts() { } as const); useEffect(() => { - if (!navigationRef.isReady()) { - return; - } - if (!cloudBackupEnabled && !hasViewedRecoveryPhrase) { - const shouldPrompt = - loginCount > 0 && (loginCount <= 3 || (loginCount - 3) % 5 === 0); - if (shouldPrompt) { - showModal(); + async function maybePrompt() { + if (!navigationRef.isReady()) { + return; + } + if (!cloudBackupEnabled && !hasViewedRecoveryPhrase) { + try { + const docs = await getAllDocuments(); + if (Object.keys(docs).length === 0) { + return; + } + const shouldPrompt = + loginCount > 0 && (loginCount <= 3 || (loginCount - 3) % 5 === 0); + if (shouldPrompt) { + showModal(); + } + } catch (error) { + // Silently fail to avoid breaking the hook + // If we can't get documents, we shouldn't show the prompt + return; + } } } - }, [loginCount, cloudBackupEnabled, hasViewedRecoveryPhrase, showModal]); + void maybePrompt(); + }, [ + loginCount, + cloudBackupEnabled, + hasViewedRecoveryPhrase, + showModal, + getAllDocuments, + ]); return { visible }; } diff --git a/app/tests/src/hooks/useRecoveryPrompts.test.ts b/app/tests/src/hooks/useRecoveryPrompts.test.ts index 10c726ef4..af43fa5b7 100644 --- a/app/tests/src/hooks/useRecoveryPrompts.test.ts +++ b/app/tests/src/hooks/useRecoveryPrompts.test.ts @@ -1,12 +1,14 @@ // SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11 -import { renderHook } from '@testing-library/react-native'; +import { renderHook, waitFor } from '@testing-library/react-native'; import { useModal } from '../../../src/hooks/useModal'; import useRecoveryPrompts from '../../../src/hooks/useRecoveryPrompts'; +import { usePassport } from '../../../src/providers/passportDataProvider'; import { useSettingStore } from '../../../src/stores/settingStore'; jest.mock('../../../src/hooks/useModal'); +jest.mock('../../../src/providers/passportDataProvider'); jest.mock('../../../src/navigation', () => ({ navigationRef: { isReady: jest.fn(() => true), @@ -16,10 +18,13 @@ jest.mock('../../../src/navigation', () => ({ const showModal = jest.fn(); (useModal as jest.Mock).mockReturnValue({ showModal, visible: false }); +const getAllDocuments = jest.fn(); +(usePassport as jest.Mock).mockReturnValue({ getAllDocuments }); describe('useRecoveryPrompts', () => { beforeEach(() => { showModal.mockClear(); + getAllDocuments.mockResolvedValue({ doc1: {} as any }); useSettingStore.setState({ loginCount: 0, cloudBackupEnabled: false, @@ -27,51 +32,74 @@ describe('useRecoveryPrompts', () => { }); }); - it('shows modal on first login', () => { + it('shows modal on first login', async () => { useSettingStore.setState({ loginCount: 1 }); renderHook(() => useRecoveryPrompts()); - expect(showModal).toHaveBeenCalled(); + await waitFor(() => { + expect(showModal).toHaveBeenCalled(); + }); }); - it('does not show modal when login count is 4', () => { + it('does not show modal when login count is 4', async () => { useSettingStore.setState({ loginCount: 4 }); renderHook(() => useRecoveryPrompts()); - expect(showModal).not.toHaveBeenCalled(); + await waitFor(() => { + expect(showModal).not.toHaveBeenCalled(); + }); }); - it('shows modal on eighth login', () => { + it('shows modal on eighth login', async () => { useSettingStore.setState({ loginCount: 8 }); renderHook(() => useRecoveryPrompts()); - expect(showModal).toHaveBeenCalled(); + await waitFor(() => { + expect(showModal).toHaveBeenCalled(); + }); }); - it('does not show modal if backup already enabled', () => { + it('does not show modal if backup already enabled', async () => { useSettingStore.setState({ loginCount: 1, cloudBackupEnabled: true }); renderHook(() => useRecoveryPrompts()); - expect(showModal).not.toHaveBeenCalled(); + await waitFor(() => { + expect(showModal).not.toHaveBeenCalled(); + }); }); - it('does not show modal when navigation is not ready', () => { + it('does not show modal when navigation is not ready', async () => { const navigationRef = require('../../../src/navigation').navigationRef; navigationRef.isReady.mockReturnValueOnce(false); useSettingStore.setState({ loginCount: 1 }); renderHook(() => useRecoveryPrompts()); - expect(showModal).not.toHaveBeenCalled(); + await waitFor(() => { + expect(showModal).not.toHaveBeenCalled(); + }); }); - it('does not show modal when recovery phrase has been viewed', () => { + it('does not show modal when recovery phrase has been viewed', async () => { useSettingStore.setState({ loginCount: 1, hasViewedRecoveryPhrase: true }); renderHook(() => useRecoveryPrompts()); - expect(showModal).not.toHaveBeenCalled(); + await waitFor(() => { + expect(showModal).not.toHaveBeenCalled(); + }); }); - it('shows modal for other valid login counts', () => { - [2, 3, 13, 18].forEach(count => { + it('does not show modal when no documents exist', async () => { + getAllDocuments.mockResolvedValueOnce({}); + useSettingStore.setState({ loginCount: 1 }); + renderHook(() => useRecoveryPrompts()); + await waitFor(() => { + expect(showModal).not.toHaveBeenCalled(); + }); + }); + + it('shows modal for other valid login counts', async () => { + for (const count of [2, 3, 13, 18]) { showModal.mockClear(); useSettingStore.setState({ loginCount: count }); renderHook(() => useRecoveryPrompts()); - expect(showModal).toHaveBeenCalled(); - }); + await waitFor(() => { + expect(showModal).toHaveBeenCalled(); + }); + } }); it('returns correct visible state', () => {