From e5f60812c0407fc6a4a3fc0299173be84742481f Mon Sep 17 00:00:00 2001 From: Jeel Dobariya Date: Mon, 19 Oct 2026 14:13:10 +0530 Subject: [PATCH 1/4] refactor: screenshot prevention libs Signed-off-by: Jeel Dobariya --- .../(tabs)/(password-manager)/password-details.tsx | 4 ++-- src/app/(tabs)/(password-manager)/save-password.tsx | 4 ++-- src/app/(tabs)/(settings)/settings.tsx | 8 ++++---- src/features/screenshot-prevention/index.ts | 12 ++++++++++++ .../screenshot-prevention}/screenshot_prevention.ts | 11 ----------- 5 files changed, 20 insertions(+), 19 deletions(-) create mode 100644 src/features/screenshot-prevention/index.ts rename src/{libs => features/screenshot-prevention}/screenshot_prevention.ts (72%) diff --git a/src/app/(tabs)/(password-manager)/password-details.tsx b/src/app/(tabs)/(password-manager)/password-details.tsx index e98836a5..569b0180 100644 --- a/src/app/(tabs)/(password-manager)/password-details.tsx +++ b/src/app/(tabs)/(password-manager)/password-details.tsx @@ -4,8 +4,8 @@ import PasswordFormCard, { } from "@/components/PasswordFormCard"; import { usePasswordRepository } from "@/contexts/RepositoryContext"; import { useToast } from "@/contexts/ToastContext"; +import { useSecretScreenController } from "@/features/screenshot-prevention"; -import { getScreenShotSecureScreen } from "@/libs/screenshot_prevention"; import { formatDate } from "@passcodes/passalgo"; import FontAwesome6 from "@react-native-vector-icons/fontawesome6"; @@ -28,7 +28,7 @@ export default function PasswordDetailsScreen() { const { showToast } = useToast(); - getScreenShotSecureScreen(); + useSecretScreenController("password-details"); /** * Remove focus from whichever TextInput is currently focused diff --git a/src/app/(tabs)/(password-manager)/save-password.tsx b/src/app/(tabs)/(password-manager)/save-password.tsx index 0e32456b..0be68768 100644 --- a/src/app/(tabs)/(password-manager)/save-password.tsx +++ b/src/app/(tabs)/(password-manager)/save-password.tsx @@ -3,7 +3,7 @@ import FormTextField from "@/components/FormTextField"; import SecureTextField from "@/components/SecureTextField"; import { usePasswordRepository } from "@/contexts/RepositoryContext"; import { useToast } from "@/contexts/ToastContext"; -import { getScreenShotSecureScreen } from "@/libs/screenshot_prevention"; +import { useSecretScreenController } from "@/features/screenshot-prevention"; import FontAwesome6 from "@react-native-vector-icons/fontawesome6"; import { router, useLocalSearchParams } from "expo-router"; import { useState } from "react"; @@ -59,7 +59,7 @@ export default function SavePasswordScreen() { const { showToast } = useToast(); - getScreenShotSecureScreen(); + useSecretScreenController("save-password"); return ( diff --git a/src/app/(tabs)/(settings)/settings.tsx b/src/app/(tabs)/(settings)/settings.tsx index 37080b7e..76603248 100644 --- a/src/app/(tabs)/(settings)/settings.tsx +++ b/src/app/(tabs)/(settings)/settings.tsx @@ -2,6 +2,10 @@ import LinkButton from "@/components/LinkButton"; import { LinkIconButton } from "@/components/LinkIconButton"; import Text from "@/components/Text"; import Config from "@/config"; +import { + isScreenshotPreventionEnabled, + toggleScreenshotPreventionFeature, +} from "@/features/screenshot-prevention/screenshot_prevention"; import { isBiometricsAuthEnabled, toggleBiometricsFeature, @@ -11,10 +15,6 @@ import { useHomeLayout, type HomeLayout, } from "@/libs/home_layout"; -import { - isScreenshotPreventionEnabled, - toggleScreenshotPreventionFeature, -} from "@/libs/screenshot_prevention"; import { FontAwesome6 } from "@react-native-vector-icons/fontawesome6"; import { LinearGradient } from "expo-linear-gradient"; import { router } from "expo-router"; diff --git a/src/features/screenshot-prevention/index.ts b/src/features/screenshot-prevention/index.ts new file mode 100644 index 00000000..ca073c6b --- /dev/null +++ b/src/features/screenshot-prevention/index.ts @@ -0,0 +1,12 @@ +import * as ScreenCapture from "expo-screen-capture"; +import { useEffect } from "react"; + +export function useSecretScreenController(key: string) { + useEffect(() => { + ScreenCapture.preventScreenCaptureAsync(key); + + return () => { + ScreenCapture.allowScreenCaptureAsync(key); + }; + }, []); +} diff --git a/src/libs/screenshot_prevention.ts b/src/features/screenshot-prevention/screenshot_prevention.ts similarity index 72% rename from src/libs/screenshot_prevention.ts rename to src/features/screenshot-prevention/screenshot_prevention.ts index b5159953..959872f0 100644 --- a/src/libs/screenshot_prevention.ts +++ b/src/features/screenshot-prevention/screenshot_prevention.ts @@ -1,22 +1,11 @@ import AsyncStorage from "expo-sqlite/kv-store"; -import * as ScreenCapture from "expo-screen-capture"; const IS_SCREENSHOT_PREVENTION_ENABLED = "feat_screenshot_prevention_isEnabled"; enum SCREENSHOT_PREVENTION_KV { ENABLED = "true", DISABLED = "false", } -export function getScreenShotSecureScreen() { - if (isScreenshotPreventionEnabled()) { - ScreenCapture.preventScreenCaptureAsync(); - return true; - } else { - ScreenCapture.allowScreenCaptureAsync(); - return false; - } -} - export function toggleScreenshotPreventionFeature() { let isEnabled = isScreenshotPreventionEnabled(); From c9fcfcb64ac298d1af724037d31d548e58a55e20 Mon Sep 17 00:00:00 2001 From: Jeel Dobariya Date: Mon, 19 Oct 2026 14:16:53 +0530 Subject: [PATCH 2/4] feat: improve screen capture. Signed-off-by: Jeel Dobariya --- src/app/(tabs)/(password-manager)/password-details.tsx | 4 ++-- src/app/(tabs)/(password-manager)/save-password.tsx | 4 ++-- src/features/screenshot-prevention/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/(tabs)/(password-manager)/password-details.tsx b/src/app/(tabs)/(password-manager)/password-details.tsx index 569b0180..75b705e6 100644 --- a/src/app/(tabs)/(password-manager)/password-details.tsx +++ b/src/app/(tabs)/(password-manager)/password-details.tsx @@ -4,7 +4,7 @@ import PasswordFormCard, { } from "@/components/PasswordFormCard"; import { usePasswordRepository } from "@/contexts/RepositoryContext"; import { useToast } from "@/contexts/ToastContext"; -import { useSecretScreenController } from "@/features/screenshot-prevention"; +import { useSecretScreen } from "@/features/screenshot-prevention"; import { formatDate } from "@passcodes/passalgo"; @@ -28,7 +28,7 @@ export default function PasswordDetailsScreen() { const { showToast } = useToast(); - useSecretScreenController("password-details"); + useSecretScreen("password-details"); /** * Remove focus from whichever TextInput is currently focused diff --git a/src/app/(tabs)/(password-manager)/save-password.tsx b/src/app/(tabs)/(password-manager)/save-password.tsx index 0be68768..6aa0ef84 100644 --- a/src/app/(tabs)/(password-manager)/save-password.tsx +++ b/src/app/(tabs)/(password-manager)/save-password.tsx @@ -3,7 +3,7 @@ import FormTextField from "@/components/FormTextField"; import SecureTextField from "@/components/SecureTextField"; import { usePasswordRepository } from "@/contexts/RepositoryContext"; import { useToast } from "@/contexts/ToastContext"; -import { useSecretScreenController } from "@/features/screenshot-prevention"; +import { useSecretScreen } from "@/features/screenshot-prevention"; import FontAwesome6 from "@react-native-vector-icons/fontawesome6"; import { router, useLocalSearchParams } from "expo-router"; import { useState } from "react"; @@ -59,7 +59,7 @@ export default function SavePasswordScreen() { const { showToast } = useToast(); - useSecretScreenController("save-password"); + useSecretScreen("save-password"); return ( diff --git a/src/features/screenshot-prevention/index.ts b/src/features/screenshot-prevention/index.ts index ca073c6b..38a227f2 100644 --- a/src/features/screenshot-prevention/index.ts +++ b/src/features/screenshot-prevention/index.ts @@ -1,7 +1,7 @@ import * as ScreenCapture from "expo-screen-capture"; import { useEffect } from "react"; -export function useSecretScreenController(key: string) { +export function useSecretScreen(key: string) { useEffect(() => { ScreenCapture.preventScreenCaptureAsync(key); From 4857dfe0a3efe3694f20fd97aee6d5c110b554fb Mon Sep 17 00:00:00 2001 From: Jeel Dobariya Date: Mon, 19 Oct 2026 15:23:52 +0530 Subject: [PATCH 3/4] feat: smart screenshot prevention Signed-off-by: Jeel Dobariya --- .../(password-manager)/password-details.tsx | 3 -- .../(password-manager)/save-password.tsx | 4 +-- src/components/PasswordFormCard.tsx | 1 + src/components/SecureTextField.tsx | 29 ++++++++++++++++--- src/features/screenshot-prevention/index.ts | 13 +-------- src/features/screenshot-prevention/runtime.ts | 9 ++++++ .../useSecretScreenController.ts | 16 ++++++++++ 7 files changed, 53 insertions(+), 22 deletions(-) create mode 100644 src/features/screenshot-prevention/runtime.ts create mode 100644 src/features/screenshot-prevention/useSecretScreenController.ts diff --git a/src/app/(tabs)/(password-manager)/password-details.tsx b/src/app/(tabs)/(password-manager)/password-details.tsx index 75b705e6..3e00c057 100644 --- a/src/app/(tabs)/(password-manager)/password-details.tsx +++ b/src/app/(tabs)/(password-manager)/password-details.tsx @@ -4,7 +4,6 @@ import PasswordFormCard, { } from "@/components/PasswordFormCard"; import { usePasswordRepository } from "@/contexts/RepositoryContext"; import { useToast } from "@/contexts/ToastContext"; -import { useSecretScreen } from "@/features/screenshot-prevention"; import { formatDate } from "@passcodes/passalgo"; @@ -28,8 +27,6 @@ export default function PasswordDetailsScreen() { const { showToast } = useToast(); - useSecretScreen("password-details"); - /** * Remove focus from whichever TextInput is currently focused * and dismiss the keyboard. diff --git a/src/app/(tabs)/(password-manager)/save-password.tsx b/src/app/(tabs)/(password-manager)/save-password.tsx index 6aa0ef84..be98496e 100644 --- a/src/app/(tabs)/(password-manager)/save-password.tsx +++ b/src/app/(tabs)/(password-manager)/save-password.tsx @@ -3,7 +3,6 @@ import FormTextField from "@/components/FormTextField"; import SecureTextField from "@/components/SecureTextField"; import { usePasswordRepository } from "@/contexts/RepositoryContext"; import { useToast } from "@/contexts/ToastContext"; -import { useSecretScreen } from "@/features/screenshot-prevention"; import FontAwesome6 from "@react-native-vector-icons/fontawesome6"; import { router, useLocalSearchParams } from "expo-router"; import { useState } from "react"; @@ -59,8 +58,6 @@ export default function SavePasswordScreen() { const { showToast } = useToast(); - useSecretScreen("save-password"); - return ( {lockCategory !== "1" && ( diff --git a/src/components/PasswordFormCard.tsx b/src/components/PasswordFormCard.tsx index 124e1722..be2fe7f6 100644 --- a/src/components/PasswordFormCard.tsx +++ b/src/components/PasswordFormCard.tsx @@ -56,6 +56,7 @@ export default function PasswordFormCard({ value={value.password} onChangeText={(text) => onChange("password", text)} editable={editable} + screenCaptureKey="password-form" /> ; +type StrippedTextFieldProps = Omit; +type SecureTextFieldProps = StrippedTextFieldProps & { + /* generally, you can set to name of screen route, we need it for screen shot prevention */ + screenCaptureKey: string; +}; -// isPasswordVisible ? "eye-off" : "eye" -export default function SecureTextField({ ...props }: SecureTextFieldProps) { +export default function SecureTextField({ + screenCaptureKey, + ...props +}: SecureTextFieldProps) { + let { preventScreenShots, allowScreenShots } = + useSecretScreenController(screenCaptureKey); const [isPasswordVisible, setIsPasswordVisible] = useState(false); return ( @@ -17,7 +26,19 @@ export default function SecureTextField({ ...props }: SecureTextFieldProps) { secureTextEntry={!isPasswordVisible} right={ setIsPasswordVisible(!isPasswordVisible)} + onPress={() => { + setIsPasswordVisible((prev) => { + // Note: this checks are hard to understand, + // because we using previous state, read it twice!! + if (!prev) { + preventScreenShots(); + } else { + allowScreenShots(); + } + + return !prev; + }); + }} icon={({ size, color }) => ( { - ScreenCapture.preventScreenCaptureAsync(key); - - return () => { - ScreenCapture.allowScreenCaptureAsync(key); - }; - }, []); -} +export { default as useSecretScreenController } from "./useSecretScreenController"; diff --git a/src/features/screenshot-prevention/runtime.ts b/src/features/screenshot-prevention/runtime.ts new file mode 100644 index 00000000..ff11f860 --- /dev/null +++ b/src/features/screenshot-prevention/runtime.ts @@ -0,0 +1,9 @@ +import * as ScreenCapture from "expo-screen-capture"; + +export async function preventScreenShots(key?: string) { + await ScreenCapture.preventScreenCaptureAsync(key); +} + +export async function allowScreenShots(key?: string) { + await ScreenCapture.allowScreenCaptureAsync(key); +} diff --git a/src/features/screenshot-prevention/useSecretScreenController.ts b/src/features/screenshot-prevention/useSecretScreenController.ts new file mode 100644 index 00000000..d2036804 --- /dev/null +++ b/src/features/screenshot-prevention/useSecretScreenController.ts @@ -0,0 +1,16 @@ +import { useEffect } from "react"; +import { allowScreenShots, preventScreenShots } from "./runtime"; + +export default function useSecretScreenController(key: string) { + // This for clean up, whenever screen goes it reenables screen shots. + useEffect(() => { + return () => { + allowScreenShots(key); + }; + }, []); + + return { + preventScreenShots: () => preventScreenShots(key), + allowScreenShots: () => allowScreenShots(key), + }; +} From 2855e04b0d43a490a8f2c278432a9503a206c7f7 Mon Sep 17 00:00:00 2001 From: Jeel Dobariya Date: Mon, 19 Oct 2026 15:34:06 +0530 Subject: [PATCH 4/4] refactor: store Signed-off-by: Jeel Dobariya --- src/app/(tabs)/(settings)/settings.tsx | 2 +- src/features/screenshot-prevention/index.ts | 4 ++++ src/features/screenshot-prevention/runtime.ts | 7 +++++-- .../{screenshot_prevention.ts => store.ts} | 0 4 files changed, 10 insertions(+), 3 deletions(-) rename src/features/screenshot-prevention/{screenshot_prevention.ts => store.ts} (100%) diff --git a/src/app/(tabs)/(settings)/settings.tsx b/src/app/(tabs)/(settings)/settings.tsx index 76603248..fcd9aef5 100644 --- a/src/app/(tabs)/(settings)/settings.tsx +++ b/src/app/(tabs)/(settings)/settings.tsx @@ -5,7 +5,7 @@ import Config from "@/config"; import { isScreenshotPreventionEnabled, toggleScreenshotPreventionFeature, -} from "@/features/screenshot-prevention/screenshot_prevention"; +} from "@/features/screenshot-prevention"; import { isBiometricsAuthEnabled, toggleBiometricsFeature, diff --git a/src/features/screenshot-prevention/index.ts b/src/features/screenshot-prevention/index.ts index c3a30f8b..6f260c2b 100644 --- a/src/features/screenshot-prevention/index.ts +++ b/src/features/screenshot-prevention/index.ts @@ -1 +1,5 @@ +export { + isScreenshotPreventionEnabled, + toggleScreenshotPreventionFeature, +} from "./store"; export { default as useSecretScreenController } from "./useSecretScreenController"; diff --git a/src/features/screenshot-prevention/runtime.ts b/src/features/screenshot-prevention/runtime.ts index ff11f860..977d73b9 100644 --- a/src/features/screenshot-prevention/runtime.ts +++ b/src/features/screenshot-prevention/runtime.ts @@ -1,9 +1,12 @@ import * as ScreenCapture from "expo-screen-capture"; +import { isScreenshotPreventionEnabled } from "./store"; export async function preventScreenShots(key?: string) { - await ScreenCapture.preventScreenCaptureAsync(key); + isScreenshotPreventionEnabled() && + (await ScreenCapture.preventScreenCaptureAsync(key)); } export async function allowScreenShots(key?: string) { - await ScreenCapture.allowScreenCaptureAsync(key); + isScreenshotPreventionEnabled() && + (await ScreenCapture.allowScreenCaptureAsync(key)); } diff --git a/src/features/screenshot-prevention/screenshot_prevention.ts b/src/features/screenshot-prevention/store.ts similarity index 100% rename from src/features/screenshot-prevention/screenshot_prevention.ts rename to src/features/screenshot-prevention/store.ts