├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── .nvmrc ├── .watchmanconfig ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── android ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── captureprotection │ ├── CaptureProtectionModule.kt │ ├── CaptureProtectionPackage.kt │ └── android │ ├── constants │ ├── CaptureEventType.kt │ └── Constants.kt │ ├── lifecycle │ └── CaptureProtectionLifecycleListener.kt │ └── utils │ ├── ActivityUtils.kt │ ├── FileUtils.kt │ ├── Reflection.kt │ ├── Response.kt │ └── Thread.kt ├── babel.config.js ├── docs ├── MIGRATION.md ├── example.md ├── method.md └── type.md ├── ios ├── CaptureProtection-Bridging-Header.h ├── CaptureProtection.m ├── CaptureProtection.xcodeproj │ └── project.pbxproj └── CaptureProtection │ ├── CaptureProtection.swift │ ├── Constants │ ├── Config.swift │ └── Constants.swift │ └── Utils │ ├── TextUtils.swift │ └── UIViewUtils.swift ├── lefthook.yml ├── package.json ├── react-native-capture-protection.podspec ├── src ├── hooks.tsx ├── index.ts ├── modules.ts ├── providers.tsx └── type.ts ├── tsconfig.build.json └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | buy_me_a_coffee: arang 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/**/ios/Pods 46 | 47 | # Ruby 48 | example/**/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Expo 64 | .expo/ 65 | 66 | # Turborepo 67 | .turbo/ 68 | 69 | # generated by bob 70 | lib/ 71 | 72 | package-lock.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "example"] 2 | path = example 3 | url = https://github.com/wn-na/react-native-capture-protection-example.git 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.18.1 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 10 | 11 | ```sh 12 | yarn 13 | ``` 14 | 15 | > While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. 16 | 17 | While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. 18 | 19 | To start the packager: 20 | 21 | ```sh 22 | yarn example start 23 | ``` 24 | 25 | To run the example app on Android: 26 | 27 | ```sh 28 | yarn example android 29 | ``` 30 | 31 | To run the example app on iOS: 32 | 33 | ```sh 34 | yarn example ios 35 | ``` 36 | 37 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 38 | 39 | ```sh 40 | yarn typescript 41 | yarn lint 42 | ``` 43 | 44 | To fix formatting errors, run the following: 45 | 46 | ```sh 47 | yarn lint --fix 48 | ``` 49 | 50 | Remember to add tests for your change if possible. Run the unit tests by: 51 | 52 | ```sh 53 | yarn test 54 | ``` 55 | 56 | To edit the Objective-C or Swift files, open `example/ios/CaptureProtectionExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-capture-protection`. 57 | 58 | To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `react-native-capture-protection` under `Android`. 59 | 60 | 61 | ### Commit message convention 62 | 63 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 64 | 65 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 66 | - `feat`: new features, e.g. add new method to the module. 67 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 68 | - `docs`: changes into documentation, e.g. add usage example for the module.. 69 | - `test`: adding or updating tests, e.g. add integration tests using detox. 70 | - `chore`: tooling changes, e.g. change CI config. 71 | 72 | Our pre-commit hooks verify that your commit message matches this format when committing. 73 | 74 | ### Linting and tests 75 | 76 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 77 | 78 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 79 | 80 | Our pre-commit hooks verify that the linter and tests pass when committing. 81 | 82 | ### Publishing to npm 83 | 84 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 85 | 86 | To publish new versions, run the following: 87 | 88 | ```sh 89 | yarn release 90 | ``` 91 | 92 | ### Scripts 93 | 94 | The `package.json` file contains various scripts for common tasks: 95 | 96 | - `yarn bootstrap`: setup project by installing all dependencies and pods. 97 | - `yarn typescript`: type-check files with TypeScript. 98 | - `yarn lint`: lint files with ESLint. 99 | - `yarn test`: run unit tests with Jest. 100 | - `yarn example start`: start the Metro server for the example app. 101 | - `yarn example android`: run the example app on Android. 102 | - `yarn example ios`: run the example app on iOS. 103 | 104 | ### Sending a pull request 105 | 106 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 107 | 108 | When you're sending a pull request: 109 | 110 | - Prefer small pull requests focused on one change. 111 | - Verify that linters and tests are passing. 112 | - Review the documentation to make sure it looks good. 113 | - Follow the pull request template when opening a pull request. 114 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 lethe 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-capture-protection 2 | 3 | > 🛡️ A React Native library to prevent screen capture, screenshots and app switcher for enhanced security. Fully compatible with both Expo and CLI. 4 | 5 | | screenshot | app switcher | 6 | | ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 7 | | ![Simulator Screen Recording](https://user-images.githubusercontent.com/37437842/206644553-e4c3f2bc-b624-47ac-a005-132199e049b2.gif) | ![Simulator Screen Recording - iPhone 15 Pro - 2024-07-02 at 21 19 17](https://github.com/0xlethe/react-native-capture-protection/assets/37437842/ac98e942-8dba-4e5d-9f23-fa10f946b26b) | 8 | 9 | ## Features 10 | 11 | - iOS Capture Protection (Screenshot, Screen Recording, App Switcher) 12 | - Android Capture Protection (Screenshot, Screen Recording) 13 | - Event Listener for Capture Events 14 | - Provider and Hooks Support 15 | - Android 14 Support 16 | 17 | ## Installation 18 | 19 | ### Use npm 20 | 21 | ```sh 22 | npm install react-native-capture-protection 23 | ``` 24 | 25 | ### Use yarn 26 | 27 | ```sh 28 | yarn add react-native-capture-protection 29 | ``` 30 | 31 | ### use Expo 32 | 33 | > Only Expo Dev client compatible 34 | > This library has native code, so it's not work for Expo Go but it's compatible with custom dev client. 35 | 36 | ``` 37 | npx expo install react-native-capture-protection 38 | ``` 39 | 40 | ## How to Use 41 | 42 | ```js 43 | import { 44 | CaptureProtection, 45 | useCaptureProtection, 46 | CaptureEventType 47 | } from 'react-native-capture-protection'; 48 | 49 | const Component = () => { 50 | const { protectionStatus, status } = useCaptureProtection(); 51 | 52 | React.useEffect(() => { 53 | // Prevent all capture events 54 | CaptureProtection.prevent(); 55 | 56 | // Or prevent specific events 57 | CaptureProtection.prevent({ 58 | screenshot: true, 59 | record: true, 60 | appSwitcher: true 61 | }); 62 | }, []); 63 | 64 | React.useEffect(() => { 65 | // Check if any capture is prevented 66 | console.log('Prevent Status:', protectionStatus); 67 | 68 | // Check current protection status 69 | console.log('Protection Status:', status); 70 | }, [protectionStatus, status]); 71 | 72 | // Allow all capture events 73 | const onAllow = async () => { 74 | await CaptureProtection.allow(); 75 | }; 76 | 77 | // Allow specific events 78 | const onAllowSpecific = async () => { 79 | await CaptureProtection.allow({ 80 | screenshot: true, 81 | record: false, 82 | appSwitcher: true 83 | }); 84 | }; 85 | 86 | // Check if screen is being recorded 87 | const checkRecording = async () => { 88 | const isRecording = await CaptureProtection.isScreenRecording(); 89 | console.log('Is Recording:', isRecording); 90 | }; 91 | 92 | return ( 93 | // Your component JSX 94 | ); 95 | }; 96 | ``` 97 | 98 | ## Documentation 99 | 100 | - [Methods](./docs/method.md) - Detailed documentation of all available methods 101 | - [Types](./docs/type.md) - Type definitions and interfaces 102 | - [Migration Guide](./docs/MIGRATION.md) - Guide for migrating from v1.x to v2.x 103 | 104 | ## Contributing 105 | 106 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 107 | 108 | ## License 109 | 110 | MIT 111 | 112 | --- 113 | 114 | Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) 115 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | // The Android Gradle plugin is only required when opening the Android folder stand-alone. 3 | // This avoids unnecessary downloads and potential conflicts when the library is included as a 4 | // module dependency in an application project. 5 | if (project == rootProject) { 6 | ext { 7 | kotlinVersion = rootProject.ext.has("kotlinVersion") ? rootProject.ext.kotlinVersion : "1.8.0" 8 | } 9 | repositories { 10 | google() 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | classpath 'com.android.tools.build:gradle:3.5.3' 16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${rootProject.ext.kotlinVersion}" 17 | } 18 | } 19 | } 20 | 21 | def isNewArchitectureEnabled() { 22 | return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" 23 | } 24 | 25 | apply plugin: 'com.android.library' 26 | apply plugin: "kotlin-android" 27 | 28 | if (isNewArchitectureEnabled()) { 29 | apply plugin: 'com.facebook.react' 30 | } 31 | 32 | def getExtOrDefault(name) { 33 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['CaptureProtection_' + name] 34 | } 35 | 36 | def getExtOrIntegerDefault(name) { 37 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['CaptureProtection_' + name]).toInteger() 38 | } 39 | 40 | android { 41 | compileSdkVersion getExtOrIntegerDefault('compileSdkVersion') 42 | 43 | //Get Android Gradle Plugin version 44 | def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION 45 | //Apply namespace only if Android Gradle Plugin version equal or greater than 7 46 | if (agpVersion.tokenize('.')[0].toInteger() >= 7) { 47 | namespace "com.captureprotection" 48 | } 49 | 50 | defaultConfig { 51 | minSdkVersion getExtOrIntegerDefault('minSdkVersion') 52 | targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') 53 | buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() 54 | } 55 | buildTypes { 56 | release { 57 | minifyEnabled false 58 | } 59 | } 60 | 61 | lintOptions { 62 | disable 'GradleCompatible' 63 | } 64 | 65 | compileOptions { 66 | sourceCompatibility JavaVersion.VERSION_1_8 67 | targetCompatibility JavaVersion.VERSION_1_8 68 | } 69 | 70 | } 71 | 72 | repositories { 73 | mavenCentral() 74 | google() 75 | 76 | def found = false 77 | def defaultDir = null 78 | def androidSourcesName = 'React Native sources' 79 | 80 | if (rootProject.ext.has('reactNativeAndroidRoot')) { 81 | defaultDir = rootProject.ext.get('reactNativeAndroidRoot') 82 | } else { 83 | defaultDir = new File( 84 | projectDir, 85 | '/../../../node_modules/react-native/android' 86 | ) 87 | } 88 | 89 | if (defaultDir.exists()) { 90 | maven { 91 | url defaultDir.toString() 92 | name androidSourcesName 93 | } 94 | 95 | logger.info(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}") 96 | found = true 97 | } else { 98 | def parentDir = rootProject.projectDir 99 | 100 | 1.upto(5, { 101 | if (found) return true 102 | parentDir = parentDir.parentFile 103 | 104 | def androidSourcesDir = new File( 105 | parentDir, 106 | 'node_modules/react-native' 107 | ) 108 | 109 | def androidPrebuiltBinaryDir = new File( 110 | parentDir, 111 | 'node_modules/react-native/android' 112 | ) 113 | 114 | if (androidPrebuiltBinaryDir.exists()) { 115 | maven { 116 | url androidPrebuiltBinaryDir.toString() 117 | name androidSourcesName 118 | } 119 | 120 | logger.info(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}") 121 | found = true 122 | } else if (androidSourcesDir.exists()) { 123 | maven { 124 | url androidSourcesDir.toString() 125 | name androidSourcesName 126 | } 127 | 128 | logger.info(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}") 129 | found = true 130 | } 131 | }) 132 | } 133 | 134 | if (!found) { 135 | throw new GradleException( 136 | "${project.name}: unable to locate React Native android sources. " + 137 | "Ensure you have you installed React Native as a dependency in your project and try again." 138 | ) 139 | } 140 | } 141 | 142 | 143 | dependencies { 144 | //noinspection GradleDynamicVersion 145 | implementation "com.facebook.react:react-native:+" 146 | // From node_modules 147 | } 148 | 149 | if (isNewArchitectureEnabled()) { 150 | react { 151 | jsRootDir = file("../src/") 152 | libraryName = "CaptureProtection" 153 | codegenJavaPackageName = "com.captureprotection" 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | CaptureProtection_kotlinVersion=1.7.0 2 | CaptureProtection_minSdkVersion=21 3 | CaptureProtection_targetSdkVersion=31 4 | CaptureProtection_compileSdkVersion=31 5 | CaptureProtection_ndkversion=21.4.7075529 6 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/CaptureProtectionModule.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection 2 | 3 | import android.view.WindowManager 4 | import com.captureprotection.constants.CaptureEventType 5 | import com.captureprotection.constants.Constants 6 | import com.facebook.react.bridge.* 7 | import com.facebook.react.module.annotations.ReactModule 8 | 9 | @ReactModule(name = Constants.NAME) 10 | class CaptureProtectionModule(private val reactContext: ReactApplicationContext) : 11 | CaptureProtectionLifecycleListener(reactContext) { 12 | 13 | @ReactMethod 14 | fun addListener(eventName: String) { 15 | super.addScreenCaptureListener() 16 | } 17 | 18 | @ReactMethod 19 | fun removeListeners(count: Int) { 20 | // super.removeScreenCaptureListener() 21 | } 22 | 23 | @ReactMethod 24 | fun hasListener(promise: Promise) { 25 | currentActivity?.runOnUiThread { 26 | try { 27 | val params = super.hasScreenCaptureListener() 28 | promise.resolve(params) 29 | } catch (e: Exception) { 30 | promise.reject("hasListener", e) 31 | } 32 | } 33 | } 34 | 35 | @ReactMethod 36 | fun isScreenRecording(promise: Promise) { 37 | currentActivity?.runOnUiThread { 38 | try { 39 | promise.resolve(super.screens.size > 1) 40 | } catch (e: Exception) { 41 | promise.reject("isScreenRecording", e) 42 | } 43 | } 44 | } 45 | 46 | @ReactMethod 47 | fun prevent(promise: Promise) { 48 | currentActivity?.runOnUiThread { 49 | try { 50 | val currentActivity = ActivityUtils.getReactCurrentActivity(reactContext) 51 | currentActivity!!.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) 52 | Response.sendEvent( 53 | reactContext, 54 | Constants.LISTENER_EVENT_NAME, 55 | CaptureEventType.PREVENT_SCREEN_CAPTURE.value + 56 | CaptureEventType.PREVENT_SCREEN_RECORDING.value + 57 | CaptureEventType.PREVENT_SCREEN_APP_SWITCHING.value 58 | ) 59 | promise.resolve(true) 60 | } catch (e: Exception) { 61 | promise.reject("prevent", e) 62 | } 63 | } 64 | } 65 | 66 | @ReactMethod 67 | fun allow(promise: Promise) { 68 | currentActivity?.runOnUiThread { 69 | try { 70 | val currentActivity = ActivityUtils.getReactCurrentActivity(reactContext) 71 | currentActivity!!.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) 72 | Response.sendEvent( 73 | reactContext, 74 | Constants.LISTENER_EVENT_NAME, 75 | CaptureEventType.ALLOW.value 76 | ) 77 | promise.resolve(true) 78 | } catch (e: Exception) { 79 | promise.reject("allow", e) 80 | } 81 | } 82 | } 83 | 84 | @ReactMethod 85 | fun protectionStatus(promise: Promise) { 86 | currentActivity?.runOnUiThread { 87 | try { 88 | val flags = ActivityUtils.isSecureFlag(reactContext) 89 | promise.resolve(flags) 90 | } catch (e: Exception) { 91 | promise.reject("protectionStatus", e) 92 | } 93 | } 94 | } 95 | 96 | @ReactMethod 97 | fun requestPermission(promise: Promise) { 98 | val isPermission = super.requestStoragePermission() 99 | promise.resolve(isPermission) 100 | } 101 | 102 | @ReactMethod 103 | fun checkPermission(promise: Promise) { 104 | val isPermission = super.checkStoragePermission() 105 | promise.resolve(isPermission) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/CaptureProtectionPackage.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection 2 | 3 | import android.view.View 4 | import com.facebook.react.ReactPackage 5 | import com.facebook.react.bridge.NativeModule 6 | import com.facebook.react.bridge.ReactApplicationContext 7 | import com.facebook.react.uimanager.ReactShadowNode 8 | import com.facebook.react.uimanager.ViewManager 9 | 10 | class CaptureProtectionPackage : ReactPackage { 11 | override fun createViewManagers( 12 | reactContext: ReactApplicationContext 13 | ): MutableList>> = mutableListOf() 14 | 15 | override fun createNativeModules( 16 | reactContext: ReactApplicationContext 17 | ): MutableList = listOf(CaptureProtectionModule(reactContext)).toMutableList() 18 | } 19 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/android/constants/CaptureEventType.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection.constants 2 | 3 | enum class CaptureEventType(val value: Int) { 4 | NONE(0), 5 | RECORDING(1), 6 | END_RECORDING(2), 7 | CAPTURED(3), 8 | APP_SWITCHING(4), 9 | UNKNOWN(5), 10 | ALLOW(8), 11 | PREVENT_SCREEN_CAPTURE(16), 12 | PREVENT_SCREEN_RECORDING(32), 13 | PREVENT_SCREEN_APP_SWITCHING(64), 14 | } 15 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/android/constants/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection.constants 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | import com.facebook.react.bridge.* 6 | 7 | class Constants { 8 | companion object { 9 | const val LISTENER_EVENT_NAME = "CaptureProtectionListener" 10 | const val NAME = "CaptureProtection" 11 | val requestPermission: String = 12 | if (Build.VERSION.SDK_INT >= 33) { 13 | "android.permission.READ_MEDIA_IMAGES" 14 | } else { 15 | Manifest.permission.READ_EXTERNAL_STORAGE 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/android/lifecycle/CaptureProtectionLifecycleListener.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.database.ContentObserver 7 | import android.hardware.display.DisplayManager 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.provider.MediaStore 11 | import android.util.Log 12 | import androidx.core.app.ActivityCompat 13 | import androidx.core.content.ContextCompat 14 | import com.captureprotection.constants.CaptureEventType 15 | import com.captureprotection.constants.Constants 16 | import com.captureprotection.utils.FileUtils 17 | import com.captureprotection.utils.ModuleThread 18 | import com.facebook.react.bridge.* 19 | import java.lang.reflect.Method 20 | import java.util.ArrayList 21 | import kotlinx.coroutines.* 22 | 23 | open class CaptureProtectionLifecycleListener( 24 | private val reactContext: ReactApplicationContext, 25 | ) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener { 26 | 27 | override fun getName() = Constants.NAME 28 | 29 | val displayManager: DisplayManager = 30 | reactContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager 31 | 32 | val screens = ArrayList() 33 | val reactCurrentActivity: Activity? 34 | get() = ActivityUtils.getReactCurrentActivity(reactContext) 35 | var eventJob: Job? = null 36 | 37 | companion object { 38 | var screenCaptureCallback: Any? = null 39 | var registerScreenCaptureCallback: Method? = null 40 | var unregisterScreenCaptureCallback: Method? = null 41 | 42 | var contentObserver: ContentObserver? = null 43 | var displayListener: DisplayManager.DisplayListener? = null 44 | var reactContext: ReactApplicationContext? = null 45 | } 46 | 47 | fun reflectionCallback() { 48 | if (Build.VERSION.SDK_INT < 34) { 49 | return 50 | } 51 | if (reactCurrentActivity == null) { 52 | return 53 | } 54 | 55 | if (CaptureProtectionLifecycleListener.registerScreenCaptureCallback == null) { 56 | CaptureProtectionLifecycleListener.registerScreenCaptureCallback = 57 | Reflection.getMethod( 58 | reactCurrentActivity!!.javaClass, 59 | "registerScreenCaptureCallback" 60 | ) 61 | } 62 | 63 | if (CaptureProtectionLifecycleListener.unregisterScreenCaptureCallback == null) { 64 | CaptureProtectionLifecycleListener.unregisterScreenCaptureCallback = 65 | Reflection.getMethod( 66 | reactCurrentActivity!!.javaClass, 67 | "unregisterScreenCaptureCallback" 68 | ) 69 | } 70 | 71 | if (CaptureProtectionLifecycleListener.screenCaptureCallback == null || 72 | CaptureProtectionLifecycleListener.reactContext != reactContext 73 | ) { 74 | CaptureProtectionLifecycleListener.screenCaptureCallback = 75 | Reflection.createScreenCaptureCallback { 76 | triggerCaptureEvent(CaptureEventType.CAPTURED) 77 | } 78 | } 79 | } 80 | 81 | fun triggerCaptureEvent(type: CaptureEventType) { 82 | eventJob?.cancel() 83 | eventJob = CoroutineScope(Dispatchers.Main).launch { 84 | try { 85 | Response.sendEvent(reactContext, Constants.LISTENER_EVENT_NAME, type.value) 86 | delay(1000) 87 | if (screens.isNotEmpty()) { 88 | Response.sendEvent( 89 | reactContext, 90 | Constants.LISTENER_EVENT_NAME, 91 | CaptureEventType.RECORDING.value 92 | ) 93 | } else { 94 | Response.sendEvent( 95 | reactContext, 96 | Constants.LISTENER_EVENT_NAME, 97 | CaptureEventType.NONE.value 98 | ) 99 | } 100 | } catch (e: Exception) { 101 | Log.e(Constants.NAME, "Error in triggerCaptureEvent: ${e.message}") 102 | } 103 | } 104 | } 105 | 106 | fun registerDisplayListener() { 107 | if (CaptureProtectionLifecycleListener.displayListener == null || 108 | CaptureProtectionLifecycleListener.reactContext != reactContext 109 | ) { 110 | CaptureProtectionLifecycleListener.displayListener = 111 | object : DisplayManager.DisplayListener { 112 | override fun onDisplayAdded(displayId: Int) { 113 | reactCurrentActivity?.runOnUiThread { 114 | if (displayManager.getDisplay(displayId) == null) { 115 | screens.add(displayId) 116 | } 117 | try { 118 | Response.sendEvent( 119 | reactContext, 120 | Constants.LISTENER_EVENT_NAME, 121 | if (screens.isEmpty()) CaptureEventType.NONE.value 122 | else CaptureEventType.RECORDING.value 123 | ) 124 | Log.d(Constants.NAME, "=> display add event $displayId") 125 | } catch (e: Exception) { 126 | Log.e( 127 | Constants.NAME, 128 | "display add event Error with displayId: $displayId, error: ${e.message}" 129 | ) 130 | } 131 | } 132 | } 133 | 134 | override fun onDisplayRemoved(displayId: Int) { 135 | reactCurrentActivity?.runOnUiThread { 136 | val index = screens.indexOf(displayId) 137 | if (index > -1) { 138 | screens.removeAt(index) 139 | } 140 | try { 141 | if (screens.isEmpty()) { 142 | triggerCaptureEvent(CaptureEventType.END_RECORDING) 143 | } else { 144 | Response.sendEvent( 145 | reactContext, 146 | Constants.LISTENER_EVENT_NAME, 147 | CaptureEventType.RECORDING.value 148 | ) 149 | } 150 | Log.d(Constants.NAME, "=> display remove event $displayId") 151 | } catch (e: Exception) { 152 | Log.e( 153 | Constants.NAME, 154 | "display remove event Error with displayId: $displayId, error: ${e.message}" 155 | ) 156 | } 157 | } 158 | } 159 | 160 | override fun onDisplayChanged(displayId: Int) { 161 | Log.d(Constants.NAME, "=> display change event $displayId") 162 | } 163 | } 164 | } 165 | } 166 | 167 | init { 168 | if (CaptureProtectionLifecycleListener.reactContext != reactContext) { 169 | CaptureProtectionLifecycleListener.screenCaptureCallback = null 170 | } 171 | reflectionCallback() 172 | registerDisplayListener() 173 | displayManager.registerDisplayListener( 174 | CaptureProtectionLifecycleListener.displayListener, 175 | ModuleThread.MainHandler 176 | ) 177 | CaptureProtectionLifecycleListener.reactContext = reactContext 178 | reactContext.addLifecycleEventListener(this) 179 | } 180 | 181 | override fun onHostResume() { 182 | try { 183 | reflectionCallback() 184 | 185 | CaptureProtectionLifecycleListener.registerScreenCaptureCallback?.let { method -> 186 | method.invoke( 187 | reactCurrentActivity, 188 | ModuleThread.MainExecutor, 189 | CaptureProtectionLifecycleListener.screenCaptureCallback 190 | ) 191 | } 192 | } catch (e: Exception) { 193 | Log.e(Constants.NAME, "onHostResume has raise Exception: " + e.message) 194 | } 195 | } 196 | 197 | override fun onHostPause() {} 198 | 199 | override fun onHostDestroy() { 200 | try { 201 | CaptureProtectionLifecycleListener.unregisterScreenCaptureCallback?.let { method -> 202 | method.invoke( 203 | reactCurrentActivity, 204 | CaptureProtectionLifecycleListener.screenCaptureCallback 205 | ) 206 | } 207 | } catch (e: Exception) { 208 | Log.e(Constants.NAME, "onHostDestroy has raise Exception: " + e.localizedMessage) 209 | } 210 | } 211 | 212 | fun checkStoragePermission(): Boolean { 213 | return when { 214 | Build.VERSION.SDK_INT < Build.VERSION_CODES.M -> true 215 | CaptureProtectionLifecycleListener.registerScreenCaptureCallback != null -> true 216 | else -> checkPermission() 217 | } 218 | } 219 | 220 | private fun checkPermission(): Boolean { 221 | return try { 222 | reactCurrentActivity?.let { 223 | ContextCompat.checkSelfPermission(it, Constants.requestPermission) == 224 | PackageManager.PERMISSION_GRANTED 225 | } 226 | ?: false 227 | } catch (e: Exception) { 228 | Log.e(Constants.NAME, "checkStoragePermission raised Exception: ${e.localizedMessage}") 229 | false 230 | } 231 | } 232 | 233 | fun requestStoragePermission(): Boolean { 234 | return try { 235 | val isGranted = checkStoragePermission() 236 | if (!isGranted) { 237 | Log.d(Constants.NAME, "Permission is revoked") 238 | requestPermission() 239 | false 240 | } else { 241 | Log.d(Constants.NAME, "Permission is granted") 242 | true 243 | } 244 | } catch (e: Exception) { 245 | Log.e( 246 | Constants.NAME, 247 | "requestStoragePermission raised Exception: ${e.localizedMessage}" 248 | ) 249 | false 250 | } 251 | } 252 | 253 | private fun requestPermission() { 254 | reactCurrentActivity?.let { 255 | ActivityCompat.requestPermissions(it, arrayOf(Constants.requestPermission), 1) 256 | } 257 | } 258 | 259 | fun addScreenCaptureListener() { 260 | reflectionCallback() 261 | 262 | if (CaptureProtectionLifecycleListener.registerScreenCaptureCallback == null) { 263 | if (CaptureProtectionLifecycleListener.contentObserver == null && 264 | checkStoragePermission() 265 | ) { 266 | CaptureProtectionLifecycleListener.contentObserver = 267 | object : ContentObserver(ModuleThread.MainHandler) { 268 | override fun onChange(selfChange: Boolean, uri: Uri?) { 269 | if (FileUtils.isImageUri(uri)) { 270 | if (FileUtils.isScreenshotFile(reactContext, uri!!)) { 271 | Log.d( 272 | Constants.NAME, 273 | "CaptureProtectionLifecycleListener.contentObserver detect screenshot file" 274 | ) 275 | triggerCaptureEvent(CaptureEventType.CAPTURED) 276 | } 277 | } 278 | super.onChange(selfChange, uri) 279 | } 280 | } 281 | reactContext.contentResolver.registerContentObserver( 282 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 283 | true, 284 | CaptureProtectionLifecycleListener.contentObserver!! 285 | ) 286 | } 287 | } 288 | } 289 | 290 | fun removeScreenCaptureListener() { 291 | CaptureProtectionLifecycleListener.contentObserver?.let { 292 | reactContext.contentResolver.unregisterContentObserver(it) 293 | } 294 | } 295 | 296 | fun hasScreenCaptureListener(): Boolean { 297 | return CaptureProtectionLifecycleListener.contentObserver != null || 298 | CaptureProtectionLifecycleListener.registerScreenCaptureCallback != null 299 | } 300 | 301 | fun hasScreenRecordListener(): Boolean { 302 | return displayListener != null 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/android/utils/ActivityUtils.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection 2 | 3 | import android.app.Activity 4 | import android.view.WindowManager 5 | import com.facebook.react.bridge.* 6 | 7 | class ActivityUtils { 8 | companion object { 9 | fun getReactCurrentActivity(reactContext: ReactApplicationContext): Activity? { 10 | return reactContext.currentActivity 11 | } 12 | 13 | fun isSecureFlag(reactContext: ReactApplicationContext): Boolean { 14 | val currentActivity = getReactCurrentActivity(reactContext) 15 | return currentActivity?.window?.attributes?.flags?.and( 16 | WindowManager.LayoutParams.FLAG_SECURE 17 | ) != 0 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/android/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection.utils 2 | 3 | import android.database.Cursor 4 | import android.net.Uri 5 | import android.provider.MediaStore 6 | import com.facebook.react.bridge.* 7 | 8 | class FileUtils { 9 | companion object { 10 | fun isImageUri(uri: Uri?): Boolean { 11 | return uri?.toString() 12 | ?.matches(Regex("${MediaStore.Images.Media.EXTERNAL_CONTENT_URI}/[0-9]+")) 13 | ?: false 14 | } 15 | 16 | fun isScreenshotFile(reactContext: ReactApplicationContext, uri: Uri): Boolean { 17 | var cursor: Cursor? = null 18 | try { 19 | cursor = 20 | reactContext.contentResolver.query( 21 | uri, 22 | arrayOf(MediaStore.Images.Media.DATA), 23 | null, 24 | null, 25 | null 26 | ) 27 | if (cursor != null && cursor.moveToFirst()) { 28 | val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA)) 29 | return (path != null && path.toLowerCase().contains("screenshots")) 30 | } 31 | return false 32 | } finally { 33 | cursor?.close() 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/android/utils/Reflection.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection 2 | 3 | import android.app.Activity 4 | import android.util.Log 5 | import com.captureprotection.constants.Constants 6 | import java.lang.reflect.InvocationHandler 7 | import java.lang.reflect.Method 8 | import java.lang.reflect.Proxy 9 | 10 | class Reflection { 11 | companion object { 12 | private val NAME = "${Constants.NAME}_Reflection" 13 | fun getMethod(c: Class<*>?, methodName: String): Method? { 14 | return try { 15 | generateSequence(c) { it.superclass } 16 | .flatMap { it.declaredMethods.asSequence() } 17 | .firstOrNull { it.name == methodName } 18 | } catch (e: Exception) { 19 | Log.e(NAME, "Exception: ${methodName} -> ${e.localizedMessage}") 20 | null 21 | } 22 | } 23 | 24 | fun createScreenCaptureCallback(onCapturedAction: () -> Unit): Any? { 25 | val declaredClasses = Activity::class.java.declaredClasses 26 | if (declaredClasses.isNullOrEmpty()) { 27 | Log.e(Constants.NAME, "No declared classes found in Activity.") 28 | return null 29 | } 30 | 31 | val clazz = declaredClasses.find { it.simpleName == "ScreenCaptureCallback" } 32 | if (clazz == null || !clazz.isInterface) { 33 | Log.e( 34 | Constants.NAME, 35 | "ScreenCaptureCallback interface not found or is not an interface." 36 | ) 37 | return null 38 | } 39 | 40 | return Proxy.newProxyInstance( 41 | clazz.classLoader, 42 | arrayOf(clazz), 43 | InvocationHandler { proxy, method, args -> 44 | if (method.name == "onScreenCaptured") { 45 | try { 46 | Log.d(Constants.NAME, "=> capture onScreenCaptured add event") 47 | onCapturedAction() 48 | } catch (e: Exception) { 49 | Log.e( 50 | Constants.NAME, 51 | "onScreenCaptured has raised Exception: ${e.localizedMessage}" 52 | ) 53 | } 54 | return@InvocationHandler null 55 | } 56 | 57 | when (method.returnType) { 58 | Boolean::class.javaPrimitiveType -> return@InvocationHandler false 59 | Int::class.javaPrimitiveType -> return@InvocationHandler 0 60 | Float::class.javaPrimitiveType -> return@InvocationHandler 0.0f 61 | Double::class.javaPrimitiveType -> return@InvocationHandler 0.0 62 | Long::class.javaPrimitiveType -> return@InvocationHandler 0L 63 | Short::class.javaPrimitiveType -> return@InvocationHandler 0.toShort() 64 | Byte::class.javaPrimitiveType -> return@InvocationHandler 0.toByte() 65 | Char::class.javaPrimitiveType -> return@InvocationHandler '\u0000' 66 | else -> return@InvocationHandler null 67 | } 68 | } 69 | ) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/android/utils/Response.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection 2 | 3 | import com.facebook.react.bridge.* 4 | import com.facebook.react.modules.core.DeviceEventManagerModule 5 | 6 | class Response { 7 | companion object { 8 | fun sendEvent(reactContext: ReactApplicationContext, eventName: String, status: Int) { 9 | reactContext 10 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) 11 | .emit(eventName, status) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/src/main/java/com/captureprotection/android/utils/Thread.kt: -------------------------------------------------------------------------------- 1 | package com.captureprotection.utils 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import java.util.concurrent.Executor 6 | 7 | class ModuleThread { 8 | companion object { 9 | val MainHandler = Handler(Looper.getMainLooper()) 10 | val MainExecutor = Executor { command -> MainHandler.post(command) } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migration Guide 2 | 3 | ## From v1.x to v2.x 4 | 5 | ### Breaking Changes 6 | 7 | 1. **Hook Return Value Changes** 8 | 9 | ```diff 10 | - const { isPrevent, status } = useCaptureProtection(); 11 | + const { protectionStatus, status } = useCaptureProtection(); 12 | ``` 13 | 14 | 2. **Method Name Changes** 15 | 16 | ```diff 17 | - CaptureProtection.preventScreenRecord(); 18 | - CaptureProtection.allowScreenRecord(); 19 | + CaptureProtection.prevent({ record: true }); 20 | + CaptureProtection.allow({ record: true }); 21 | 22 | - CaptureProtection.setScreenRecordScreenWithText("test"); 23 | + CaptureProtection.prevent({ record: { text: "test" } }); 24 | 25 | - CaptureProtection.setScreenRecordScreenWithImage(require("")); 26 | + CaptureProtection.prevent({ record: { image: require("") } }); 27 | 28 | - CaptureProtection.preventBackground(); 29 | - CaptureProtection.allowBackground(); 30 | + CaptureProtection.prevent({ appSwitcher: true }); 31 | + CaptureProtection.allow({ appSwitcher: true }); 32 | ``` 33 | 34 | ### New Features 35 | 36 | 1. **Unified API** 37 | 38 | - Single `prevent()` and `allow()` methods with options 39 | - Platform-specific options for iOS and Android 40 | 41 | 2. **Enhanced Event Types** 42 | 43 | - New event types for better capture event handling 44 | - Improved event listener system 45 | 46 | 3. **Improved Type Safety** 47 | - Better TypeScript support 48 | - More precise type definitions 49 | 50 | ### Migration Steps 51 | 52 | 1. Update the package: 53 | 54 | ```bash 55 | npm install react-native-capture-protection@latest 56 | # or 57 | yarn add react-native-capture-protection@latest 58 | ``` 59 | 60 | 2. Update your imports: 61 | 62 | ```diff 63 | - import { 64 | - CaptureProtection, 65 | - CaptureProtectionModuleStatus, 66 | - isCapturedStatus 67 | - } from 'react-native-capture-protection'; 68 | + import { 69 | + CaptureProtection, 70 | + useCaptureProtection, 71 | + CaptureEventType 72 | + } from 'react-native-capture-protection'; 73 | ``` 74 | 75 | 3. Update your hook usage: 76 | 77 | ```diff 78 | - const { isPrevent, status } = useCaptureProtection(); 79 | + const { protectionStatus, status } = useCaptureProtection(); 80 | ``` 81 | 82 | 4. Update your method calls: 83 | 84 | ```diff 85 | - CaptureProtection.preventScreenRecord(); 86 | + CaptureProtection.prevent({ record: true }); 87 | ``` 88 | 89 | 5. Update your event listeners: 90 | ```diff 91 | - CaptureProtection.addListener((event) => { 92 | - // Handle event 93 | - }); 94 | + CaptureProtection.addListener((event) => { 95 | + switch (event) { 96 | + case CaptureEventType.CAPTURED: 97 | + // Handle capture 98 | + break; 99 | + case CaptureEventType.RECORDING: 100 | + // Handle recording 101 | + break; 102 | + // ... other cases 103 | + } 104 | + }); 105 | ``` 106 | -------------------------------------------------------------------------------- /docs/example.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | ## How to prevent the screen in App switcher only (Android) 4 | 5 | ```typescript 6 | useEffect(() => { 7 | const appStateFocusSub = AppState.addEventListener('focus', () => { 8 | CaptureProtection.allow(); 9 | }); 10 | const appStateBlurSub = AppState.addEventListener('blur', () => { 11 | CaptureProtection.prevent(); 12 | }); 13 | 14 | return () => { 15 | appStateFocusSub.remove(); 16 | appStateBlurSub.remove(); 17 | }; 18 | }, []); 19 | ``` 20 | 21 | - [source](https://github.com/wn-na/react-native-capture-protection/issues/80#issuecomment-2854515681) 22 | -------------------------------------------------------------------------------- /docs/method.md: -------------------------------------------------------------------------------- 1 | # Method Documentation 2 | 3 | ## Core Methods 4 | 5 | ### `prevent(option?: PreventOption)` 6 | 7 | Prevents screen capture and recording based on the provided options. 8 | 9 | > For Android, prevent specific events is not available. 10 | 11 | **Parameters:** 12 | 13 | - `option?: PreventOption` - Configuration object for preventing specific capture events 14 | ```typescript 15 | { 16 | screenshot?: boolean; // Prevent screenshots 17 | record?: boolean | IOSProtectionScreenOption; // Prevent screen recording 18 | appSwitcher?: boolean | IOSProtectionScreenOption; // Prevent app switcher capture 19 | } 20 | ``` 21 | 22 | **Example:** 23 | 24 | ```typescript 25 | // Prevent all capture events 26 | await CaptureProtection.prevent(); 27 | 28 | // Prevent specific events 29 | await CaptureProtection.prevent({ 30 | screenshot: true, 31 | record: true, 32 | appSwitcher: true, 33 | }); 34 | ``` 35 | 36 | ### `allow(option?: AllowOption)` 37 | 38 | Allows screen capture and recording based on the provided options. 39 | 40 | > For Android, allow specific events is not available. 41 | 42 | **Parameters:** 43 | 44 | - `option?: AllowOption` - Configuration object for allowing specific capture events 45 | ```typescript 46 | { 47 | screenshot?: boolean; // Allow screenshots 48 | record?: boolean; // Allow screen recording 49 | appSwitcher?: boolean; // Allow app switcher capture 50 | } 51 | ``` 52 | 53 | **Example:** 54 | 55 | ```typescript 56 | // Allow all capture events 57 | await CaptureProtection.allow(); 58 | 59 | // Allow specific events 60 | await CaptureProtection.allow({ 61 | screenshot: true, 62 | record: false, 63 | appSwitcher: true, 64 | }); 65 | ``` 66 | 67 | ### `isScreenRecording()` 68 | 69 | Checks if the screen is currently being recorded. 70 | 71 | > This behavior may not work properly on **Android** 72 | 73 | **Returns:** 74 | 75 | - `Promise` - `true` if screen is being recorded, `false` otherwise 76 | 77 | **Example:** 78 | 79 | ```typescript 80 | const isRecording = await CaptureProtection.isScreenRecording(); 81 | console.log('Is Recording:', isRecording); 82 | ``` 83 | 84 | ### `protectionStatus()` 85 | 86 | Gets the current protection status for all capture events. 87 | 88 | **Returns:** 89 | 90 | - `Promise` - Object containing protection status for each event type 91 | 92 | **Example:** 93 | 94 | ```typescript 95 | const status = await CaptureProtection.protectionStatus(); 96 | console.log('Protection Status:', status); 97 | ``` 98 | 99 | ## Hook Methods 100 | 101 | ### `useCaptureProtection()` 102 | 103 | React hook for managing capture protection state. 104 | 105 | > In order to use that hook, you need to declare a `CaptureProtectionProvider` 106 | 107 | **Returns:** 108 | 109 | ```typescript 110 | { 111 | isPrevent: boolean; // Whether any capture is prevented 112 | status: CaptureProtectionModuleStatus; // Current protection status 113 | prevent: CaptureProtectionFunction['prevent']; 114 | allow: CaptureProtectionFunction['allow']; 115 | } 116 | ``` 117 | 118 | **Example:** 119 | 120 | - `App.tsx` 121 | 122 | ```typescript 123 | return ...; 124 | ``` 125 | 126 | ```typescript 127 | const { isPrevent, status, allow, prevent } = useCaptureProtection(); 128 | ``` 129 | 130 | ### `useCaptureDetection()` 131 | 132 | React hook for managing capture protection state. 133 | 134 | **Returns:** 135 | 136 | ```typescript 137 | { 138 | isPrevent: boolean; // Whether any capture is prevented 139 | status: CaptureProtectionModuleStatus; // Current protection status 140 | } 141 | ``` 142 | 143 | **Example:** 144 | 145 | ```typescript 146 | const { isPrevent, status } = useCaptureDetection(); 147 | ``` 148 | -------------------------------------------------------------------------------- /docs/type.md: -------------------------------------------------------------------------------- 1 | # Type Documentation 2 | 3 | ## Enums 4 | 5 | ### `CaptureEventType` 6 | 7 | Enum representing different types of capture events. 8 | 9 | ```typescript 10 | enum CaptureEventType { 11 | NONE = 0, // No capture event 12 | RECORDING = 1, // Screen recording started 13 | END_RECORDING = 2, // Screen recording ended 14 | CAPTURED = 3, // Screen captured 15 | APP_SWITCHING = 4, // App switcher used 16 | UNKNOWN = 5, // Unknown event 17 | ALLOW = 8, // All capture events allowed 18 | PREVENT_SCREEN_CAPTURE = 16, // Screen capture prevented 19 | PREVENT_SCREEN_RECORDING = 32, // Screen recording prevented 20 | PREVENT_SCREEN_APP_SWITCHING = 64, // App switcher capture prevented 21 | } 22 | ``` 23 | 24 | ## Interfaces 25 | 26 | ### `IOSProtectionScreenOption` 27 | 28 | Options for iOS screen protection with custom UI. 29 | 30 | ```typescript 31 | type IOSProtectionCustomScreenOption { 32 | text: string; // Text to display 33 | textColor?: `#${string}`; // Text color in hex format, default is Black 34 | backgroundColor?: `#${string}`; // Background color in hex format, default is White 35 | } 36 | type IOSProtectionScreenOption = 37 | | { 38 | image: NodeRequire; // Image to display 39 | } 40 | | IOSProtectionCustomScreenOption; 41 | ``` 42 | 43 | ### `PreventOption` 44 | 45 | Configuration options for preventing capture events. 46 | 47 | ```typescript 48 | interface PreventOption { 49 | screenshot?: boolean; // Prevent screenshots 50 | record?: boolean | IOSProtectionScreenOption; // Prevent screen recording 51 | appSwitcher?: boolean | IOSProtectionScreenOption; // Prevent app switcher capture 52 | } 53 | ``` 54 | 55 | ### `AllowOption` 56 | 57 | Configuration options for allowing capture events. 58 | 59 | ```typescript 60 | interface AllowOption { 61 | screenshot?: boolean; // Allow screenshots 62 | record?: boolean; // Allow screen recording 63 | appSwitcher?: boolean; // Allow app switcher capture 64 | } 65 | ``` 66 | 67 | ### `CaptureProtectionModuleStatus` 68 | 69 | Status of protection for different capture events. 70 | 71 | ```typescript 72 | interface CaptureProtectionModuleStatus { 73 | screenshot: boolean; // Screenshot protection status 74 | record: boolean; // Screen recording protection status 75 | appSwitcher: boolean; // App switcher protection status 76 | } 77 | ``` 78 | 79 | ### `CaptureEventCallback` 80 | 81 | Type for the event listener callback function. 82 | 83 | ```typescript 84 | type CaptureEventCallback = (event: CaptureEventType) => void; 85 | ``` 86 | -------------------------------------------------------------------------------- /ios/CaptureProtection-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #ifndef Bridging_Header_h 2 | #define Bridging_Header_h 3 | 4 | #import 5 | #import 6 | #import 7 | #import 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import 17 | #import 18 | #import 19 | #import 20 | #import 21 | 22 | #endif /* Bridging_Header_h */ 23 | -------------------------------------------------------------------------------- /ios/CaptureProtection.m: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | #import 4 | #import 5 | #import 6 | 7 | @interface RCT_EXTERN_MODULE(CaptureProtection, RCTEventEmitter) 8 | // ScreenShot 9 | RCT_EXTERN_METHOD(allowScreenshot:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 10 | 11 | RCT_EXTERN_METHOD(preventScreenshot:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 12 | // Screen Record 13 | RCT_EXTERN_METHOD(allowScreenRecord:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 14 | 15 | RCT_EXTERN_METHOD(preventScreenRecord:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 16 | RCT_EXTERN_METHOD(preventScreenRecordWithText:(NSString *) text 17 | textColor:(NSString *) textColor 18 | backgroundColor: (NSString *) backgroundColor 19 | resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 20 | RCT_EXTERN_METHOD(preventScreenRecordWithImage:(NSDictionary*) image 21 | backgroundColor: (NSString *) backgroundColor 22 | contentMode: (double) contentMode 23 | resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 24 | 25 | // App Switcher 26 | RCT_EXTERN_METHOD(allowAppSwitcher:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 27 | 28 | RCT_EXTERN_METHOD(preventAppSwitcher:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 29 | RCT_EXTERN_METHOD(preventAppSwitcherWithText:(NSString *)text 30 | textColor:(NSString *)textColor 31 | backgroundColor: (NSString *)backgroundColor 32 | resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 33 | RCT_EXTERN_METHOD(preventAppSwitcherWithImage:(NSDictionary*) image 34 | backgroundColor: (NSString *) backgroundColor 35 | contentMode: (double) contentMode 36 | resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 37 | 38 | // Etc 39 | RCT_EXTERN_METHOD(hasListener:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 40 | RCT_EXTERN_METHOD(protectionStatus:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 41 | RCT_EXTERN_METHOD(isScreenRecording:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) 42 | @end 43 | -------------------------------------------------------------------------------- /ios/CaptureProtection.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 040D5EA62DA21C7B0014FE24 /* CaptureProtection-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = 0434F4A52D756BAA00247D8B /* CaptureProtection-Bridging-Header.h */; }; 11 | 0434F4A22D756B8300247D8B /* CaptureProtection.m in Sources */ = {isa = PBXBuildFile; fileRef = 0434F4A12D756B8300247D8B /* CaptureProtection.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXCopyFilesBuildPhase section */ 15 | 58B511D91A9E6C8500147676 /* CopyFiles */ = { 16 | isa = PBXCopyFilesBuildPhase; 17 | buildActionMask = 2147483647; 18 | dstPath = "include/$(PRODUCT_NAME)"; 19 | dstSubfolderSpec = 16; 20 | files = ( 21 | ); 22 | runOnlyForDeploymentPostprocessing = 0; 23 | }; 24 | /* End PBXCopyFilesBuildPhase section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 0434F4A12D756B8300247D8B /* CaptureProtection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CaptureProtection.m; sourceTree = ""; }; 28 | 0434F4A52D756BAA00247D8B /* CaptureProtection-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CaptureProtection-Bridging-Header.h"; sourceTree = ""; }; 29 | 134814201AA4EA6300B7C361 /* libCaptureProtection.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCaptureProtection.a; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ 33 | 040D5EA42DA21C740014FE24 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = { 34 | isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; 35 | buildPhase = 58B511D71A9E6C8500147676 /* Sources */; 36 | membershipExceptions = ( 37 | Constants, 38 | Utils, 39 | ); 40 | }; 41 | /* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ 42 | 43 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 44 | 040D5EA22DA21C2B0014FE24 /* CaptureProtection */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (040D5EA42DA21C740014FE24 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (Constants, Utils, ); path = CaptureProtection; sourceTree = ""; }; 45 | /* End PBXFileSystemSynchronizedRootGroup section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 58B511D81A9E6C8500147676 /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 134814211AA4EA7D00B7C361 /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 134814201AA4EA6300B7C361 /* libCaptureProtection.a */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | 58B511D21A9E6C8500147676 = { 67 | isa = PBXGroup; 68 | children = ( 69 | 040D5EA22DA21C2B0014FE24 /* CaptureProtection */, 70 | 0434F4A12D756B8300247D8B /* CaptureProtection.m */, 71 | 0434F4A52D756BAA00247D8B /* CaptureProtection-Bridging-Header.h */, 72 | 134814211AA4EA7D00B7C361 /* Products */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | /* End PBXGroup section */ 77 | 78 | /* Begin PBXNativeTarget section */ 79 | 58B511DA1A9E6C8500147676 /* CaptureProtection */ = { 80 | isa = PBXNativeTarget; 81 | buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "CaptureProtection" */; 82 | buildPhases = ( 83 | 58B511D71A9E6C8500147676 /* Sources */, 84 | 58B511D81A9E6C8500147676 /* Frameworks */, 85 | 58B511D91A9E6C8500147676 /* CopyFiles */, 86 | ); 87 | buildRules = ( 88 | ); 89 | dependencies = ( 90 | ); 91 | fileSystemSynchronizedGroups = ( 92 | 040D5EA22DA21C2B0014FE24 /* CaptureProtection */, 93 | ); 94 | name = CaptureProtection; 95 | productName = RCTDataManager; 96 | productReference = 134814201AA4EA6300B7C361 /* libCaptureProtection.a */; 97 | productType = "com.apple.product-type.library.static"; 98 | }; 99 | /* End PBXNativeTarget section */ 100 | 101 | /* Begin PBXProject section */ 102 | 58B511D31A9E6C8500147676 /* Project object */ = { 103 | isa = PBXProject; 104 | attributes = { 105 | LastUpgradeCheck = 0920; 106 | ORGANIZATIONNAME = Facebook; 107 | TargetAttributes = { 108 | 58B511DA1A9E6C8500147676 = { 109 | CreatedOnToolsVersion = 6.1.1; 110 | LastSwiftMigration = 1620; 111 | }; 112 | }; 113 | }; 114 | buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "CaptureProtection" */; 115 | developmentRegion = en; 116 | hasScannedForEncodings = 0; 117 | knownRegions = ( 118 | en, 119 | ); 120 | mainGroup = 58B511D21A9E6C8500147676; 121 | preferredProjectObjectVersion = 46; 122 | productRefGroup = 58B511D21A9E6C8500147676; 123 | projectDirPath = ""; 124 | projectRoot = ""; 125 | targets = ( 126 | 58B511DA1A9E6C8500147676 /* CaptureProtection */, 127 | ); 128 | }; 129 | /* End PBXProject section */ 130 | 131 | /* Begin PBXSourcesBuildPhase section */ 132 | 58B511D71A9E6C8500147676 /* Sources */ = { 133 | isa = PBXSourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | 040D5EA62DA21C7B0014FE24 /* CaptureProtection-Bridging-Header.h in Sources */, 137 | 0434F4A22D756B8300247D8B /* CaptureProtection.m in Sources */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | /* End PBXSourcesBuildPhase section */ 142 | 143 | /* Begin XCBuildConfiguration section */ 144 | 58B511ED1A9E6C8500147676 /* Debug */ = { 145 | isa = XCBuildConfiguration; 146 | buildSettings = { 147 | ALWAYS_SEARCH_USER_PATHS = NO; 148 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 149 | CLANG_CXX_LIBRARY = "libc++"; 150 | CLANG_ENABLE_MODULES = YES; 151 | CLANG_ENABLE_OBJC_ARC = YES; 152 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 153 | CLANG_WARN_BOOL_CONVERSION = YES; 154 | CLANG_WARN_COMMA = YES; 155 | CLANG_WARN_CONSTANT_CONVERSION = YES; 156 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 157 | CLANG_WARN_EMPTY_BODY = YES; 158 | CLANG_WARN_ENUM_CONVERSION = YES; 159 | CLANG_WARN_INFINITE_RECURSION = YES; 160 | CLANG_WARN_INT_CONVERSION = YES; 161 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 162 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 163 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 164 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 165 | CLANG_WARN_STRICT_PROTOTYPES = YES; 166 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 167 | CLANG_WARN_UNREACHABLE_CODE = YES; 168 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 169 | COPY_PHASE_STRIP = NO; 170 | ENABLE_STRICT_OBJC_MSGSEND = YES; 171 | ENABLE_TESTABILITY = YES; 172 | "EXCLUDED_ARCHS[sdk=*]" = arm64; 173 | GCC_C_LANGUAGE_STANDARD = gnu99; 174 | GCC_DYNAMIC_NO_PIC = NO; 175 | GCC_NO_COMMON_BLOCKS = YES; 176 | GCC_OPTIMIZATION_LEVEL = 0; 177 | GCC_PREPROCESSOR_DEFINITIONS = ( 178 | "DEBUG=1", 179 | "$(inherited)", 180 | ); 181 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 182 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 183 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 184 | GCC_WARN_UNDECLARED_SELECTOR = YES; 185 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 186 | GCC_WARN_UNUSED_FUNCTION = YES; 187 | GCC_WARN_UNUSED_VARIABLE = YES; 188 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 189 | MTL_ENABLE_DEBUG_INFO = YES; 190 | ONLY_ACTIVE_ARCH = YES; 191 | SDKROOT = iphoneos; 192 | }; 193 | name = Debug; 194 | }; 195 | 58B511EE1A9E6C8500147676 /* Release */ = { 196 | isa = XCBuildConfiguration; 197 | buildSettings = { 198 | ALWAYS_SEARCH_USER_PATHS = NO; 199 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 200 | CLANG_CXX_LIBRARY = "libc++"; 201 | CLANG_ENABLE_MODULES = YES; 202 | CLANG_ENABLE_OBJC_ARC = YES; 203 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 204 | CLANG_WARN_BOOL_CONVERSION = YES; 205 | CLANG_WARN_COMMA = YES; 206 | CLANG_WARN_CONSTANT_CONVERSION = YES; 207 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 208 | CLANG_WARN_EMPTY_BODY = YES; 209 | CLANG_WARN_ENUM_CONVERSION = YES; 210 | CLANG_WARN_INFINITE_RECURSION = YES; 211 | CLANG_WARN_INT_CONVERSION = YES; 212 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 213 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 214 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 215 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 216 | CLANG_WARN_STRICT_PROTOTYPES = YES; 217 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 218 | CLANG_WARN_UNREACHABLE_CODE = YES; 219 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 220 | COPY_PHASE_STRIP = YES; 221 | ENABLE_NS_ASSERTIONS = NO; 222 | ENABLE_STRICT_OBJC_MSGSEND = YES; 223 | "EXCLUDED_ARCHS[sdk=*]" = arm64; 224 | GCC_C_LANGUAGE_STANDARD = gnu99; 225 | GCC_NO_COMMON_BLOCKS = YES; 226 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 227 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 228 | GCC_WARN_UNDECLARED_SELECTOR = YES; 229 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 230 | GCC_WARN_UNUSED_FUNCTION = YES; 231 | GCC_WARN_UNUSED_VARIABLE = YES; 232 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 233 | MTL_ENABLE_DEBUG_INFO = NO; 234 | SDKROOT = iphoneos; 235 | VALIDATE_PRODUCT = YES; 236 | }; 237 | name = Release; 238 | }; 239 | 58B511F01A9E6C8500147676 /* Debug */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | CLANG_ENABLE_MODULES = YES; 243 | HEADER_SEARCH_PATHS = ( 244 | "$(inherited)", 245 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 246 | "$(SRCROOT)/../../../React/**", 247 | "$(SRCROOT)/../../react-native/React/**", 248 | ); 249 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 250 | OTHER_LDFLAGS = "-ObjC"; 251 | PRODUCT_NAME = CaptureProtection; 252 | SKIP_INSTALL = YES; 253 | SWIFT_OBJC_BRIDGING_HEADER = "CaptureProtection-Bridging-Header.h"; 254 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 255 | SWIFT_VERSION = 6.0; 256 | }; 257 | name = Debug; 258 | }; 259 | 58B511F11A9E6C8500147676 /* Release */ = { 260 | isa = XCBuildConfiguration; 261 | buildSettings = { 262 | CLANG_ENABLE_MODULES = YES; 263 | HEADER_SEARCH_PATHS = ( 264 | "$(inherited)", 265 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 266 | "$(SRCROOT)/../../../React/**", 267 | "$(SRCROOT)/../../react-native/React/**", 268 | ); 269 | LIBRARY_SEARCH_PATHS = "$(inherited)"; 270 | OTHER_LDFLAGS = "-ObjC"; 271 | PRODUCT_NAME = CaptureProtection; 272 | SKIP_INSTALL = YES; 273 | SWIFT_OBJC_BRIDGING_HEADER = "CaptureProtection-Bridging-Header.h"; 274 | SWIFT_VERSION = 6.0; 275 | }; 276 | name = Release; 277 | }; 278 | /* End XCBuildConfiguration section */ 279 | 280 | /* Begin XCConfigurationList section */ 281 | 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "CaptureProtection" */ = { 282 | isa = XCConfigurationList; 283 | buildConfigurations = ( 284 | 58B511ED1A9E6C8500147676 /* Debug */, 285 | 58B511EE1A9E6C8500147676 /* Release */, 286 | ); 287 | defaultConfigurationIsVisible = 0; 288 | defaultConfigurationName = Release; 289 | }; 290 | 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "CaptureProtection" */ = { 291 | isa = XCConfigurationList; 292 | buildConfigurations = ( 293 | 58B511F01A9E6C8500147676 /* Debug */, 294 | 58B511F11A9E6C8500147676 /* Release */, 295 | ); 296 | defaultConfigurationIsVisible = 0; 297 | defaultConfigurationName = Release; 298 | }; 299 | /* End XCConfigurationList section */ 300 | }; 301 | rootObject = 58B511D31A9E6C8500147676 /* Project object */; 302 | } 303 | -------------------------------------------------------------------------------- /ios/CaptureProtection/CaptureProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureProtection.swift 3 | // 4 | // 5 | // Created by lethe(wn-na, lecheln00@gmail.com) on 4/6/25. 6 | // 7 | 8 | import React 9 | import UIKit 10 | import Foundation 11 | 12 | @objc(CaptureProtection) 13 | class CaptureProtection: RCTEventEmitter { 14 | private var hasListeners = false 15 | static var config = CaptureProtectionConfig() 16 | static var protectionViewConfig = ProtectionViewConfig() 17 | private var protectorTimer: DispatchSourceTimer? 18 | 19 | override init() { 20 | super.init() 21 | addScreenshotObserver() 22 | addScreenRecordObserver() 23 | addAppSwitcherObserver() 24 | addBundleReloadObserver() 25 | } 26 | 27 | deinit { 28 | removeBundleReloadObserver() 29 | } 30 | 31 | 32 | func cancelTimer() { 33 | if let timer = protectorTimer { 34 | timer.setEventHandler {} 35 | timer.cancel() 36 | protectorTimer = nil 37 | } 38 | } 39 | 40 | // MARK: - React Native Module Function 41 | @objc(supportedEvents) 42 | override func supportedEvents() -> [String] { 43 | ["CaptureProtectionListener"] 44 | } 45 | 46 | @objc(requiresMainQueueSetup) 47 | override static func requiresMainQueueSetup() -> Bool { 48 | false 49 | } 50 | 51 | @objc(startObserving) 52 | override func startObserving() { 53 | hasListeners = true 54 | eventScreenRecordImmediate() 55 | } 56 | 57 | @objc(stopObserving) 58 | override func stopObserving() { 59 | hasListeners = false 60 | } 61 | 62 | @objc func allowScreenshot(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { 63 | secureScreenshot(isSecure: false) 64 | sendListener(status: CaptureProtection.config.protectionStatus()) 65 | resolver(true) 66 | } 67 | 68 | @objc func preventScreenshot(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { 69 | secureScreenshot(isSecure: true) 70 | sendListener(status: CaptureProtection.config.protectionStatus()) 71 | resolver(true) 72 | } 73 | 74 | @objc func allowScreenRecord(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { 75 | DispatchQueue.main.async { [self] in 76 | CaptureProtection.config.prevent.screenRecord = false 77 | removeScreenRecordView() 78 | sendListener(status: CaptureProtection.config.protectionStatus()) 79 | resolver(true) 80 | } 81 | } 82 | 83 | @objc func preventScreenRecord(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { 84 | DispatchQueue.main.async { [self] in 85 | CaptureProtection.protectionViewConfig.screenRecord.type = Constants.CaptureProtectionType.NONE 86 | eventScreenRecordImmediate(true) 87 | sendListener(status: CaptureProtection.config.protectionStatus()) 88 | resolver(true) 89 | } 90 | } 91 | 92 | @objc func preventScreenRecordWithText(_ text: String, 93 | textColor: String, 94 | backgroundColor: String, 95 | resolver: @escaping RCTPromiseResolveBlock, 96 | rejecter: @escaping RCTPromiseRejectBlock) { 97 | DispatchQueue.main.async { [self] in 98 | CaptureProtection.protectionViewConfig.screenRecord.type = Constants.CaptureProtectionType.TEXT 99 | CaptureProtection.protectionViewConfig.screenRecord.text = text 100 | CaptureProtection.protectionViewConfig.screenRecord.textColor = textColor 101 | CaptureProtection.protectionViewConfig.screenRecord.backgroundColor = backgroundColor 102 | eventScreenRecordImmediate(true) 103 | sendListener(status: CaptureProtection.config.protectionStatus()) 104 | resolver(nil) 105 | } 106 | } 107 | 108 | @objc func preventScreenRecordWithImage(_ image: NSDictionary, 109 | backgroundColor: String, 110 | contentMode: Double, 111 | resolver: @escaping RCTPromiseResolveBlock, 112 | rejecter: @escaping RCTPromiseRejectBlock) { 113 | DispatchQueue.main.async { [self] in 114 | self.eventScreenRecordImmediate(true) 115 | sendListener(status: CaptureProtection.config.protectionStatus()) 116 | 117 | do { 118 | CaptureProtection.protectionViewConfig.screenRecord.type = Constants.CaptureProtectionType.IMAGE 119 | CaptureProtection.protectionViewConfig.screenRecord.backgroundColor = backgroundColor 120 | 121 | CaptureProtection.protectionViewConfig.screenRecord.contentMode = UIView.ContentMode(rawValue: Int(contentMode)) ?? .scaleAspectFit 122 | if let screenImage = RCTConvert.uiImage(image) { 123 | CaptureProtection.protectionViewConfig.screenRecord.image = screenImage 124 | resolver(nil) 125 | } else { 126 | throw NSError(domain: "preventScreenRecordWithImage", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid image data"]) 127 | } 128 | } catch { 129 | CaptureProtection.protectionViewConfig.screenRecord.type = Constants.CaptureProtectionType.NONE 130 | rejecter("preventScreenRecordWithImage", error.localizedDescription, error) 131 | } 132 | } 133 | } 134 | 135 | @objc func allowAppSwitcher(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { 136 | CaptureProtection.config.prevent.appSwitcher = false 137 | removeAppSwitcherView() 138 | sendListener(status: CaptureProtection.config.protectionStatus()) 139 | resolver(nil) 140 | } 141 | 142 | @objc func preventAppSwitcher(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { 143 | CaptureProtection.config.prevent.appSwitcher = true 144 | CaptureProtection.protectionViewConfig.appSwitcher.type = Constants.CaptureProtectionType.NONE 145 | sendListener(status: CaptureProtection.config.protectionStatus()) 146 | resolver(nil) 147 | } 148 | 149 | @objc func preventAppSwitcherWithText(_ text: String, 150 | textColor: String, 151 | backgroundColor: String, 152 | resolver: @escaping RCTPromiseResolveBlock, 153 | rejecter: @escaping RCTPromiseRejectBlock) { 154 | CaptureProtection.config.prevent.appSwitcher = true 155 | CaptureProtection.protectionViewConfig.appSwitcher.type = Constants.CaptureProtectionType.TEXT 156 | CaptureProtection.protectionViewConfig.appSwitcher.text = text 157 | CaptureProtection.protectionViewConfig.appSwitcher.textColor = textColor 158 | CaptureProtection.protectionViewConfig.appSwitcher.backgroundColor = backgroundColor 159 | sendListener(status: CaptureProtection.config.protectionStatus()) 160 | resolver(nil) 161 | } 162 | 163 | @objc func preventAppSwitcherWithImage(_ image: NSDictionary, 164 | backgroundColor: String, 165 | contentMode: Double, 166 | resolver: @escaping RCTPromiseResolveBlock, 167 | rejecter: @escaping RCTPromiseRejectBlock) { 168 | DispatchQueue.main.async { [self] in 169 | CaptureProtection.config.prevent.appSwitcher = true 170 | sendListener(status: CaptureProtection.config.protectionStatus()) 171 | 172 | do { 173 | CaptureProtection.protectionViewConfig.appSwitcher.type = Constants.CaptureProtectionType.IMAGE 174 | CaptureProtection.protectionViewConfig.appSwitcher.backgroundColor = backgroundColor 175 | CaptureProtection.protectionViewConfig.appSwitcher.contentMode = UIView.ContentMode(rawValue: Int(contentMode)) ?? .scaleAspectFit 176 | if let screenImage = RCTConvert.uiImage(image) { 177 | CaptureProtection.protectionViewConfig.appSwitcher.image = screenImage 178 | resolver(nil) 179 | } else { 180 | throw NSError(domain: "preventAppSwitcherWithImage", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid image data"]) 181 | } 182 | } catch { 183 | CaptureProtection.protectionViewConfig.appSwitcher.type = Constants.CaptureProtectionType.NONE 184 | rejecter("preventAppSwitcherWithImage", error.localizedDescription, error) 185 | } 186 | } 187 | } 188 | 189 | @objc func hasListener(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { 190 | resolver(hasListeners) 191 | } 192 | 193 | @objc func protectionStatus(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { 194 | resolver([ 195 | "screenshot": CaptureProtection.config.prevent.screenshot, 196 | "record": CaptureProtection.config.prevent.screenRecord, 197 | "appSwitcher": CaptureProtection.config.prevent.appSwitcher 198 | ]) 199 | } 200 | 201 | @objc func isScreenRecording(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { 202 | if let isCaptured = UIScreen.main.value(forKey: "isCaptured") as? Bool { 203 | resolver(isCaptured) 204 | } else { 205 | rejecter("isScreenRecording", "Failed to get screen recording status", nil) 206 | } 207 | } 208 | 209 | // MARK: - Send Event Listener 210 | func sendListener(status: Int) { 211 | if hasListeners { 212 | self.sendEvent(withName: "CaptureProtectionListener", body: status) 213 | } 214 | } 215 | 216 | func triggerEvent(_ event: Constants.CaptureEventType) { 217 | if hasListeners { 218 | sendListener(status: event.rawValue) 219 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { [self] in 220 | if let isCaptured = UIScreen.main.value(forKey: "isCaptured") as? Bool, isCaptured == true { 221 | sendListener(status: Constants.CaptureEventType.RECORDING.rawValue) 222 | } else { 223 | sendListener(status: Constants.CaptureEventType.UNKNOWN.rawValue) 224 | } 225 | } 226 | } 227 | } 228 | 229 | @objc func eventScreenshot(notification: Notification) { 230 | self.triggerEvent(Constants.CaptureEventType.CAPTURED) 231 | } 232 | 233 | @objc func eventScreenRecord(notification: Notification, isEvent: Bool = false) { 234 | if let isCaptured = UIScreen.main.value(forKey: "isCaptured") as? Bool { 235 | if isCaptured { 236 | if CaptureProtection.config.prevent.screenRecord { 237 | secureScreenRecord() 238 | } 239 | sendListener(status: Constants.CaptureEventType.RECORDING.rawValue) 240 | } else { 241 | removeScreenRecordView() 242 | if !isEvent { 243 | triggerEvent(Constants.CaptureEventType.END_RECORDING) 244 | } 245 | } 246 | } 247 | } 248 | 249 | func eventScreenRecordImmediate(_ prevent: Bool = false) { 250 | if (prevent) { 251 | CaptureProtection.config.prevent.screenRecord = true 252 | } 253 | eventScreenRecord(notification: Notification(name: Notification.Name("Init")), isEvent: true) 254 | } 255 | 256 | // MARK: - Observer 257 | private func addScreenshotObserver() { 258 | guard !CaptureProtection.config.observer.screenshot else { return } 259 | CaptureProtection.config.observer.screenshot = true 260 | NotificationCenter.default.addObserver(self, selector: #selector(eventScreenshot(notification:)), name: UIApplication.userDidTakeScreenshotNotification, object: nil) 261 | } 262 | 263 | private func removeScreenshotObserver() { 264 | guard CaptureProtection.config.observer.screenshot else { return } 265 | CaptureProtection.config.observer.screenshot = false 266 | NotificationCenter.default.removeObserver(self, name: UIApplication.userDidTakeScreenshotNotification, object: nil) 267 | } 268 | 269 | private func addScreenRecordObserver() { 270 | guard !CaptureProtection.config.observer.screenRecord else { return } 271 | CaptureProtection.config.observer.screenRecord = true 272 | NotificationCenter.default.addObserver(self, selector: #selector(eventScreenRecord), name: UIScreen.capturedDidChangeNotification, object: nil) 273 | } 274 | 275 | private func removeScreenRecordObserver() { 276 | guard CaptureProtection.config.observer.screenRecord else { return } 277 | CaptureProtection.config.observer.screenRecord = false 278 | NotificationCenter.default.removeObserver(self, name: UIScreen.capturedDidChangeNotification, object: nil) 279 | } 280 | 281 | private func addAppSwitcherObserver() { 282 | guard !CaptureProtection.config.observer.appSwitcher else { return } 283 | CaptureProtection.config.observer.appSwitcher = true 284 | NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in 285 | DispatchQueue.main.async { 286 | self?.secureAppSwitcher() 287 | } 288 | } 289 | 290 | NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in 291 | DispatchQueue.main.async { 292 | self?.secureAppSwitcher() 293 | } 294 | } 295 | 296 | NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in 297 | DispatchQueue.main.async { 298 | self?.removeAppSwitcherView() 299 | } 300 | } 301 | 302 | NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { [weak self] _ in 303 | DispatchQueue.main.async { 304 | self?.removeAppSwitcherView() 305 | } 306 | } 307 | } 308 | 309 | private func removeBackgroundObserver() { 310 | guard CaptureProtection.config.observer.appSwitcher else { return } 311 | CaptureProtection.config.observer.appSwitcher = false 312 | DispatchQueue.main.async { 313 | self.removeAppSwitcherView() 314 | } 315 | NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) 316 | NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) 317 | NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) 318 | NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) 319 | } 320 | 321 | private func addBundleReloadObserver() { 322 | NotificationCenter.default.addObserver(forName: NSNotification.Name.RCTTriggerReloadCommand, object: nil, queue: .main) { [weak self] _ in 323 | DispatchQueue.main.async { 324 | self?.cancelTimer() 325 | if let secureTextField = CaptureProtection.protectionViewConfig.secureTextField { 326 | secureTextField.isSecureTextEntry = false 327 | } 328 | 329 | self?.secureScreenshot(isSecure: false) 330 | self?.removeScreenshotObserver() 331 | self?.removeScreenRecordObserver() 332 | self?.removeBackgroundObserver() 333 | self?.removeBundleReloadObserver() 334 | } 335 | } 336 | } 337 | 338 | private func removeBundleReloadObserver() { 339 | NotificationCenter.default.removeObserver(self, name: NSNotification.Name.RCTTriggerReloadCommand, object: nil) 340 | } 341 | // MARK: - Protection UI with ScreenShot 342 | @objc func secureScreenshot(isSecure: Bool) { 343 | CaptureProtection.config.prevent.screenshot = isSecure 344 | if isSecure == false { 345 | DispatchQueue.main.async { 346 | CaptureProtection.protectionViewConfig.secureTextField?.isSecureTextEntry = false 347 | } 348 | return 349 | } else { 350 | if CaptureProtection.protectionViewConfig.secureTextField == nil { 351 | DispatchQueue.main.async { 352 | if CaptureProtection.protectionViewConfig.secureTextField == nil { 353 | CaptureProtection.protectionViewConfig.secureTextField = UITextField.init() 354 | CaptureProtection.protectionViewConfig.secureTextField!.isUserInteractionEnabled = false 355 | CaptureProtection.protectionViewConfig.secureTextField!.tag = Constants.TAG_SCREENSHOT_PROTECTION 356 | CaptureProtection.protectionViewConfig.secureTextField!.isSecureTextEntry = true 357 | if let window = UIApplication.shared.delegate?.window { 358 | window?.makeKeyAndVisible() 359 | 360 | window?.layer.superlayer?.addSublayer(CaptureProtection.protectionViewConfig.secureTextField!.layer) 361 | CaptureProtection.protectionViewConfig.secureTextField?.layer.sublayers?.first?.addSublayer(window!.layer) 362 | CaptureProtection.protectionViewConfig.secureTextField?.layer.sublayers?.last?.addSublayer(window!.layer) 363 | } 364 | } 365 | } 366 | } else { 367 | DispatchQueue.main.async { 368 | CaptureProtection.protectionViewConfig.secureTextField?.isSecureTextEntry = true 369 | } 370 | } 371 | } 372 | } 373 | 374 | // MARK: - Protection UI with ScreenRecord 375 | private func secureScreenRecord() { 376 | removeScreenRecordView() 377 | if CaptureProtection.config.prevent.screenRecord { 378 | DispatchQueue.main.async { 379 | let config = CaptureProtection.protectionViewConfig.screenRecord 380 | if config.type == Constants.CaptureProtectionType.TEXT { 381 | CaptureProtection.protectionViewConfig.screenRecord.viewController = UIViewUtils.textView( 382 | tag: Constants.TAG_RECORD_PROTECTION_SCREEN, 383 | text: config.text!, 384 | textColor: config.textColor, 385 | backgroundColor: config.backgroundColor) 386 | } else if config.type == Constants.CaptureProtectionType.IMAGE { 387 | CaptureProtection.protectionViewConfig.screenRecord.viewController = UIViewUtils.imageView( 388 | tag: Constants.TAG_RECORD_PROTECTION_SCREEN, 389 | image: config.image!, 390 | backgroundColor: config.backgroundColor, 391 | contentMode: config.contentMode) 392 | } else { 393 | CaptureProtection.protectionViewConfig.screenRecord.viewController = UIViewUtils.view( 394 | tag: Constants.TAG_RECORD_PROTECTION_SCREEN, 395 | backgroundColor: config.backgroundColor 396 | ) 397 | } 398 | 399 | let protectionWindow = UIWindow(frame: UIScreen.main.bounds) 400 | protectionWindow.windowLevel = .alert + 1 401 | protectionWindow.backgroundColor = .clear 402 | protectionWindow.rootViewController = CaptureProtection.protectionViewConfig.screenRecord.viewController 403 | protectionWindow.makeKeyAndVisible() 404 | CaptureProtection.protectionViewConfig.screenRecord.window = protectionWindow 405 | } 406 | } 407 | } 408 | 409 | private func removeScreenRecordView() { 410 | CaptureProtection.protectionViewConfig.screenRecord.window?.isHidden = true 411 | CaptureProtection.protectionViewConfig.screenRecord.window = nil 412 | } 413 | 414 | // MARK: - Protection UI with App Swither 415 | private func secureAppSwitcher() { 416 | removeAppSwitcherView() { [self] in 417 | if CaptureProtection.config.prevent.appSwitcher { 418 | protectorTimer = DispatchSource.makeTimerSource(queue: DispatchQueue.main) 419 | protectorTimer!.schedule(deadline: .now() + 0.05) 420 | protectorTimer!.setEventHandler { 421 | DispatchQueue.main.async { 422 | let config = CaptureProtection.protectionViewConfig.appSwitcher 423 | if config.type == Constants.CaptureProtectionType.TEXT { 424 | CaptureProtection.protectionViewConfig.appSwitcher.viewController = UIViewUtils.textView(tag: Constants.TAG_APP_SWITCHER_PROTECTION, text: config.text!, textColor: config.textColor, backgroundColor: config.backgroundColor) 425 | } else if config.type == Constants.CaptureProtectionType.IMAGE { 426 | CaptureProtection.protectionViewConfig.appSwitcher.viewController = UIViewUtils.imageView(tag: Constants.TAG_APP_SWITCHER_PROTECTION, image: config.image!, backgroundColor: config.backgroundColor, contentMode: config.contentMode) 427 | } else { 428 | CaptureProtection.protectionViewConfig.appSwitcher.viewController = UIViewUtils.view( 429 | tag: Constants.TAG_APP_SWITCHER_PROTECTION, 430 | backgroundColor: config.backgroundColor 431 | ) 432 | } 433 | 434 | let protectionWindow = UIWindow(frame: UIScreen.main.bounds) 435 | protectionWindow.windowLevel = .alert + 1 436 | protectionWindow.backgroundColor = .clear 437 | protectionWindow.rootViewController = CaptureProtection.protectionViewConfig.appSwitcher.viewController 438 | protectionWindow.makeKeyAndVisible() 439 | CaptureProtection.protectionViewConfig.appSwitcher.window = protectionWindow 440 | } 441 | } 442 | protectorTimer!.resume() 443 | } 444 | } 445 | } 446 | 447 | private func removeAppSwitcherView(completion: (() -> Void)? = nil) { 448 | self.cancelTimer() 449 | CaptureProtection.protectionViewConfig.appSwitcher.window?.isHidden = true 450 | CaptureProtection.protectionViewConfig.appSwitcher.window = nil 451 | completion?() 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /ios/CaptureProtection/Constants/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // 4 | // 5 | // Created by lethe(wn-na, lecheln00@gmail.com) on 4/6/25. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public struct ProtectionConfig { 12 | public var screenshot: Bool = false 13 | public var screenRecord: Bool = false 14 | public var appSwitcher: Bool = false 15 | } 16 | 17 | public class CaptureProtectionConfig { 18 | public var prevent = ProtectionConfig() 19 | public var observer = ProtectionConfig() 20 | public func protectionStatus() -> Int { 21 | let result = 22 | (prevent.screenshot ? Constants.CaptureEventType.PREVENT_SCREEN_CAPTURE.rawValue : 0) 23 | + (prevent.screenRecord ? Constants.CaptureEventType.PREVENT_SCREEN_RECORDING.rawValue : 0) 24 | + (prevent.appSwitcher ? Constants.CaptureEventType.PREVENT_SCREEN_APP_SWITCHING.rawValue : 0) 25 | 26 | if result == 0 { 27 | return Constants.CaptureEventType.ALLOW.rawValue 28 | } 29 | return result 30 | } 31 | } 32 | 33 | public struct ProtectorViewOption { 34 | public var viewController: UIViewController? 35 | public var text: String? 36 | public var textColor: String = "#000000" 37 | public var backgroundColor: String = "#FFFFFF" 38 | public var image: UIImage? 39 | public var type: Constants.CaptureProtectionType = Constants.CaptureProtectionType.NONE 40 | public var contentMode: UIView.ContentMode = .scaleAspectFit 41 | public var window: UIWindow? 42 | } 43 | 44 | public class ProtectionViewConfig { 45 | public var secureTextField: UITextField? 46 | public var screenRecord = ProtectorViewOption() 47 | public var appSwitcher = ProtectorViewOption() 48 | } 49 | 50 | -------------------------------------------------------------------------------- /ios/CaptureProtection/Constants/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by lethe(wn-na, lecheln00@gmail.com) on 3/3/25. 3 | // Copyright © 2025 Facebook. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | public class Constants { 9 | public enum CaptureEventType: Int { 10 | case NONE = 0 11 | case RECORDING = 1 12 | case END_RECORDING = 2 13 | case CAPTURED = 3 14 | case APP_SWITCHING = 4 15 | case UNKNOWN = 5 16 | case ALLOW = 8 17 | case PREVENT_SCREEN_CAPTURE = 16 18 | case PREVENT_SCREEN_RECORDING = 32 19 | case PREVENT_SCREEN_APP_SWITCHING = 64 20 | } 21 | 22 | public enum CaptureProtectionType: Int { 23 | case NONE = 0 24 | case TEXT = 1 25 | case IMAGE = 2 26 | } 27 | 28 | static let TAG_RECORD_PROTECTION_SCREEN = -1002 29 | static let TAG_SCREENSHOT_PROTECTION = -1004 30 | static let TAG_APP_SWITCHER_PROTECTION = -1005 31 | } 32 | -------------------------------------------------------------------------------- /ios/CaptureProtection/Utils/TextUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextUtils.swift 3 | // CaptureProtection 4 | // 5 | // Created by lethe(wn-na, lecheln00@gmail.com) on 3/3/25. 6 | // Copyright © 2025 Facebook. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | 12 | public class TextUtils { 13 | public static func colorFromHexString(hexString: String, defaultColor: UIColor = .black) -> UIColor { 14 | var hex = hexString.trimmingCharacters(in: .whitespacesAndNewlines) 15 | 16 | if hex.hasPrefix("#") { 17 | hex.removeFirst() 18 | } 19 | 20 | guard hex.count == 6, let rgbValue = UInt64(hex, radix: 16) else { 21 | return defaultColor 22 | } 23 | 24 | let red = CGFloat((rgbValue >> 16) & 0xFF) / 255.0 25 | let green = CGFloat((rgbValue >> 8) & 0xFF) / 255.0 26 | let blue = CGFloat(rgbValue & 0xFF) / 255.0 27 | 28 | return UIColor(red: red, green: green, blue: blue, alpha: 1.0) 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /ios/CaptureProtection/Utils/UIViewUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // 4 | // 5 | // Created by lethe(wn-na, lecheln00@gmail.com) on 4/6/25. 6 | // 7 | 8 | import UIKit 9 | import Foundation 10 | 11 | public class UIViewUtils { 12 | public static func imageView(tag: Int, image: UIImage, backgroundColor: String, contentMode: UIView.ContentMode) -> UIViewController { 13 | guard let window = UIApplication.shared.delegate?.window ?? nil else { return UIViewController() } 14 | 15 | let viewController = UIViewController() 16 | viewController.view.tag = tag 17 | 18 | let imageView = UIImageView(image: image) 19 | imageView.frame = window.frame 20 | imageView.clipsToBounds = true 21 | imageView.contentMode = contentMode 22 | 23 | viewController.view.addSubview(imageView) 24 | viewController.view.backgroundColor = TextUtils.colorFromHexString(hexString: backgroundColor, defaultColor: .white) 25 | 26 | return viewController 27 | } 28 | 29 | public static func textView(tag: Int, text: String, 30 | textColor: String, 31 | backgroundColor: String) -> UIViewController { 32 | guard let window = UIApplication.shared.delegate?.window ?? nil else { return UIViewController() } 33 | 34 | let viewController = UIViewController() 35 | viewController.view.tag = tag 36 | viewController.view.backgroundColor = TextUtils.colorFromHexString(hexString: backgroundColor, defaultColor: .white) 37 | 38 | let label = UILabel() 39 | label.textAlignment = .center 40 | label.textColor = TextUtils.colorFromHexString(hexString: textColor) 41 | label.isUserInteractionEnabled = false 42 | label.text = text 43 | label.frame = window.frame 44 | 45 | viewController.view.addSubview(label) 46 | return viewController 47 | } 48 | 49 | public static func view(tag: Int, backgroundColor: String) -> UIViewController { 50 | guard (UIApplication.shared.delegate?.window ?? nil) != nil else { return UIViewController() } 51 | 52 | let viewController = UIViewController() 53 | viewController.view.tag = tag 54 | viewController.view.backgroundColor = TextUtils.colorFromHexString(hexString: backgroundColor, defaultColor: .white) 55 | return viewController 56 | } 57 | 58 | public static func remove(tag: Int) { 59 | DispatchQueue.main.async { 60 | guard let window = UIApplication.shared.delegate?.window else { return } 61 | if let existingViewController = window?.viewWithTag(tag)?.next as? UIViewController { 62 | existingViewController.willMove(toParent: nil) 63 | existingViewController.view.removeFromSuperview() 64 | existingViewController.removeFromParent() 65 | } 66 | } 67 | } 68 | 69 | public static func remove(viewController: UIViewController?, completion: (() -> Void)? = nil) { 70 | DispatchQueue.main.async { 71 | if let existingViewController = viewController { 72 | existingViewController.willMove(toParent: nil) 73 | existingViewController.view.removeFromSuperview() 74 | existingViewController.removeFromParent() 75 | } 76 | completion?() 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | files: git diff --name-only @{push} 6 | glob: "*.{js,ts,jsx,tsx}" 7 | run: npx eslint {files} 8 | types: 9 | files: git diff --name-only @{push} 10 | glob: "*.{js,ts, jsx, tsx}" 11 | run: npx tsc --noEmit 12 | commit-msg: 13 | parallel: true 14 | commands: 15 | commitlint: 16 | run: npx commitlint --edit 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-capture-protection", 3 | "version": "2.0.7", 4 | "description": "🛡️ A React Native library to prevent and detect for screen capture, screenshots and app switcher for enhanced security. Fully compatible with both Expo and CLI.", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "*.podspec", 17 | "!lib/typescript/example", 18 | "!ios/build", 19 | "!android/build", 20 | "!android/gradle", 21 | "!android/gradlew", 22 | "!android/gradlew.bat", 23 | "!android/local.properties", 24 | "!**/__tests__", 25 | "!**/__fixtures__", 26 | "!**/__mocks__", 27 | "!**/.*" 28 | ], 29 | "scripts": { 30 | "test": "jest", 31 | "typescript": "tsc --noEmit", 32 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 33 | "prepare": "bob build", 34 | "release": "release-it", 35 | "example": "yarn --cwd example", 36 | "bootstrap": "yarn example && yarn install && yarn example pods", 37 | "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build" 38 | }, 39 | "keywords": [ 40 | "react-native", 41 | "ios", 42 | "android" 43 | ], 44 | "repository": "https://github.com/wn-na/react-native-capture-protection", 45 | "author": "lethe (https://github.com/wn-na)", 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/wn-na/react-native-capture-protection/issues" 49 | }, 50 | "homepage": "https://github.com/wn-na/react-native-capture-protection#readme", 51 | "publishConfig": { 52 | "registry": "https://registry.npmjs.org/" 53 | }, 54 | "devDependencies": { 55 | "@arkweid/lefthook": "^0.7.7", 56 | "@commitlint/config-conventional": "^17.0.2", 57 | "@react-native-community/eslint-config": "^3.0.2", 58 | "@release-it/conventional-changelog": "^5.0.0", 59 | "@types/jest": "^28.1.2", 60 | "@types/react": "~17.0.21", 61 | "@types/react-native": "0.70.0", 62 | "commitlint": "^17.0.2", 63 | "del-cli": "^5.0.0", 64 | "eslint": "^8.4.1", 65 | "eslint-config-prettier": "^8.5.0", 66 | "eslint-plugin-prettier": "^4.0.0", 67 | "jest": "^28.1.1", 68 | "pod-install": "^0.1.0", 69 | "prettier": "^2.0.5", 70 | "react": "18.1.0", 71 | "react-native": "0.70.6", 72 | "react-native-builder-bob": "^0.20.1", 73 | "release-it": "^15.0.0", 74 | "typescript": "^4.5.2" 75 | }, 76 | "resolutions": { 77 | "@types/react": "17.0.21" 78 | }, 79 | "peerDependencies": { 80 | "react": "*", 81 | "react-native": "*" 82 | }, 83 | "engines": { 84 | "node": ">= 16.0.0" 85 | }, 86 | "packageManager": "^yarn@1.22.21", 87 | "jest": { 88 | "preset": "react-native", 89 | "modulePathIgnorePatterns": [ 90 | "/example/node_modules", 91 | "/lib/" 92 | ] 93 | }, 94 | "commitlint": { 95 | "extends": [ 96 | "@commitlint/config-conventional" 97 | ] 98 | }, 99 | "release-it": { 100 | "git": { 101 | "commitMessage": "chore: release ${version}", 102 | "tagName": "v${version}" 103 | }, 104 | "npm": { 105 | "publish": true 106 | }, 107 | "github": { 108 | "release": true 109 | }, 110 | "plugins": { 111 | "@release-it/conventional-changelog": { 112 | "preset": "angular" 113 | } 114 | } 115 | }, 116 | "eslintConfig": { 117 | "root": true, 118 | "extends": [ 119 | "@react-native-community", 120 | "prettier" 121 | ], 122 | "rules": { 123 | "prettier/prettier": [ 124 | "error", 125 | { 126 | "quoteProps": "consistent", 127 | "singleQuote": true, 128 | "tabWidth": 2, 129 | "trailingComma": "es5", 130 | "useTabs": false 131 | } 132 | ] 133 | } 134 | }, 135 | "eslintIgnore": [ 136 | "node_modules/", 137 | "lib/" 138 | ], 139 | "prettier": { 140 | "quoteProps": "consistent", 141 | "singleQuote": true, 142 | "tabWidth": 2, 143 | "trailingComma": "es5", 144 | "useTabs": false 145 | }, 146 | "react-native-builder-bob": { 147 | "source": "src", 148 | "output": "lib", 149 | "targets": [ 150 | "commonjs", 151 | "module", 152 | [ 153 | "typescript", 154 | { 155 | "project": "tsconfig.build.json" 156 | } 157 | ] 158 | ] 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /react-native-capture-protection.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 5 | 6 | Pod::Spec.new do |s| 7 | s.name = "react-native-capture-protection" 8 | s.version = package["version"] 9 | s.summary = package["description"] 10 | s.homepage = package["homepage"] 11 | s.license = package["license"] 12 | s.authors = package["author"] 13 | 14 | s.platforms = { :ios => "13.0" } 15 | s.source = { :git => "https://github.com/0xlethe/react-native-capture-protection.git", :tag => "#{s.version}" } 16 | 17 | s.source_files = "ios/**/*.{h,m,mm,swift}" 18 | 19 | s.dependency "React-Core" 20 | 21 | # Don't install the dependencies when we run `pod install` in the old architecture. 22 | if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then 23 | s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" 24 | s.pod_target_xcconfig = { 25 | "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", 26 | "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", 27 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" 28 | } 29 | s.dependency "React-Codegen" 30 | s.dependency "RCT-Folly" 31 | s.dependency "RCTRequired" 32 | s.dependency "RCTTypeSafety" 33 | s.dependency "ReactCommon/turbomodule/core" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /src/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { CaptureProtection } from './modules'; 3 | import { 4 | CaptureDetectionHookType, 5 | CaptureEventType, 6 | CaptureProtectionModuleStatus, 7 | } from './type'; 8 | 9 | const useCaptureDetection = (): CaptureDetectionHookType => { 10 | const [status, setStatus] = useState(CaptureEventType.NONE); 11 | const [protectionStatus, setProtectionStatus] = 12 | useState({ 13 | screenshot: false, 14 | record: false, 15 | appSwitcher: false, 16 | }); 17 | 18 | useEffect(() => { 19 | const listener = CaptureProtection.addListener((eventType) => { 20 | if (eventType < CaptureEventType.ALLOW) { 21 | setStatus(eventType); 22 | } else if (eventType === CaptureEventType.ALLOW) { 23 | setProtectionStatus((_) => ({ 24 | screenshot: false, 25 | record: false, 26 | appSwitcher: false, 27 | })); 28 | } else if (eventType > CaptureEventType.ALLOW) { 29 | setProtectionStatus((prev) => ({ 30 | ...prev, 31 | screenshot: !!(eventType & CaptureEventType.PREVENT_SCREEN_CAPTURE), 32 | record: !!(eventType & CaptureEventType.PREVENT_SCREEN_RECORDING), 33 | appSwitcher: !!( 34 | eventType & CaptureEventType.PREVENT_SCREEN_APP_SWITCHING 35 | ), 36 | })); 37 | } 38 | }); 39 | return () => { 40 | listener?.remove?.(); 41 | }; 42 | }, []); 43 | 44 | return { status, protectionStatus }; 45 | }; 46 | 47 | export { useCaptureDetection }; 48 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './modules'; 3 | export * from './providers'; 4 | export * from './type'; 5 | -------------------------------------------------------------------------------- /src/modules.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Image, 3 | NativeEventEmitter, 4 | NativeModules, 5 | Platform, 6 | } from 'react-native'; 7 | import { 8 | CaptureProtectionAndroidNativeModules, 9 | CaptureProtectionFunction, 10 | CaptureProtectionIOSNativeModules, 11 | ContentMode, 12 | } from './type'; 13 | 14 | const CaptureProtectionModule = NativeModules?.CaptureProtection ?? {}; 15 | 16 | const CaptureProtectionAndroidModule = 17 | CaptureProtectionModule as CaptureProtectionAndroidNativeModules; 18 | const CaptureProtectionIOSModule = 19 | CaptureProtectionModule as CaptureProtectionIOSNativeModules; 20 | 21 | const isPlatformSupported = Platform.OS === 'ios' || Platform.OS === 'android'; 22 | 23 | const CaptureNotificationEmitter = isPlatformSupported 24 | ? new NativeEventEmitter(CaptureProtectionModule) 25 | : undefined; 26 | 27 | const CaptureProtectionEventType = 'CaptureProtectionListener' as const; 28 | 29 | const allow: CaptureProtectionFunction['allow'] = async (option) => { 30 | if (Platform.OS === 'android') { 31 | return await CaptureProtectionAndroidModule?.allow?.(); 32 | } 33 | if (Platform.OS === 'ios') { 34 | const { 35 | record = false, 36 | appSwitcher = false, 37 | screenshot = false, 38 | } = option ?? { 39 | record: true, 40 | appSwitcher: true, 41 | screenshot: true, 42 | }; 43 | 44 | if (screenshot) { 45 | await CaptureProtectionIOSModule?.allowScreenshot?.(); 46 | } 47 | 48 | if (appSwitcher) { 49 | await CaptureProtectionIOSModule?.allowAppSwitcher?.(); 50 | } 51 | 52 | if (record) { 53 | await CaptureProtectionIOSModule?.allowScreenRecord?.(); 54 | } 55 | } 56 | }; 57 | 58 | const prevent: CaptureProtectionFunction['prevent'] = async (option) => { 59 | if (Platform.OS === 'android') { 60 | return await CaptureProtectionAndroidModule?.prevent?.(); 61 | } 62 | if (Platform.OS === 'ios') { 63 | const { 64 | record = false, 65 | appSwitcher = false, 66 | screenshot = false, 67 | } = option ?? { 68 | record: true, 69 | appSwitcher: true, 70 | screenshot: true, 71 | }; 72 | 73 | if (screenshot) { 74 | await CaptureProtectionIOSModule?.preventScreenshot?.(); 75 | } 76 | 77 | if (appSwitcher) { 78 | if (typeof appSwitcher === 'boolean') { 79 | await CaptureProtectionIOSModule?.preventAppSwitcher?.(); 80 | } else if (typeof appSwitcher === 'object') { 81 | if ('image' in appSwitcher) { 82 | await CaptureProtectionIOSModule?.preventAppSwitcherWithImage?.( 83 | Image.resolveAssetSource(appSwitcher.image as unknown as number), 84 | appSwitcher?.backgroundColor ?? "#ffffff", 85 | appSwitcher?.contentMode ?? ContentMode.scaleAspectFit 86 | ); 87 | } else { 88 | await CaptureProtectionIOSModule?.preventAppSwitcherWithText?.( 89 | appSwitcher.text, 90 | appSwitcher?.textColor, 91 | appSwitcher?.backgroundColor 92 | ); 93 | } 94 | } 95 | } 96 | 97 | if (record) { 98 | if (typeof record === 'boolean') { 99 | await CaptureProtectionIOSModule?.preventScreenRecord?.(); 100 | } else if (typeof record === 'object') { 101 | if ('image' in record) { 102 | await CaptureProtectionIOSModule?.preventScreenRecordWithImage?.( 103 | Image.resolveAssetSource(record.image as unknown as number), 104 | record?.backgroundColor ?? "#ffffff", 105 | record?.contentMode?? ContentMode.scaleAspectFit 106 | ); 107 | } else { 108 | await CaptureProtectionIOSModule?.preventScreenRecordWithText?.( 109 | record.text, 110 | record?.textColor, 111 | record?.backgroundColor 112 | ); 113 | } 114 | } 115 | } 116 | } 117 | }; 118 | 119 | const protectionStatus: CaptureProtectionFunction['protectionStatus'] = 120 | async () => { 121 | if (Platform.OS === 'android') { 122 | const status = await CaptureProtectionAndroidModule?.protectionStatus?.(); 123 | return { 124 | record: status, 125 | appSwitcher: status, 126 | screenshot: status, 127 | }; 128 | } 129 | if (Platform.OS === 'ios') { 130 | return await CaptureProtectionIOSModule?.protectionStatus?.(); 131 | } 132 | return { record: undefined, appSwitcher: undefined, screenshot: undefined }; 133 | }; 134 | 135 | const hasListener: CaptureProtectionFunction['hasListener'] = async () => { 136 | if (Platform.OS === 'android') { 137 | return await CaptureProtectionAndroidModule?.hasListener?.(); 138 | } 139 | if (Platform.OS === 'ios') { 140 | return await CaptureProtectionIOSModule?.hasListener?.(); 141 | } 142 | return undefined; 143 | }; 144 | 145 | const addListener: CaptureProtectionFunction['addListener'] = (callback) => { 146 | if (!isPlatformSupported) { 147 | return; 148 | } 149 | return CaptureNotificationEmitter?.addListener?.( 150 | CaptureProtectionEventType, 151 | callback 152 | ); 153 | }; 154 | 155 | const removeListener: CaptureProtectionFunction['removeListener'] = async ( 156 | emitter 157 | ) => { 158 | if (!isPlatformSupported) { 159 | return; 160 | } 161 | if (emitter) { 162 | emitter.remove(); 163 | } 164 | }; 165 | 166 | const isScreenRecording: CaptureProtectionFunction['isScreenRecording'] = 167 | async () => { 168 | if (Platform.OS === 'android') { 169 | return await CaptureProtectionAndroidModule?.isScreenRecording?.(); 170 | } 171 | if (Platform.OS === 'ios') { 172 | return await CaptureProtectionIOSModule?.isScreenRecording?.(); 173 | } 174 | return undefined; 175 | }; 176 | 177 | const requestPermission: CaptureProtectionFunction['requestPermission'] = 178 | async () => { 179 | if (Platform.OS !== 'android') { 180 | console.warn( 181 | '[react-native-capture-protection] requestPermission is only available on Android' 182 | ); 183 | return false; 184 | } 185 | 186 | try { 187 | return await CaptureProtectionAndroidModule?.requestPermission?.(); 188 | } catch (e) { 189 | console.error( 190 | '[react-native-capture-protection] requestPermission throw error', 191 | e 192 | ); 193 | return false; 194 | } 195 | }; 196 | 197 | export const CaptureProtection: CaptureProtectionFunction = { 198 | addListener, 199 | hasListener, 200 | isScreenRecording, 201 | requestPermission, 202 | allow, 203 | prevent, 204 | removeListener, 205 | protectionStatus, 206 | }; 207 | -------------------------------------------------------------------------------- /src/providers.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, PropsWithChildren, useContext } from 'react'; 2 | import { useCaptureDetection } from './hooks'; 3 | import { CaptureProtection } from './modules'; 4 | import { 5 | AllowOption, 6 | CaptureEventType, 7 | CaptureProtectionContextType, 8 | PreventOption, 9 | } from './type'; 10 | 11 | const CaptureProtectionContext = createContext({ 12 | protectionStatus: { screenshot: false, record: false, appSwitcher: false }, 13 | status: CaptureEventType.NONE, 14 | prevent: async () => undefined, 15 | allow: async () => undefined, 16 | }); 17 | 18 | const CaptureProtectionProvider = ({ children }: PropsWithChildren<{}>) => { 19 | const { protectionStatus, status } = useCaptureDetection(); 20 | 21 | const prevent = async (option?: PreventOption) => { 22 | CaptureProtection.prevent(option); 23 | }; 24 | 25 | const allow = async (allowOption?: AllowOption) => { 26 | CaptureProtection.allow(allowOption); 27 | }; 28 | 29 | return ( 30 | 33 | <>{children} 34 | 35 | ); 36 | }; 37 | 38 | const useCaptureProtection = () => useContext(CaptureProtectionContext); 39 | 40 | export { CaptureProtectionProvider, useCaptureProtection }; 41 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import { EmitterSubscription, ImageResolvedAssetSource } from 'react-native'; 2 | 3 | export enum CaptureEventType { 4 | NONE, 5 | RECORDING, 6 | END_RECORDING, 7 | CAPTURED, 8 | APP_SWITCHING, 9 | UNKNOWN, 10 | ALLOW = 8, 11 | PREVENT_SCREEN_CAPTURE = 16, 12 | PREVENT_SCREEN_RECORDING = 32, 13 | PREVENT_SCREEN_APP_SWITCHING = 64, 14 | } 15 | 16 | export type CaptureProtectionModuleStatus = { 17 | screenshot?: boolean; 18 | record?: boolean; 19 | appSwitcher?: boolean; 20 | }; 21 | 22 | export type IOSProtectionCustomScreenOption = { 23 | text: string; 24 | textColor?: `#${string}`; 25 | backgroundColor?: `#${string}`; 26 | }; 27 | 28 | export type IOSProtectionScreenOption = 29 | | { 30 | image: NodeRequire; 31 | backgroundColor?: `#${string}`; 32 | contentMode?: ContentMode; 33 | } 34 | | IOSProtectionCustomScreenOption; 35 | 36 | export type PreventOption = { 37 | screenshot?: boolean; 38 | record?: boolean | IOSProtectionScreenOption; 39 | appSwitcher?: boolean | IOSProtectionScreenOption; 40 | }; 41 | 42 | export type AllowOption = { 43 | screenshot?: boolean; 44 | record?: boolean; 45 | appSwitcher?: boolean; 46 | }; 47 | 48 | export type CaptureEventCallback = (eventType: CaptureEventType) => void; 49 | 50 | export enum ContentMode { 51 | scaleToFill = 0, 52 | scaleAspectFit = 1, 53 | scaleAspectFill = 2, 54 | redraw = 3, 55 | center = 4, 56 | top = 5, 57 | bottom = 6, 58 | left = 7, 59 | right = 8, 60 | topLeft = 9, 61 | topRight = 10, 62 | bottomLeft = 11, 63 | bottomRight = 12, 64 | } 65 | export interface CaptureProtectionIOSNativeModules { 66 | allowScreenshot: () => Promise; 67 | preventScreenshot: () => Promise; 68 | allowScreenRecord: () => Promise; 69 | preventScreenRecord: () => Promise; 70 | preventScreenRecordWithText: ( 71 | text: string, 72 | textColor?: `#${string}`, 73 | backgroundColor?: `#${string}` 74 | ) => Promise; 75 | preventScreenRecordWithImage: ( 76 | image: ImageResolvedAssetSource, 77 | backgroundColor?: `#${string}`, 78 | contentMode?: number 79 | ) => Promise; 80 | allowAppSwitcher: () => Promise; 81 | preventAppSwitcher: () => Promise; 82 | preventAppSwitcherWithText: ( 83 | text: string, 84 | textColor?: `#${string}`, 85 | backgroundColor?: `#${string}` 86 | ) => Promise; 87 | preventAppSwitcherWithImage: ( 88 | image: ImageResolvedAssetSource, 89 | backgroundColor?: `#${string}`, 90 | contentMode?: number 91 | ) => Promise; 92 | hasListener: () => Promise; 93 | protectionStatus: () => Promise; 94 | isScreenRecording: () => Promise; 95 | } 96 | export interface CaptureProtectionAndroidNativeModules { 97 | allow: () => Promise; 98 | prevent: () => Promise; 99 | hasListener: () => Promise; 100 | protectionStatus: () => Promise; 101 | isScreenRecording: () => Promise; 102 | requestPermission: () => Promise; 103 | checkPermission: () => Promise; 104 | } 105 | 106 | export interface CaptureProtectionFunction { 107 | /** If no option is specified, all actions are prevent */ 108 | prevent: (option?: PreventOption) => Promise; 109 | /** If no option is specified, all actions are allow */ 110 | allow: (option?: AllowOption) => Promise; 111 | addListener: ( 112 | callback: CaptureEventCallback 113 | ) => EmitterSubscription | undefined; 114 | removeListener: (emitter: EmitterSubscription) => void; 115 | hasListener: () => Promise; 116 | protectionStatus: () => Promise; 117 | isScreenRecording: () => Promise; 118 | requestPermission: () => Promise; 119 | } 120 | 121 | export type CaptureProtectionContextType = { 122 | protectionStatus: CaptureProtectionModuleStatus; 123 | status: CaptureEventType; 124 | prevent: CaptureProtectionFunction['prevent']; 125 | allow: CaptureProtectionFunction['allow']; 126 | }; 127 | 128 | export type CaptureDetectionHookType = { 129 | protectionStatus: CaptureProtectionModuleStatus; 130 | status: CaptureEventType; 131 | }; 132 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": ["example"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "react-native-capture-protection": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noStrictGenericChecks": false, 19 | "noUncheckedIndexedAccess": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext" 26 | } 27 | } 28 | --------------------------------------------------------------------------------