├── .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 | |  |  |
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 |
--------------------------------------------------------------------------------