@momo-kits/anti-screenshot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AntiScreenshot.podspec +43 -0
- package/LICENSE +20 -0
- package/README.md +45 -0
- package/android/build.gradle +83 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/momokits/antiscreenshot/AntiScreenshotPackage.kt +16 -0
- package/android/src/main/java/com/momokits/antiscreenshot/AntiScreenshotView.kt +284 -0
- package/android/src/main/java/com/momokits/antiscreenshot/AntiScreenshotViewManager.kt +19 -0
- package/ios/AntiScreenshotView.h +12 -0
- package/ios/AntiScreenshotView.m +84 -0
- package/ios/RNAntiScreenshot.h +10 -0
- package/ios/RNAntiScreenshot.mm +59 -0
- package/package.json +81 -0
- package/src/AntiScreenshotView.tsx +41 -0
- package/src/RNAntiScreenshotNativeComponent.ts +6 -0
- package/src/index.tsx +2 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "AntiScreenshot"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://www.npmjs.com/.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
|
21
|
+
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
|
|
22
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
23
|
+
install_modules_dependencies(s)
|
|
24
|
+
else
|
|
25
|
+
s.dependency "React-Core"
|
|
26
|
+
|
|
27
|
+
# Don't install the dependencies when we run `pod install` in the old architecture.
|
|
28
|
+
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
|
|
29
|
+
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
|
30
|
+
s.pod_target_xcconfig = {
|
|
31
|
+
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
|
32
|
+
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
|
|
33
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
|
|
34
|
+
}
|
|
35
|
+
s.dependency "React-RCTFabric"
|
|
36
|
+
s.dependency "React-Codegen"
|
|
37
|
+
s.dependency "RCT-Folly"
|
|
38
|
+
s.dependency "RCTRequired"
|
|
39
|
+
s.dependency "RCTTypeSafety"
|
|
40
|
+
s.dependency "ReactCommon/turbomodule/core"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 dung.pham2
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @momo-kits/anti-screenshot
|
|
2
|
+
|
|
3
|
+
A React Native component that protects content from screenshots and screen recordings.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @momo-kits/anti-screenshot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { AntiScreenshotView } from '@momo-kits/anti-screenshot';
|
|
15
|
+
|
|
16
|
+
function SecureScreen() {
|
|
17
|
+
return (
|
|
18
|
+
<AntiScreenshotView style={{ flex: 1 }}>
|
|
19
|
+
<Text>This content is protected from screenshots</Text>
|
|
20
|
+
<Text>It will appear blank/black in screenshots and screen recordings</Text>
|
|
21
|
+
</AntiScreenshotView>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
### Android
|
|
29
|
+
Uses a secure `SurfaceView` with `setSecure(true)` combined with bitmap rendering. Child views are rendered to a bitmap, which is then drawn onto the secure surface. Screenshots only capture the secure surface (which appears black).
|
|
30
|
+
|
|
31
|
+
### iOS
|
|
32
|
+
Uses the secure `UITextField` trick. When `secureTextEntry` is enabled, UITextField uses an internal view that prevents screenshot capture. The component extracts this internal view and uses it as a container for the protected content.
|
|
33
|
+
|
|
34
|
+
## Props
|
|
35
|
+
|
|
36
|
+
| Prop | Type | Description |
|
|
37
|
+
|------|------|-------------|
|
|
38
|
+
| `style` | `StyleProp<ViewStyle>` | Style for the container view |
|
|
39
|
+
| `children` | `React.ReactNode` | Content to be protected from screenshots |
|
|
40
|
+
|
|
41
|
+
All standard `View` props are also supported.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = {name ->
|
|
3
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['AntiScreenshot_' + name]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
dependencies {
|
|
12
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
13
|
+
// noinspection DifferentKotlinGradleVersion
|
|
14
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
apply plugin: "com.android.library"
|
|
20
|
+
apply plugin: "kotlin-android"
|
|
21
|
+
|
|
22
|
+
apply plugin: "com.facebook.react"
|
|
23
|
+
|
|
24
|
+
def getExtOrIntegerDefault(name) {
|
|
25
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["AntiScreenshot_" + name]).toInteger()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
android {
|
|
29
|
+
namespace "com.momokits.antiscreenshot"
|
|
30
|
+
|
|
31
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
32
|
+
|
|
33
|
+
defaultConfig {
|
|
34
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
35
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
buildFeatures {
|
|
39
|
+
buildConfig true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
buildTypes {
|
|
43
|
+
release {
|
|
44
|
+
minifyEnabled false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lintOptions {
|
|
49
|
+
disable "GradleCompatible"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
compileOptions {
|
|
53
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
54
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sourceSets {
|
|
58
|
+
main {
|
|
59
|
+
java.srcDirs += [
|
|
60
|
+
"generated/java",
|
|
61
|
+
"generated/jni"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
repositories {
|
|
68
|
+
mavenCentral()
|
|
69
|
+
google()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
73
|
+
|
|
74
|
+
dependencies {
|
|
75
|
+
implementation "com.facebook.react:react-android"
|
|
76
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
react {
|
|
80
|
+
jsRootDir = file("../src/")
|
|
81
|
+
libraryName = "AntiScreenshotView"
|
|
82
|
+
codegenJavaPackageName = "com.momokits.antiscreenshot"
|
|
83
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.momokits.antiscreenshot
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class AntiScreenshotPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return emptyList()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return listOf(AntiScreenshotViewManager())
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
package com.momokits.antiscreenshot
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.graphics.PixelFormat
|
|
8
|
+
import android.graphics.PorterDuff
|
|
9
|
+
import android.os.Handler
|
|
10
|
+
import android.os.Looper
|
|
11
|
+
import android.util.AttributeSet
|
|
12
|
+
import android.view.SurfaceHolder
|
|
13
|
+
import android.view.SurfaceView
|
|
14
|
+
import android.view.View
|
|
15
|
+
import android.view.ViewGroup
|
|
16
|
+
import android.view.ViewTreeObserver
|
|
17
|
+
import android.widget.FrameLayout
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* AntiScreenshotView - A ViewGroup that protects its content from screenshots.
|
|
21
|
+
*
|
|
22
|
+
* Content inside this view will appear as black/blank in screenshots and screen recordings,
|
|
23
|
+
* while displaying normally on the device screen.
|
|
24
|
+
*
|
|
25
|
+
* How it works:
|
|
26
|
+
* 1. Child views are added to a hidden container
|
|
27
|
+
* 2. The container is rendered to a Bitmap
|
|
28
|
+
* 3. The Bitmap is drawn onto a secure SurfaceView
|
|
29
|
+
* 4. Screenshots only capture the secure surface (which appears black)
|
|
30
|
+
*/
|
|
31
|
+
class AntiScreenshotView @JvmOverloads constructor(
|
|
32
|
+
context: Context,
|
|
33
|
+
attrs: AttributeSet? = null,
|
|
34
|
+
defStyleAttr: Int = 0
|
|
35
|
+
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
36
|
+
|
|
37
|
+
private var secureSurface: SurfaceView? = null
|
|
38
|
+
private var contentContainer: FrameLayout? = null
|
|
39
|
+
private var isSurfaceReady = false
|
|
40
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
41
|
+
private var redrawRunnable: Runnable? = null
|
|
42
|
+
private var contentBitmap: Bitmap? = null
|
|
43
|
+
private var isFirstDraw = true
|
|
44
|
+
|
|
45
|
+
private val surfaceCallback = object : SurfaceHolder.Callback {
|
|
46
|
+
override fun surfaceCreated(holder: SurfaceHolder) {
|
|
47
|
+
isSurfaceReady = true
|
|
48
|
+
scheduleRedraw()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
|
52
|
+
scheduleRedraw()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
|
56
|
+
isSurfaceReady = false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
|
|
61
|
+
scheduleRedraw()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
init {
|
|
65
|
+
initializeViews()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun initializeViews() {
|
|
69
|
+
// 1. Create content container - Initially VISIBLE for layout/measure
|
|
70
|
+
contentContainer = FrameLayout(context).apply {
|
|
71
|
+
layoutParams = LayoutParams(
|
|
72
|
+
LayoutParams.MATCH_PARENT,
|
|
73
|
+
LayoutParams.WRAP_CONTENT
|
|
74
|
+
)
|
|
75
|
+
visibility = View.VISIBLE
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Create secure SurfaceView
|
|
79
|
+
secureSurface = SurfaceView(context).apply {
|
|
80
|
+
layoutParams = LayoutParams(
|
|
81
|
+
LayoutParams.MATCH_PARENT,
|
|
82
|
+
LayoutParams.MATCH_PARENT
|
|
83
|
+
)
|
|
84
|
+
setZOrderOnTop(true)
|
|
85
|
+
holder.setFormat(PixelFormat.TRANSLUCENT)
|
|
86
|
+
holder.addCallback(surfaceCallback)
|
|
87
|
+
setSecure(true)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Add content container first (will be hidden after first render)
|
|
91
|
+
super.addView(contentContainer, -1, contentContainer!!.layoutParams)
|
|
92
|
+
// Add secure surface on top
|
|
93
|
+
super.addView(secureSurface, -1, secureSurface!!.layoutParams)
|
|
94
|
+
|
|
95
|
+
// Listen for layout changes
|
|
96
|
+
viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun scheduleRedraw() {
|
|
100
|
+
redrawRunnable?.let { handler.removeCallbacks(it) }
|
|
101
|
+
redrawRunnable = Runnable { redrawContent() }
|
|
102
|
+
// Delay to ensure content is laid out
|
|
103
|
+
handler.postDelayed(redrawRunnable!!, if (isFirstDraw) 100 else 16)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Redraws the content onto the secure surface.
|
|
108
|
+
*/
|
|
109
|
+
private fun redrawContent() {
|
|
110
|
+
if (!isSurfaceReady) return
|
|
111
|
+
|
|
112
|
+
val container = contentContainer ?: return
|
|
113
|
+
val surface = secureSurface ?: return
|
|
114
|
+
val holder = surface.holder
|
|
115
|
+
|
|
116
|
+
// Ensure content is visible and laid out before capturing
|
|
117
|
+
if (container.visibility != View.VISIBLE) {
|
|
118
|
+
container.visibility = View.VISIBLE
|
|
119
|
+
handler.postDelayed({ redrawContent() }, 50)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (container.width <= 0 || container.height <= 0) {
|
|
124
|
+
// Wait for layout
|
|
125
|
+
handler.postDelayed({ redrawContent() }, 50)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Create or recreate bitmap if needed
|
|
131
|
+
val bitmapWidth = container.width
|
|
132
|
+
val bitmapHeight = container.height
|
|
133
|
+
|
|
134
|
+
if (contentBitmap == null ||
|
|
135
|
+
contentBitmap?.width != bitmapWidth ||
|
|
136
|
+
contentBitmap?.height != bitmapHeight ||
|
|
137
|
+
contentBitmap?.isRecycled == true
|
|
138
|
+
) {
|
|
139
|
+
contentBitmap?.recycle()
|
|
140
|
+
contentBitmap = Bitmap.createBitmap(
|
|
141
|
+
bitmapWidth,
|
|
142
|
+
bitmapHeight,
|
|
143
|
+
Bitmap.Config.ARGB_8888
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
val bitmap = contentBitmap ?: return
|
|
148
|
+
|
|
149
|
+
// Draw content to bitmap
|
|
150
|
+
val bitmapCanvas = Canvas(bitmap)
|
|
151
|
+
bitmapCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
152
|
+
container.draw(bitmapCanvas)
|
|
153
|
+
|
|
154
|
+
// Draw bitmap to secure surface
|
|
155
|
+
val canvas = holder.lockCanvas()
|
|
156
|
+
if (canvas != null) {
|
|
157
|
+
try {
|
|
158
|
+
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
|
|
159
|
+
canvas.drawBitmap(bitmap, 0f, 0f, null)
|
|
160
|
+
} finally {
|
|
161
|
+
holder.unlockCanvasAndPost(canvas)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Hide original content after drawing to surface
|
|
166
|
+
container.visibility = View.INVISIBLE
|
|
167
|
+
isFirstDraw = false
|
|
168
|
+
|
|
169
|
+
} catch (e: Exception) {
|
|
170
|
+
e.printStackTrace()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Force redraw - makes content visible briefly to capture updates.
|
|
176
|
+
*/
|
|
177
|
+
fun forceRedraw() {
|
|
178
|
+
contentContainer?.visibility = View.VISIBLE
|
|
179
|
+
scheduleRedraw()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Override addView to redirect children to content container
|
|
183
|
+
override fun addView(child: View?) {
|
|
184
|
+
if (contentContainer != null && child != secureSurface && child != contentContainer) {
|
|
185
|
+
contentContainer?.addView(child)
|
|
186
|
+
setupChildListener(child)
|
|
187
|
+
forceRedraw()
|
|
188
|
+
} else {
|
|
189
|
+
super.addView(child)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
override fun addView(child: View?, index: Int) {
|
|
194
|
+
if (contentContainer != null && child != secureSurface && child != contentContainer) {
|
|
195
|
+
contentContainer?.addView(child, index)
|
|
196
|
+
setupChildListener(child)
|
|
197
|
+
forceRedraw()
|
|
198
|
+
} else {
|
|
199
|
+
super.addView(child, index)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override fun addView(child: View?, params: ViewGroup.LayoutParams?) {
|
|
204
|
+
if (contentContainer != null && child != secureSurface && child != contentContainer) {
|
|
205
|
+
contentContainer?.addView(child, params)
|
|
206
|
+
setupChildListener(child)
|
|
207
|
+
forceRedraw()
|
|
208
|
+
} else {
|
|
209
|
+
super.addView(child, params)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
|
214
|
+
if (contentContainer != null && child != secureSurface && child != contentContainer) {
|
|
215
|
+
contentContainer?.addView(child, index, params)
|
|
216
|
+
setupChildListener(child)
|
|
217
|
+
forceRedraw()
|
|
218
|
+
} else {
|
|
219
|
+
super.addView(child, index, params)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
override fun addView(child: View?, width: Int, height: Int) {
|
|
224
|
+
if (contentContainer != null && child != secureSurface && child != contentContainer) {
|
|
225
|
+
contentContainer?.addView(child, width, height)
|
|
226
|
+
setupChildListener(child)
|
|
227
|
+
forceRedraw()
|
|
228
|
+
} else {
|
|
229
|
+
super.addView(child, width, height)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private fun setupChildListener(child: View?) {
|
|
234
|
+
child?.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
235
|
+
forceRedraw()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
override fun removeView(view: View?) {
|
|
240
|
+
if (view != secureSurface && view != contentContainer) {
|
|
241
|
+
contentContainer?.removeView(view)
|
|
242
|
+
forceRedraw()
|
|
243
|
+
} else {
|
|
244
|
+
super.removeView(view)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
override fun removeViewAt(index: Int) {
|
|
249
|
+
contentContainer?.removeViewAt(index)
|
|
250
|
+
forceRedraw()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
override fun removeAllViews() {
|
|
254
|
+
contentContainer?.removeAllViews()
|
|
255
|
+
forceRedraw()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
override fun onDetachedFromWindow() {
|
|
259
|
+
super.onDetachedFromWindow()
|
|
260
|
+
redrawRunnable?.let { handler.removeCallbacks(it) }
|
|
261
|
+
contentBitmap?.recycle()
|
|
262
|
+
contentBitmap = null
|
|
263
|
+
secureSurface?.holder?.removeCallback(surfaceCallback)
|
|
264
|
+
try {
|
|
265
|
+
viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
|
|
266
|
+
} catch (e: Exception) {
|
|
267
|
+
// Ignore
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Gets the actual child count (excluding internal views).
|
|
273
|
+
*/
|
|
274
|
+
fun getContentChildCount(): Int {
|
|
275
|
+
return contentContainer?.childCount ?: 0
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Gets a child view at the specified index.
|
|
280
|
+
*/
|
|
281
|
+
fun getContentChildAt(index: Int): View? {
|
|
282
|
+
return contentContainer?.getChildAt(index)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package com.momokits.antiscreenshot
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
6
|
+
|
|
7
|
+
@ReactModule(name = AntiScreenshotViewManager.NAME)
|
|
8
|
+
class AntiScreenshotViewManager : ViewGroupManager<AntiScreenshotView>() {
|
|
9
|
+
|
|
10
|
+
override fun getName(): String = NAME
|
|
11
|
+
|
|
12
|
+
override fun createViewInstance(reactContext: ThemedReactContext): AntiScreenshotView {
|
|
13
|
+
return AntiScreenshotView(reactContext)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
companion object {
|
|
17
|
+
const val NAME = "RNAntiScreenshot"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/// A UIView that protects its content from being captured in screenshots or
|
|
6
|
+
/// screen recordings. It uses a secure UITextField's internal view as a
|
|
7
|
+
/// container for the content.
|
|
8
|
+
@interface AntiScreenshotView : UIView
|
|
9
|
+
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#import "AntiScreenshotView.h"
|
|
2
|
+
|
|
3
|
+
@interface AntiScreenshotView ()
|
|
4
|
+
|
|
5
|
+
@property(nonatomic, strong) UITextField *secureTextField;
|
|
6
|
+
@property(nonatomic, strong) UIView *secureContainerView;
|
|
7
|
+
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
@implementation AntiScreenshotView
|
|
11
|
+
|
|
12
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
13
|
+
self = [super initWithFrame:frame];
|
|
14
|
+
if (self) {
|
|
15
|
+
[self setupSecureView];
|
|
16
|
+
}
|
|
17
|
+
return self;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
- (instancetype)initWithCoder:(NSCoder *)coder {
|
|
21
|
+
self = [super initWithCoder:coder];
|
|
22
|
+
if (self) {
|
|
23
|
+
[self setupSecureView];
|
|
24
|
+
}
|
|
25
|
+
return self;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- (void)setupSecureView {
|
|
29
|
+
// Create a secure text field - the magic ingredient
|
|
30
|
+
self.secureTextField = [[UITextField alloc] init];
|
|
31
|
+
self.secureTextField.secureTextEntry = YES;
|
|
32
|
+
self.secureTextField.userInteractionEnabled = NO;
|
|
33
|
+
|
|
34
|
+
// Get the internal secure view from UITextField
|
|
35
|
+
// When secureTextEntry is YES, UITextField uses a special internal view
|
|
36
|
+
// that prevents screenshot capture
|
|
37
|
+
if (self.secureTextField.subviews.count > 0) {
|
|
38
|
+
self.secureContainerView = self.secureTextField.subviews.firstObject;
|
|
39
|
+
|
|
40
|
+
// Add the secure container to our view hierarchy
|
|
41
|
+
[self addSubview:self.secureContainerView];
|
|
42
|
+
|
|
43
|
+
// Setup constraints
|
|
44
|
+
self.secureContainerView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
45
|
+
[NSLayoutConstraint activateConstraints:@[
|
|
46
|
+
[self.secureContainerView.topAnchor
|
|
47
|
+
constraintEqualToAnchor:self.topAnchor],
|
|
48
|
+
[self.secureContainerView.leadingAnchor
|
|
49
|
+
constraintEqualToAnchor:self.leadingAnchor],
|
|
50
|
+
[self.secureContainerView.trailingAnchor
|
|
51
|
+
constraintEqualToAnchor:self.trailingAnchor],
|
|
52
|
+
[self.secureContainerView.bottomAnchor
|
|
53
|
+
constraintEqualToAnchor:self.bottomAnchor]
|
|
54
|
+
]];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
- (void)addSubview:(UIView *)view {
|
|
59
|
+
// Redirect child views to the secure container if available
|
|
60
|
+
if (self.secureContainerView && view != self.secureContainerView) {
|
|
61
|
+
[self.secureContainerView addSubview:view];
|
|
62
|
+
} else {
|
|
63
|
+
[super addSubview:view];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index {
|
|
68
|
+
if (self.secureContainerView && view != self.secureContainerView) {
|
|
69
|
+
[self.secureContainerView insertSubview:view atIndex:index];
|
|
70
|
+
} else {
|
|
71
|
+
[super insertSubview:view atIndex:index];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
- (void)layoutSubviews {
|
|
76
|
+
[super layoutSubviews];
|
|
77
|
+
|
|
78
|
+
// Ensure secure container fills the view
|
|
79
|
+
if (self.secureContainerView) {
|
|
80
|
+
self.secureContainerView.frame = self.bounds;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#import "RNAntiScreenshot.h"
|
|
2
|
+
|
|
3
|
+
#import <react/renderer/components/AntiScreenshotViewSpec/ComponentDescriptors.h>
|
|
4
|
+
#import <react/renderer/components/AntiScreenshotViewSpec/EventEmitters.h>
|
|
5
|
+
#import <react/renderer/components/AntiScreenshotViewSpec/Props.h>
|
|
6
|
+
#import <react/renderer/components/AntiScreenshotViewSpec/RCTComponentViewHelpers.h>
|
|
7
|
+
|
|
8
|
+
#import "AntiScreenshotView.h"
|
|
9
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
10
|
+
|
|
11
|
+
using namespace facebook::react;
|
|
12
|
+
|
|
13
|
+
@interface RNAntiScreenshot () <RCTRNAntiScreenshotViewProtocol>
|
|
14
|
+
@end
|
|
15
|
+
|
|
16
|
+
@implementation RNAntiScreenshot {
|
|
17
|
+
AntiScreenshotView *_view;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
21
|
+
return concreteComponentDescriptorProvider<
|
|
22
|
+
RNAntiScreenshotComponentDescriptor>();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
26
|
+
if (self = [super initWithFrame:frame]) {
|
|
27
|
+
static const auto defaultProps =
|
|
28
|
+
std::make_shared<const RNAntiScreenshotProps>();
|
|
29
|
+
_props = defaultProps;
|
|
30
|
+
|
|
31
|
+
_view = [[AntiScreenshotView alloc] init];
|
|
32
|
+
self.contentView = _view;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return self;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
- (void)updateProps:(Props::Shared const &)props
|
|
39
|
+
oldProps:(Props::Shared const &)oldProps {
|
|
40
|
+
[super updateProps:props oldProps:oldProps];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
- (void)mountChildComponentView:
|
|
44
|
+
(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
45
|
+
index:(NSInteger)index {
|
|
46
|
+
[_view insertSubview:childComponentView atIndex:index];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
- (void)unmountChildComponentView:
|
|
50
|
+
(UIView<RCTComponentViewProtocol> *)childComponentView
|
|
51
|
+
index:(NSInteger)index {
|
|
52
|
+
[childComponentView removeFromSuperview];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@end
|
|
56
|
+
|
|
57
|
+
Class<RCTComponentViewProtocol> RNAntiScreenshotCls(void) {
|
|
58
|
+
return RNAntiScreenshot.class;
|
|
59
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@momo-kits/anti-screenshot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A React Native component that protects content from screenshots and screen recordings.",
|
|
5
|
+
"main": "./src/index.tsx",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"lib",
|
|
9
|
+
"android",
|
|
10
|
+
"ios",
|
|
11
|
+
"cpp",
|
|
12
|
+
"*.podspec",
|
|
13
|
+
"react-native.config.js",
|
|
14
|
+
"!ios/build",
|
|
15
|
+
"!android/build",
|
|
16
|
+
"!android/gradle",
|
|
17
|
+
"!android/gradlew",
|
|
18
|
+
"!android/gradlew.bat",
|
|
19
|
+
"!android/local.properties",
|
|
20
|
+
"!**/__tests__",
|
|
21
|
+
"!**/__fixtures__",
|
|
22
|
+
"!**/__mocks__",
|
|
23
|
+
"!**/.*"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"example": "yarn workspace @momo-kits/anti-screenshot-example",
|
|
27
|
+
"test": "jest",
|
|
28
|
+
"typecheck": "tsc",
|
|
29
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
30
|
+
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
31
|
+
"build": "echo",
|
|
32
|
+
"release": "release-it --only-version"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"react-native",
|
|
36
|
+
"ios",
|
|
37
|
+
"android",
|
|
38
|
+
"screenshot",
|
|
39
|
+
"anti-screenshot",
|
|
40
|
+
"screen-capture",
|
|
41
|
+
"security"
|
|
42
|
+
],
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://www.npmjs.com/.git"
|
|
46
|
+
},
|
|
47
|
+
"author": "MoMo Team",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://www.npmjs.com//issues"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://www.npmjs.com/#readme",
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"registry": "https://registry.npmjs.org/"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"react": "19.0.0",
|
|
58
|
+
"react-native": "0.80.1"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"react": "*",
|
|
62
|
+
"react-native": "*"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=18.0.0"
|
|
66
|
+
},
|
|
67
|
+
"codegenConfig": {
|
|
68
|
+
"name": "AntiScreenshotViewSpec",
|
|
69
|
+
"type": "all",
|
|
70
|
+
"jsSrcsDir": "src",
|
|
71
|
+
"android": {
|
|
72
|
+
"javaPackageName": "com.momokits.antiscreenshot"
|
|
73
|
+
},
|
|
74
|
+
"ios": {
|
|
75
|
+
"componentProvider": {
|
|
76
|
+
"RNAntiScreenshot": "RNAntiScreenshot"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"dependencies": {}
|
|
81
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ViewProps, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import RNAntiScreenshotNativeComponent from './RNAntiScreenshotNativeComponent';
|
|
4
|
+
|
|
5
|
+
export interface AntiScreenshotViewProps extends ViewProps {
|
|
6
|
+
/**
|
|
7
|
+
* Style for the container view
|
|
8
|
+
*/
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
/**
|
|
11
|
+
* Children to be protected from screenshots
|
|
12
|
+
*/
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* AntiScreenshotView - A component that protects its content from screenshots.
|
|
18
|
+
*
|
|
19
|
+
* Content inside this view will appear as black/blank in screenshots and screen recordings,
|
|
20
|
+
* while displaying normally on the device screen.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <AntiScreenshotView style={{ flex: 1 }}>
|
|
25
|
+
* <Text>This content is protected from screenshots</Text>
|
|
26
|
+
* </AntiScreenshotView>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export const AntiScreenshotView: React.FC<AntiScreenshotViewProps> = ({
|
|
30
|
+
children,
|
|
31
|
+
style,
|
|
32
|
+
...props
|
|
33
|
+
}) => {
|
|
34
|
+
return (
|
|
35
|
+
<RNAntiScreenshotNativeComponent style={style} {...props}>
|
|
36
|
+
{children}
|
|
37
|
+
</RNAntiScreenshotNativeComponent>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default AntiScreenshotView;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
|
|
2
|
+
import type { ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface NativeProps extends ViewProps {}
|
|
5
|
+
|
|
6
|
+
export default codegenNativeComponent<NativeProps>('RNAntiScreenshot');
|
package/src/index.tsx
ADDED