diff --git a/android/settings.gradle b/android/settings.gradle index a3abccc4320..e3587781d25 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -5,4 +5,4 @@ rootProject.name = 'ItaliaApp' include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') include ':react-native-cie' -project(':react-native-cie').projectDir = new File(rootProject.projectDir, '../node_modules/@pagopa/react-native-cie/android') +project(':react-native-cie').projectDir = new File(rootProject.projectDir, '../node_modules/@pagopa/react-native-cie/android') \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 06dde433409..8fdd59abd66 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2003,6 +2003,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNScreenshotPrevent (1.2.1): + - React - RNSentry (6.4.0): - DoubleConversion - glog @@ -2180,6 +2182,7 @@ DEPENDENCIES: - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) + - RNScreenshotPrevent (from `../node_modules/react-native-screenshot-prevent`) - "RNSentry (from `../node_modules/@sentry/react-native`)" - RNShare (from `../node_modules/react-native-share`) - RNStoreReview (from `../node_modules/react-native-store-review`) @@ -2429,6 +2432,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-reanimated" RNScreens: :path: "../node_modules/react-native-screens" + RNScreenshotPrevent: + :path: "../node_modules/react-native-screenshot-prevent" RNSentry: :path: "../node_modules/@sentry/react-native" RNShare: @@ -2556,6 +2561,7 @@ SPEC CHECKSUMS: RNReactNativeHapticFeedback: 00ba111b82aa266bb3ee1aa576831c2ea9a9dfad RNReanimated: 26a5a401a5de1c0cf1a3226873825b00ffa85377 RNScreens: 35bb8e81aeccf111baa0ea01a54231390dbbcfd9 + RNScreenshotPrevent: 5f8473abaa2db2476561ef8f3704b66491ce7b01 RNSentry: c5075bc124ebc8afa84e037c7fe257053a0b2cda RNShare: 694e19d7f74ac4c04de3a8af0649e9ccc03bd8b1 RNStoreReview: 613c43e9132998ed41a65946e20c223c91b36464 diff --git a/package.json b/package.json index 4f2bd6e7a6a..919b2bd20c4 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,6 @@ "react-native-easing-gradient": "^1.1.1", "react-native-exception-handler": "^2.10.8", "react-native-fingerprint-scanner": "git+https://github.com/hieuvp/react-native-fingerprint-scanner.git#9cecc0db326471c571553ea85f7c016fee2f803d", - "react-native-flag-secure-android": "^1.0.3", "react-native-fs": "^2.18.0", "react-native-gesture-handler": "^2.18.1", "react-native-haptic-feedback": "^2.3.3", @@ -142,6 +141,7 @@ "react-native-safe-area-context": "^4.10.5", "react-native-screen-brightness": "^2.0.0-alpha", "react-native-screens": "^3.35.0", + "react-native-screenshot-prevent": "^1.2.1", "react-native-share": "^10.2.1", "react-native-splash-screen": "^3.2.0", "react-native-store-review": "^0.4.3", diff --git a/patches/patches.md b/patches/patches.md index d0e1a2d24f2..da920924325 100644 --- a/patches/patches.md +++ b/patches/patches.md @@ -132,6 +132,8 @@ Created on **24/03/2022** Created on **29/08/2022** +Removed on **10/01/2025** and replaced with `react-native-screenshot-prevent` + #### Reason: - This patch is going to fix a gradle issue that breaks the compile on android platform, due to gradle imcompatibility diff --git a/patches/react-native-flag-secure-android+1.0.3.patch b/patches/react-native-flag-secure-android+1.0.3.patch deleted file mode 100644 index 82b2bba9470..00000000000 --- a/patches/react-native-flag-secure-android+1.0.3.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/node_modules/react-native-flag-secure-android/.DS_Store b/node_modules/react-native-flag-secure-android/.DS_Store -new file mode 100644 -index 0000000..8186bc2 -Binary files /dev/null and b/node_modules/react-native-flag-secure-android/.DS_Store differ -diff --git a/node_modules/react-native-flag-secure-android/android/build.gradle b/node_modules/react-native-flag-secure-android/android/build.gradle -index 88fa0af..242e219 100644 ---- a/node_modules/react-native-flag-secure-android/android/build.gradle -+++ b/node_modules/react-native-flag-secure-android/android/build.gradle -@@ -36,5 +36,5 @@ android { - } - - dependencies { -- compile 'com.facebook.react:react-native:+' -+ implementation 'com.facebook.react:react-native:+' - } diff --git a/ts/@types/react-native-flag-secure-android.d.ts b/ts/@types/react-native-flag-secure-android.d.ts deleted file mode 100644 index 12277cc4d75..00000000000 --- a/ts/@types/react-native-flag-secure-android.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "react-native-flag-secure-android" { - function activate(): void; - function deactivate(): void; -} diff --git a/ts/RootContainer.tsx b/ts/RootContainer.tsx index 52e9434ce10..dc729a15721 100644 --- a/ts/RootContainer.tsx +++ b/ts/RootContainer.tsx @@ -5,14 +5,12 @@ import { AppState, AppStateStatus, NativeEventSubscription, - Platform, StatusBar } from "react-native"; import SplashScreen from "react-native-splash-screen"; import { connect } from "react-redux"; import configurePushNotifications from "./features/pushNotifications/utils/configurePushNotification"; import DebugInfoOverlay from "./components/DebugInfoOverlay"; -import FlagSecureComponent from "./components/FlagSecure"; import PagoPATestIndicatorOverlay from "./components/PagoPATestIndicatorOverlay"; import { LightModalRoot } from "./components/ui/LightModal"; import { setLocale } from "./i18n"; @@ -101,7 +99,6 @@ class RootContainer extends React.PureComponent { barStyle={"dark-content"} backgroundColor={customVariables.androidStatusBarColor} /> - {Platform.OS === "android" && } ; - -/** - * - * On Android, it enables or disables FLAG_SECURE based on the isAllowedSnapshotCurrentScreen prop. - * - * @param props - * @constructor - */ -const FlagSecureComponent: React.FunctionComponent = props => { - useEffect(() => { - if (props.isAllowedSnapshotCurrentScreen) { - FlagSecure.deactivate(); - } else { - FlagSecure.activate(); - } - }, [props.isAllowedSnapshotCurrentScreen]); - return null; -}; - -const mapStateToProps = (state: GlobalState) => ({ - isAllowedSnapshotCurrentScreen: isAllowedSnapshotCurrentScreen(state) -}); - -export default connect(mapStateToProps)(FlagSecureComponent); diff --git a/ts/features/itwallet/identification/screens/cie/ItwCiePinScreen.tsx b/ts/features/itwallet/identification/screens/cie/ItwCiePinScreen.tsx index 65c1a52d759..dc47816d14c 100644 --- a/ts/features/itwallet/identification/screens/cie/ItwCiePinScreen.tsx +++ b/ts/features/itwallet/identification/screens/cie/ItwCiePinScreen.tsx @@ -27,6 +27,7 @@ import I18n from "../../../../../i18n"; import { useIOSelector } from "../../../../../store/hooks"; import { setAccessibilityFocus } from "../../../../../utils/accessibility"; import { useIOBottomSheetAutoresizableModal } from "../../../../../utils/hooks/bottomSheet"; +import { usePreventScreenCapture } from "../../../../../utils/hooks/usePreventScreenCapture"; import { withTrailingPoliceCarLightEmojii } from "../../../../../utils/strings"; import { openWebUrl } from "../../../../../utils/url"; import { @@ -61,6 +62,8 @@ const ForgottenPin = () => ( ); export const ItwCiePinScreen = () => { + usePreventScreenCapture(); + const useCieUat = useIOSelector(isCieLoginUatEnabledSelector); const machineRef = ItwEidIssuanceMachineContext.useActorRef(); diff --git a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialPreviewScreen.tsx b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialPreviewScreen.tsx index 5dfd6106410..9c8a11ef419 100644 --- a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialPreviewScreen.tsx +++ b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialPreviewScreen.tsx @@ -33,6 +33,7 @@ import { selectCredentialOption, selectCredentialTypeOption } from "../../machine/credential/selectors"; +import { usePreventScreenCapture } from "../../../../utils/hooks/usePreventScreenCapture"; import { ItwCredentialIssuanceMachineContext } from "../../machine/provider"; import { ItwCredentialPreviewClaimsList } from "../components/ItwCredentialPreviewClaimsList"; import { ITW_ROUTES } from "../../navigation/routes"; @@ -45,6 +46,7 @@ export const ItwIssuanceCredentialPreviewScreen = () => { selectCredentialOption ); + usePreventScreenCapture(); useItwDisableGestureNavigation(); useAvoidHardwareBackButton(); diff --git a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialTrustIssuerScreen.tsx b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialTrustIssuerScreen.tsx index df7211c1835..095c8a46a14 100644 --- a/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialTrustIssuerScreen.tsx +++ b/ts/features/itwallet/issuance/screens/ItwIssuanceCredentialTrustIssuerScreen.tsx @@ -53,6 +53,7 @@ import { trackWalletDataShareAccepted } from "../../analytics"; import LoadingScreenContent from "../../../../components/screens/LoadingScreenContent"; +import { usePreventScreenCapture } from "../../../../utils/hooks/usePreventScreenCapture"; import { itwIpzsPrivacyUrl } from "../../../../config"; import { ITW_ROUTES } from "../../navigation/routes"; @@ -68,6 +69,7 @@ const ItwIssuanceCredentialTrustIssuerScreen = () => { selectCredentialTypeOption ); + usePreventScreenCapture(); useItwDisableGestureNavigation(); useAvoidHardwareBackButton(); diff --git a/ts/features/itwallet/issuance/screens/__tests__/ItwIssuanceCredentialAuthScreen.test.tsx b/ts/features/itwallet/issuance/screens/__tests__/ItwIssuanceCredentialAuthScreen.test.tsx index f8a7a286008..58940df4ecc 100644 --- a/ts/features/itwallet/issuance/screens/__tests__/ItwIssuanceCredentialAuthScreen.test.tsx +++ b/ts/features/itwallet/issuance/screens/__tests__/ItwIssuanceCredentialAuthScreen.test.tsx @@ -9,6 +9,10 @@ import { ItwCredentialIssuanceMachineContext } from "../../../machine/provider"; import { ITW_ROUTES } from "../../../navigation/routes"; import { ItwIssuanceCredentialTrustIssuerScreen } from "../ItwIssuanceCredentialTrustIssuerScreen"; +jest.mock("../../../../../utils/hooks/usePreventScreenCapture", () => ({ + usePreventScreenCapture: jest.fn() +})); + describe("ItwIssuanceCredentialTrustIssuerScreen", () => { it("it should render the screen correctly", () => { const component = renderComponent(); diff --git a/ts/features/itwallet/presentation/screens/ItwPresentationCredentialAttachmentScreen.tsx b/ts/features/itwallet/presentation/screens/ItwPresentationCredentialAttachmentScreen.tsx index a04300f0260..91d5e24c2db 100644 --- a/ts/features/itwallet/presentation/screens/ItwPresentationCredentialAttachmentScreen.tsx +++ b/ts/features/itwallet/presentation/screens/ItwPresentationCredentialAttachmentScreen.tsx @@ -19,6 +19,7 @@ import { import { ParsedCredential } from "../../common/utils/itwTypesUtils"; import { ItwParamsList } from "../../navigation/ItwParamsList"; import { trackWalletCredentialFAC_SIMILE } from "../../analytics"; +import { usePreventScreenCapture } from "../../../../utils/hooks/usePreventScreenCapture"; // We currently only support PDF files, extend this if needed type SupportedAttachmentType = "application/pdf"; @@ -49,6 +50,7 @@ export const ItwPresentationCredentialAttachmentScreen = ({ safeBottomAreaHeight: 0 }); + usePreventScreenCapture(); useFocusEffect(trackWalletCredentialFAC_SIMILE); useHeaderSecondLevel({ diff --git a/ts/features/itwallet/presentation/screens/ItwPresentationCredentialCardModal.tsx b/ts/features/itwallet/presentation/screens/ItwPresentationCredentialCardModal.tsx index a9669ea5ad8..6683bbfe8a7 100644 --- a/ts/features/itwallet/presentation/screens/ItwPresentationCredentialCardModal.tsx +++ b/ts/features/itwallet/presentation/screens/ItwPresentationCredentialCardModal.tsx @@ -21,6 +21,7 @@ import { } from "../../common/utils/itwTypesUtils"; import { ItwParamsList } from "../../navigation/ItwParamsList"; import { ItwPresentationCredentialCardFlipButton } from "../components/ItwPresentationCredentialCardFlipButton"; +import { usePreventScreenCapture } from "../../../../utils/hooks/usePreventScreenCapture"; export type ItwPresentationCredentialCardModalNavigationParams = { credential: StoredCredential; @@ -41,6 +42,7 @@ const ItwPresentationCredentialCardModal = ({ route, navigation }: Props) => { const [isFlipped, setFlipped] = React.useState(false); const theme = useIOTheme(); + usePreventScreenCapture(); useMaxBrightness({ useSmoothTransition: true }); React.useLayoutEffect(() => { diff --git a/ts/features/itwallet/presentation/screens/ItwPresentationCredentialDetailScreen.tsx b/ts/features/itwallet/presentation/screens/ItwPresentationCredentialDetailScreen.tsx index f8c51ac4d50..962b8804dff 100644 --- a/ts/features/itwallet/presentation/screens/ItwPresentationCredentialDetailScreen.tsx +++ b/ts/features/itwallet/presentation/screens/ItwPresentationCredentialDetailScreen.tsx @@ -36,6 +36,7 @@ import { import { ItwCredentialTrustmark } from "../../trustmark/components/ItwCredentialTrustmark"; import ItwCredentialNotFound from "../../common/components/ItwCredentialNotFound"; import { ItwPresentationCredentialUnknownStatus } from "../components/ItwPresentationCredentialUnknownStatus"; +import { usePreventScreenCapture } from "../../../../utils/hooks/usePreventScreenCapture"; export type ItwPresentationCredentialDetailNavigationParams = { credentialType: string; @@ -81,6 +82,7 @@ const ItwPresentationCredentialDetail = ({ ); useDebugInfo(credential); + usePreventScreenCapture(); useFocusEffect(() => { trackCredentialDetail({ diff --git a/ts/features/itwallet/presentation/screens/ItwPresentationCredentialFiscalCodeModal.tsx b/ts/features/itwallet/presentation/screens/ItwPresentationCredentialFiscalCodeModal.tsx index 7467e3e1124..a83d56786c8 100644 --- a/ts/features/itwallet/presentation/screens/ItwPresentationCredentialFiscalCodeModal.tsx +++ b/ts/features/itwallet/presentation/screens/ItwPresentationCredentialFiscalCodeModal.tsx @@ -19,6 +19,7 @@ import { selectFiscalCodeFromEid, selectNameSurnameFromEid } from "../../credentials/store/selectors"; +import { usePreventScreenCapture } from "../../../../utils/hooks/usePreventScreenCapture"; /** * This magic number is the lenght of the encoded fiscal code in a CODE39 barcode. @@ -72,6 +73,7 @@ const ItwPresentationCredentialFiscalCodeModal = () => { const nameSurname = useIOSelector(selectNameSurnameFromEid); const fiscalCode = useIOSelector(selectFiscalCodeFromEid); + usePreventScreenCapture(); useMaxBrightness({ useSmoothTransition: true }); React.useLayoutEffect(() => { diff --git a/ts/features/itwallet/trustmark/screens/ItwCredentialTrustmarkScreen.tsx b/ts/features/itwallet/trustmark/screens/ItwCredentialTrustmarkScreen.tsx index acf0693a136..db1ba648968 100644 --- a/ts/features/itwallet/trustmark/screens/ItwCredentialTrustmarkScreen.tsx +++ b/ts/features/itwallet/trustmark/screens/ItwCredentialTrustmarkScreen.tsx @@ -9,6 +9,7 @@ import { ItwParamsList } from "../../navigation/ItwParamsList"; import { ItwTrustmarkExpirationTimer } from "../components/ItwTrustmarkExpirationTimer"; import { ItwTrustmarkQrCode } from "../components/ItwTrustmarkQrCode"; import { ItwTrustmarkMachineProvider } from "../machine/provider"; +import { usePreventScreenCapture } from "../../../../utils/hooks/usePreventScreenCapture"; export type ItwCredentialTrustmarkScreenNavigationParams = { credentialType: string; @@ -22,6 +23,7 @@ type ScreenProps = IOStackNavigationRouteProps< export const ItwCredentialTrustmarkScreen = (params: ScreenProps) => { const { credentialType } = params.route.params; + usePreventScreenCapture(); useMaxBrightness({ useSmoothTransition: true }); return ( diff --git a/ts/features/payments/onboarding/screens/PaymentsOnboardingSelectMethodScreen.tsx b/ts/features/payments/onboarding/screens/PaymentsOnboardingSelectMethodScreen.tsx index 311b1bed57c..86691f529a2 100644 --- a/ts/features/payments/onboarding/screens/PaymentsOnboardingSelectMethodScreen.tsx +++ b/ts/features/payments/onboarding/screens/PaymentsOnboardingSelectMethodScreen.tsx @@ -12,8 +12,11 @@ import { PaymentsOnboardingRoutes } from "../navigation/routes"; import { paymentsOnboardingGetMethodsAction } from "../store/actions"; import { selectPaymentOnboardingMethods } from "../store/selectors"; import { IOScrollViewWithLargeHeader } from "../../../../components/ui/IOScrollViewWithLargeHeader"; +import { usePreventScreenCapture } from "../../../../utils/hooks/usePreventScreenCapture"; const PaymentsOnboardingSelectMethodScreen = () => { + usePreventScreenCapture(); + const navigation = useIONavigation(); const dispatch = useIODispatch(); diff --git a/ts/store/reducers/__tests__/allowedSnapshotScreens.test.ts b/ts/store/reducers/__tests__/allowedSnapshotScreens.test.ts deleted file mode 100644 index 44f5dbe29b6..00000000000 --- a/ts/store/reducers/__tests__/allowedSnapshotScreens.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable eslint-comments/no-unlimited-disable */ -import { applicationChangeState } from "../../actions/application"; -import { setDebugModeEnabled } from "../../actions/debug"; -import { - isAllowedSnapshotCurrentScreen, - screenBlackList -} from "../allowedSnapshotScreens"; -import { appReducer } from "../index"; -import { GlobalState } from "../types"; - -jest.mock("@react-native-async-storage/async-storage", () => ({ - AsyncStorage: jest.fn() -})); - -jest.mock("react-native-share", () => ({ - open: jest.fn() -})); - -describe("allowed Snapshot Screens Selector test", () => { - it("Test high level composition", () => { - // with the debug mode enabled, expected true - expect( - isAllowedSnapshotCurrentScreen.resultFunc( - "NOT_SNAPSHOTTABLE_SCREEN", - true - ) - ).toBeTruthy(); - }); - it("Test all blacklisted screens", () => { - screenBlackList.forEach(screen => { - expect( - isAllowedSnapshotCurrentScreen.resultFunc(screen, false) - ).toBeFalsy(); - expect( - isAllowedSnapshotCurrentScreen.resultFunc(screen, true) - ).toBeTruthy(); - }); - }); - it("Test re-computations only when store interesting part changes", () => { - // eslint-disable-next-line - let globalState: GlobalState = appReducer( - undefined, - applicationChangeState("active") - ); - expect(isAllowedSnapshotCurrentScreen(globalState)).toBeTruthy(); - expect(isAllowedSnapshotCurrentScreen(globalState)).toBeTruthy(); - // with the same state, only one computation is expected - expect(isAllowedSnapshotCurrentScreen.recomputations()).toBe(1); - globalState = appReducer(globalState, setDebugModeEnabled(false)); - expect(isAllowedSnapshotCurrentScreen(globalState)).toBeTruthy(); - // with a change of state but the same values, no new computation are expected - expect(isAllowedSnapshotCurrentScreen.recomputations()).toBe(1); - globalState = appReducer(globalState, setDebugModeEnabled(true)); - expect(isAllowedSnapshotCurrentScreen(globalState)).toBeTruthy(); - expect(isAllowedSnapshotCurrentScreen(globalState)).toBeTruthy(); - // with a change of state and change in the interested values, a new computation is expected - expect(isAllowedSnapshotCurrentScreen.recomputations()).toBe(2); - globalState = appReducer(globalState, applicationChangeState("background")); - expect(isAllowedSnapshotCurrentScreen(globalState)).toBeTruthy(); - // with a change of state but not in the interested part, no new computation are expected - expect(isAllowedSnapshotCurrentScreen.recomputations()).toBe(2); - }); -}); diff --git a/ts/store/reducers/allowedSnapshotScreens.ts b/ts/store/reducers/allowedSnapshotScreens.ts deleted file mode 100644 index b6b2739a69b..00000000000 --- a/ts/store/reducers/allowedSnapshotScreens.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createSelector } from "reselect"; -import { PaymentsOnboardingRoutes } from "../../features/payments/onboarding/navigation/routes"; -import { isDebugModeEnabledSelector } from "./debug"; -import { currentRouteSelector } from "./navigation"; - -export const screenBlackList = new Set([ - PaymentsOnboardingRoutes.PAYMENT_ONBOARDING_SELECT_METHOD as string -]); - -/** - * Return {true} if a snapshot can be taken in the current screen (android only). - * If the app is in debug mode, the snapshot is always possible. - */ -export const isAllowedSnapshotCurrentScreen = createSelector( - [currentRouteSelector, isDebugModeEnabledSelector], - (currentRoute, debugEnabled) => - debugEnabled ? true : !screenBlackList.has(currentRoute) -); diff --git a/ts/utils/hooks/__tests__/usePreventScreenCapture.test.tsx b/ts/utils/hooks/__tests__/usePreventScreenCapture.test.tsx new file mode 100644 index 00000000000..a9bc87d51f3 --- /dev/null +++ b/ts/utils/hooks/__tests__/usePreventScreenCapture.test.tsx @@ -0,0 +1,55 @@ +import { + enableSecureView, + disableSecureView +} from "react-native-screenshot-prevent"; +import { createStore } from "redux"; +import { appReducer } from "../../../store/reducers"; +import { applicationChangeState } from "../../../store/actions/application"; +import { renderScreenWithNavigationStoreContext } from "../../testWrapper"; +import { usePreventScreenCapture } from "../usePreventScreenCapture"; + +jest.mock("react-native-screenshot-prevent", () => ({ + enableSecureView: jest.fn(), + disableSecureView: jest.fn() +})); + +describe("usePreventScreenCapture", () => { + jest.useFakeTimers(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it("calls native methods once if mounted & unmounted", async () => { + const { unmount } = renderHook(); + expect(enableSecureView).toHaveBeenCalledTimes(1); + unmount(); + jest.advanceTimersByTime(500); + expect(disableSecureView).toHaveBeenCalledTimes(1); + }); + + it("does not re-allow screen capture when two hooks are active and one is unmounted", async () => { + renderHook({ key: "A" }); + const hook2 = renderHook({ key: "B" }); + hook2.unmount(); + jest.advanceTimersByTime(500); + expect(enableSecureView).toHaveBeenCalledTimes(2); + expect(disableSecureView).toHaveBeenCalledTimes(0); + }); +}); + +type Params = { key?: string }; + +const renderHook = ({ key }: Params = {}) => { + const Component = () => { + usePreventScreenCapture(key); + return null; + }; + const globalState = appReducer(undefined, applicationChangeState("active")); + return renderScreenWithNavigationStoreContext( + Component, + "TEST", + {}, + createStore(appReducer, globalState as any) + ); +}; diff --git a/ts/utils/hooks/usePreventScreenCapture.ts b/ts/utils/hooks/usePreventScreenCapture.ts new file mode 100644 index 00000000000..6014f3d9f99 --- /dev/null +++ b/ts/utils/hooks/usePreventScreenCapture.ts @@ -0,0 +1,51 @@ +import { useFocusEffect } from "@react-navigation/native"; +import { useCallback, useMemo, useRef } from "react"; +import RNScreenshotPrevent from "react-native-screenshot-prevent"; +import uuid from "react-native-uuid"; + +const activeTags: Set = new Set(); + +const preventScreenCapture = (tag: string) => { + if (!activeTags.has(tag)) { + activeTags.add(tag); + RNScreenshotPrevent.enableSecureView(); + } +}; + +const allowScreenCapture = (tag: string) => { + activeTags.delete(tag); + if (activeTags.size === 0) { + RNScreenshotPrevent.disableSecureView(); + } +}; + +/** + * Hook that disables screen capture for as long as the component is focused. + * + * In a stack navigator unfocused screens are not unmounted, so the `useEffect` cleanup function is not called. + * + * The native library implementation is the following: + * - On Android, it enables `FLAG_SECURE` + * - On iOS, uses a hidden secure text field as the platform does not expose an API to disable screenshots + * + * @param key An optional key to prevent conflicts when using multiple instances of this hook at the same time. + */ +export function usePreventScreenCapture(key?: string) { + const tag = useMemo(() => key || uuid.v4().toString(), [key]); + + const timeoutRef = useRef(); + + useFocusEffect( + useCallback(() => { + clearTimeout(timeoutRef.current); + + preventScreenCapture(tag); + + return () => { + // Here we wait a little after the blur event for navigation transition animations. + // eslint-disable-next-line functional/immutable-data + timeoutRef.current = setTimeout(() => allowScreenCapture(tag), 500); + }; + }, [tag]) + ); +} diff --git a/yarn.lock b/yarn.lock index 71d23fe4ef2..647627ac301 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13686,7 +13686,6 @@ __metadata: react-native-easing-gradient: ^1.1.1 react-native-exception-handler: ^2.10.8 react-native-fingerprint-scanner: "git+https://github.com/hieuvp/react-native-fingerprint-scanner.git#9cecc0db326471c571553ea85f7c016fee2f803d" - react-native-flag-secure-android: ^1.0.3 react-native-fs: ^2.18.0 react-native-gesture-handler: ^2.18.1 react-native-get-random-values: ^1.11.0 @@ -13711,6 +13710,7 @@ __metadata: react-native-safe-area-context: ^4.10.5 react-native-screen-brightness: ^2.0.0-alpha react-native-screens: ^3.35.0 + react-native-screenshot-prevent: ^1.2.1 react-native-share: ^10.2.1 react-native-splash-screen: ^3.2.0 react-native-store-review: ^0.4.3 @@ -18908,13 +18908,6 @@ __metadata: languageName: node linkType: hard -"react-native-flag-secure-android@npm:^1.0.3": - version: 1.0.3 - resolution: "react-native-flag-secure-android@npm:1.0.3" - checksum: 1b95bc83a040e924d4b913284b914559891d35b52505acb981c18ad82ea3139e3a48e7f710a6c52f9fe0bf32374b45fb7c3cd9a18a35f2c1b5f61f49c0a4737c - languageName: node - linkType: hard - "react-native-fs@npm:^2.18.0": version: 2.18.0 resolution: "react-native-fs@npm:2.18.0" @@ -19272,6 +19265,16 @@ __metadata: languageName: node linkType: hard +"react-native-screenshot-prevent@npm:^1.2.1": + version: 1.2.1 + resolution: "react-native-screenshot-prevent@npm:1.2.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 5563adea419627ce1a066495c7779ffdfa85bdb9c180b47eb25215175a02fc4af402bf24cea684d208ae18651df9396fb545998dd3c566afe67f119ecfa62a4a + languageName: node + linkType: hard + "react-native-share@npm:^10.2.1": version: 10.2.1 resolution: "react-native-share@npm:10.2.1"