Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions app/src/hooks/useRecoveryPrompts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// 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 { useEffect } from 'react';
import { useEffect, useRef } from 'react';

import { useModal } from '@/hooks/useModal';
import { navigationRef } from '@/navigation';
Expand All @@ -27,6 +27,8 @@ export default function useRecoveryPrompts() {
onModalDismiss: () => {},
} as const);

const lastPromptCount = useRef<number | null>(null);

useEffect(() => {
async function maybePrompt() {
if (!navigationRef.isReady()) {
Expand All @@ -40,8 +42,13 @@ export default function useRecoveryPrompts() {
}
const shouldPrompt =
loginCount > 0 && (loginCount <= 3 || (loginCount - 3) % 5 === 0);
if (shouldPrompt) {
if (
shouldPrompt &&
!visible &&
lastPromptCount.current !== loginCount
) {
showModal();
lastPromptCount.current = loginCount;
}
} catch {
// Silently fail to avoid breaking the hook
Expand All @@ -55,6 +62,7 @@ export default function useRecoveryPrompts() {
loginCount,
cloudBackupEnabled,
hasViewedRecoveryPhrase,
visible,
showModal,
getAllDocuments,
]);
Expand Down
39 changes: 38 additions & 1 deletion app/tests/src/hooks/useRecoveryPrompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ jest.mock('@/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();
(useModal as jest.Mock).mockReturnValue({ showModal, visible: false });
getAllDocuments.mockResolvedValue({ doc1: {} as any });
act(() => {
useSettingStore.setState({
Expand Down Expand Up @@ -86,6 +86,17 @@ describe('useRecoveryPrompts', () => {
});
});

it('does not show modal if already visible', async () => {
(useModal as jest.Mock).mockReturnValueOnce({ showModal, visible: true });
act(() => {
useSettingStore.setState({ loginCount: 1 });
});
renderHook(() => useRecoveryPrompts());
await waitFor(() => {
expect(showModal).not.toHaveBeenCalled();
});
});

it('does not show modal when recovery phrase has been viewed', async () => {
act(() => {
useSettingStore.setState({
Expand Down Expand Up @@ -123,6 +134,32 @@ describe('useRecoveryPrompts', () => {
}
});

it('does not show modal again for same login count when state changes', async () => {
act(() => {
useSettingStore.setState({ loginCount: 1 });
});
renderHook(() => useRecoveryPrompts());
await waitFor(() => {
expect(showModal).toHaveBeenCalledTimes(1);
});

showModal.mockClear();

act(() => {
useSettingStore.setState({ hasViewedRecoveryPhrase: true });
});
await waitFor(() => {
expect(showModal).not.toHaveBeenCalled();
});

act(() => {
useSettingStore.setState({ hasViewedRecoveryPhrase: false });
});
await waitFor(() => {
expect(showModal).not.toHaveBeenCalled();
});
});

it('returns correct visible state', () => {
const { result } = renderHook(() => useRecoveryPrompts());
expect(result.current.visible).toBe(false);
Expand Down
Loading