Skip to content

Commit 89366b4

Browse files
authored
Merge pull request #316 from internxt/bugfix/PB-4966-display-correct-screen-protection-state
[PB-4966] bugfix/Fix screen protection state
2 parents 5efc705 + 058a0de commit 89366b4

7 files changed

Lines changed: 308 additions & 19 deletions

File tree

src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
Text,
1212
View,
1313
} from 'react-native';
14-
import { CaptureProtection, CaptureProtectionProvider } from 'react-native-capture-protection';
14+
import { CaptureProtectionProvider } from 'react-native-capture-protection';
1515
import { GestureHandlerRootView } from 'react-native-gesture-handler';
1616
import { SafeAreaProvider } from 'react-native-safe-area-context';
1717
import { useTailwind } from 'tailwind-rn';
@@ -25,6 +25,7 @@ import LinkCopiedModal from './components/modals/LinkCopiedModal';
2525
import { DriveContextProvider } from './contexts/Drive';
2626
import { getRemoteUpdateIfAvailable, useLoadFonts } from './helpers';
2727
import useGetColor from './hooks/useColor';
28+
import { useScreenProtection } from './hooks/useScreenProtection';
2829
import { useSecurity } from './hooks/useSecurity';
2930
import Navigation from './navigation';
3031
import { LockScreen } from './screens/common/LockScreen';
@@ -55,6 +56,8 @@ export default function App(): JSX.Element {
5556
const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>('light');
5657
const { performPeriodicSecurityCheck } = useSecurity();
5758

59+
useScreenProtection();
60+
5861
useEffect(() => {
5962
const initializeTheme = async () => {
6063
const savedTheme = await asyncStorageService.getThemePreference();
@@ -207,8 +210,6 @@ export default function App(): JSX.Element {
207210
};
208211

209212
useEffect(() => {
210-
CaptureProtection.prevent();
211-
212213
const initializeTheme = async () => {
213214
const savedTheme = await asyncStorageService.getThemePreference();
214215
if (savedTheme) {
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { act, renderHook, waitFor } from '@testing-library/react-native';
2+
import { CaptureProtection } from 'react-native-capture-protection';
3+
import asyncStorageService from '../services/AsyncStorageService';
4+
import { logger } from '../services/common';
5+
import { useScreenProtection } from './useScreenProtection';
6+
7+
jest.mock('react-native-capture-protection', () => ({
8+
CaptureProtection: {
9+
prevent: jest.fn(),
10+
allow: jest.fn(),
11+
},
12+
}));
13+
14+
jest.mock('../services/AsyncStorageService', () => ({
15+
getScreenProtectionEnabled: jest.fn(),
16+
saveScreenProtectionEnabled: jest.fn(),
17+
}));
18+
19+
jest.mock('../services/common', () => ({
20+
logger: {
21+
error: jest.fn(),
22+
},
23+
}));
24+
25+
describe('useScreenProtection', () => {
26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
afterEach(() => {
30+
jest.clearAllMocks();
31+
});
32+
33+
describe('initialization', () => {
34+
it('should initialize with protection enabled when saved preference is true', async () => {
35+
(asyncStorageService.getScreenProtectionEnabled as jest.Mock).mockResolvedValue(true);
36+
37+
const { result } = renderHook(() => useScreenProtection());
38+
39+
expect(result.current.isInitialized).toBe(false);
40+
41+
await waitFor(() => {
42+
expect(result.current.isInitialized).toBe(true);
43+
});
44+
45+
expect(result.current.isEnabled).toBe(true);
46+
expect(CaptureProtection.prevent).toHaveBeenCalledTimes(1);
47+
expect(CaptureProtection.allow).not.toHaveBeenCalled();
48+
expect(asyncStorageService.getScreenProtectionEnabled).toHaveBeenCalledTimes(1);
49+
});
50+
51+
it('should initialize with protection disabled when saved preference is false', async () => {
52+
(asyncStorageService.getScreenProtectionEnabled as jest.Mock).mockResolvedValue(false);
53+
54+
const { result } = renderHook(() => useScreenProtection());
55+
56+
await waitFor(() => {
57+
expect(result.current.isInitialized).toBe(true);
58+
});
59+
60+
expect(result.current.isEnabled).toBe(false);
61+
expect(CaptureProtection.allow).toHaveBeenCalledTimes(1);
62+
expect(CaptureProtection.prevent).not.toHaveBeenCalled();
63+
});
64+
65+
it('should default to enabled on initialization error', async () => {
66+
const error = new Error('Storage error');
67+
(asyncStorageService.getScreenProtectionEnabled as jest.Mock).mockRejectedValue(error);
68+
69+
const { result } = renderHook(() => useScreenProtection());
70+
71+
await waitFor(() => {
72+
expect(result.current.isInitialized).toBe(true);
73+
});
74+
75+
expect(result.current.isEnabled).toBe(true);
76+
expect(CaptureProtection.prevent).toHaveBeenCalledTimes(1);
77+
expect(logger.error).toHaveBeenCalledWith('Error initializing screen protection:', error);
78+
});
79+
});
80+
81+
describe('setScreenProtection', () => {
82+
beforeEach(async () => {
83+
(asyncStorageService.getScreenProtectionEnabled as jest.Mock).mockResolvedValue(false);
84+
});
85+
86+
it('should enable screen protection when called with true', async () => {
87+
(asyncStorageService.saveScreenProtectionEnabled as jest.Mock).mockResolvedValue(undefined);
88+
89+
const { result } = renderHook(() => useScreenProtection());
90+
91+
await waitFor(() => {
92+
expect(result.current.isInitialized).toBe(true);
93+
});
94+
95+
await act(async () => {
96+
await result.current.setScreenProtection(true);
97+
});
98+
99+
expect(result.current.isEnabled).toBe(true);
100+
expect(CaptureProtection.prevent).toHaveBeenCalled();
101+
expect(asyncStorageService.saveScreenProtectionEnabled).toHaveBeenCalledWith(true);
102+
});
103+
104+
it('should disable screen protection when called with false', async () => {
105+
(asyncStorageService.getScreenProtectionEnabled as jest.Mock).mockResolvedValue(true);
106+
(asyncStorageService.saveScreenProtectionEnabled as jest.Mock).mockResolvedValue(undefined);
107+
108+
const { result } = renderHook(() => useScreenProtection());
109+
110+
await waitFor(() => {
111+
expect(result.current.isInitialized).toBe(true);
112+
});
113+
114+
await act(async () => {
115+
await result.current.setScreenProtection(false);
116+
});
117+
118+
expect(result.current.isEnabled).toBe(false);
119+
expect(CaptureProtection.allow).toHaveBeenCalled();
120+
expect(asyncStorageService.saveScreenProtectionEnabled).toHaveBeenCalledWith(false);
121+
});
122+
123+
it('should revert to enabled on failure', async () => {
124+
const error = new Error('CaptureProtection error');
125+
(asyncStorageService.getScreenProtectionEnabled as jest.Mock).mockResolvedValue(false);
126+
(CaptureProtection.allow as jest.Mock).mockRejectedValue(error);
127+
128+
const { result } = renderHook(() => useScreenProtection());
129+
130+
await waitFor(() => {
131+
expect(result.current.isInitialized).toBe(true);
132+
});
133+
134+
await act(async () => {
135+
await result.current.setScreenProtection(false);
136+
});
137+
138+
expect(result.current.isEnabled).toBe(true);
139+
expect(CaptureProtection.prevent).toHaveBeenCalled();
140+
expect(asyncStorageService.saveScreenProtectionEnabled).toHaveBeenCalledWith(true);
141+
expect(logger.error).toHaveBeenCalledWith('Error setting screen protection:', error);
142+
});
143+
144+
it('should save preference after successfully changing protection state', async () => {
145+
(asyncStorageService.getScreenProtectionEnabled as jest.Mock).mockResolvedValue(false);
146+
(asyncStorageService.saveScreenProtectionEnabled as jest.Mock).mockResolvedValue(undefined);
147+
(CaptureProtection.prevent as jest.Mock).mockResolvedValue(undefined);
148+
(CaptureProtection.allow as jest.Mock).mockResolvedValue(undefined);
149+
150+
const { result } = renderHook(() => useScreenProtection());
151+
152+
await waitFor(() => {
153+
expect(result.current.isInitialized).toBe(true);
154+
});
155+
156+
await act(async () => {
157+
await result.current.setScreenProtection(true);
158+
});
159+
160+
expect(asyncStorageService.saveScreenProtectionEnabled).toHaveBeenCalledWith(true);
161+
expect(asyncStorageService.saveScreenProtectionEnabled).toHaveBeenCalledTimes(1);
162+
163+
await act(async () => {
164+
await result.current.setScreenProtection(false);
165+
});
166+
167+
expect(asyncStorageService.saveScreenProtectionEnabled).toHaveBeenCalledWith(false);
168+
expect(asyncStorageService.saveScreenProtectionEnabled).toHaveBeenCalledTimes(2);
169+
});
170+
});
171+
172+
describe('security defaults', () => {
173+
it('should default to enabled for security if no preference is saved', async () => {
174+
(asyncStorageService.getScreenProtectionEnabled as jest.Mock).mockResolvedValue(true);
175+
176+
const { result } = renderHook(() => useScreenProtection());
177+
178+
await waitFor(() => {
179+
expect(result.current.isInitialized).toBe(true);
180+
});
181+
182+
expect(result.current.isEnabled).toBe(true);
183+
expect(CaptureProtection.prevent).toHaveBeenCalled();
184+
});
185+
186+
it('should enable protection on error for security', async () => {
187+
(asyncStorageService.getScreenProtectionEnabled as jest.Mock).mockRejectedValue(new Error('Storage unavailable'));
188+
189+
const { result } = renderHook(() => useScreenProtection());
190+
191+
await waitFor(() => {
192+
expect(result.current.isInitialized).toBe(true);
193+
});
194+
195+
expect(result.current.isEnabled).toBe(true);
196+
expect(CaptureProtection.prevent).toHaveBeenCalled();
197+
});
198+
});
199+
});

src/hooks/useScreenProtection.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useEffect, useState } from 'react';
2+
import { CaptureProtection } from 'react-native-capture-protection';
3+
import asyncStorageService from '../services/AsyncStorageService';
4+
import { logger } from '../services/common';
5+
6+
/**
7+
* Hook to manage screen protection (prevents screenshots and screen recording)
8+
* Handles initialization, state management, and persistence of user preference
9+
*/
10+
export const useScreenProtection = () => {
11+
const [isEnabled, setIsEnabled] = useState(true);
12+
const [isInitialized, setIsInitialized] = useState(false);
13+
14+
/**
15+
* Initializes screen protection based on saved user preference
16+
* Defaults to enabled (true) for security if no preference is saved
17+
*/
18+
useEffect(() => {
19+
const initialize = async () => {
20+
try {
21+
const savedPreference = await asyncStorageService.getScreenProtectionEnabled();
22+
setIsEnabled(savedPreference);
23+
24+
if (savedPreference) {
25+
await CaptureProtection.prevent();
26+
} else {
27+
await CaptureProtection.allow();
28+
}
29+
30+
setIsInitialized(true);
31+
} catch (error) {
32+
logger.error('Error initializing screen protection:', error);
33+
setIsEnabled(true);
34+
await CaptureProtection.prevent();
35+
setIsInitialized(true);
36+
}
37+
};
38+
39+
initialize();
40+
}, []);
41+
42+
/**
43+
* Enables or disables screen protection
44+
* @param enabled - true to prevent screenshots/recording, false to allow
45+
*/
46+
const setScreenProtection = async (enabled: boolean): Promise<void> => {
47+
try {
48+
setIsEnabled(enabled);
49+
50+
if (enabled) {
51+
await CaptureProtection.prevent();
52+
} else {
53+
await CaptureProtection.allow();
54+
}
55+
56+
await asyncStorageService.saveScreenProtectionEnabled(enabled);
57+
} catch (error) {
58+
logger.error('Error setting screen protection:', error);
59+
setIsEnabled(true);
60+
await CaptureProtection.prevent();
61+
await asyncStorageService.saveScreenProtectionEnabled(true);
62+
}
63+
};
64+
65+
return {
66+
isEnabled,
67+
isInitialized,
68+
setScreenProtection,
69+
};
70+
};

src/screens/SettingsScreen/index.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import AppVersionWidget from '../../components/AppVersionWidget';
2424
import SettingsGroup from '../../components/SettingsGroup';
2525
import UserProfilePicture from '../../components/UserProfilePicture';
2626
import useGetColor from '../../hooks/useColor';
27+
import { useScreenProtection } from '../../hooks/useScreenProtection';
2728
import appService from '../../services/AppService';
2829
import { useAppDispatch, useAppSelector } from '../../store/hooks';
2930
import { authSelectors } from '../../store/slices/auth';
@@ -36,7 +37,6 @@ import { fs } from '@internxt-mobile/services/FileSystemService';
3637
import { notifications } from '@internxt-mobile/services/NotificationsService';
3738
import { internxtMobileSDKUtils } from '@internxt/mobile-sdk';
3839

39-
import { CaptureProtection, useCaptureProtection } from 'react-native-capture-protection';
4040
import { paymentsSelectors } from 'src/store/slices/payments';
4141
import asyncStorageService from '../../services/AsyncStorageService';
4242

@@ -45,11 +45,10 @@ function SettingsScreen({ navigation }: SettingsScreenProps<'SettingsHome'>): JS
4545
const tailwind = useTailwind();
4646
const getColor = useGetColor();
4747
const dispatch = useAppDispatch();
48-
const { protectionStatus } = useCaptureProtection();
4948
const scrollViewRef = useRef<ScrollView | null>(null);
5049

5150
const [isDarkMode, setIsDarkMode] = useState(false);
52-
const [screenProtectionEnabled, setScreenProtectionEnabled] = useState(protectionStatus?.screenshot);
51+
const { isEnabled: isScreenProtectionEnabled, setScreenProtection } = useScreenProtection();
5352
const showBilling = useAppSelector(paymentsSelectors.shouldShowBilling);
5453
const { user } = useAppSelector((state) => state.auth);
5554
const usagePercent = useAppSelector(storageSelectors.usagePercent);
@@ -134,18 +133,7 @@ function SettingsScreen({ navigation }: SettingsScreenProps<'SettingsHome'>): JS
134133
};
135134

136135
const handleScreenProtection = async (value: boolean) => {
137-
try {
138-
setScreenProtectionEnabled(value);
139-
140-
if (value) {
141-
await CaptureProtection.prevent();
142-
} else {
143-
await CaptureProtection.allow();
144-
}
145-
} catch (error) {
146-
setScreenProtectionEnabled(true);
147-
await CaptureProtection.prevent();
148-
}
136+
await setScreenProtection(value);
149137
};
150138

151139
const onAccountPressed = () => {
@@ -385,7 +373,7 @@ function SettingsScreen({ navigation }: SettingsScreenProps<'SettingsHome'>): JS
385373
thumbColor={isDarkMode ? getColor('text-white') : getColor('text-gray-40')}
386374
ios_backgroundColor={getColor('text-gray-20')}
387375
onValueChange={handleScreenProtection}
388-
value={screenProtectionEnabled}
376+
value={isScreenProtectionEnabled}
389377
/>
390378
</View>
391379
</View>

src/screens/drive/DrivePreviewScreen/hooks/useThumbnailRegeneration.test.ts renamed to src/screens/drive/DrivePreviewScreen/hooks/useThumbnailRegeneration.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
jest.mock('@internxt-mobile/services/drive/file', () => ({
2+
driveFileService: {
3+
regenerateThumbnail: jest.fn(),
4+
},
5+
}));
6+
7+
jest.mock('@internxt-mobile/services/common', () => ({
8+
logger: {
9+
info: jest.fn(),
10+
},
11+
}));
12+
13+
jest.mock('@internxt-mobile/services/ErrorService', () => ({
14+
reportError: jest.fn(),
15+
}));
16+
117
import { canGenerateThumbnail, shouldRegenerateThumbnail } from './useThumbnailRegeneration';
218

319
describe('useThumbnailRegeneration', () => {

0 commit comments

Comments
 (0)