@onekeyfe/react-native-capture-protection 0.0.0-stage → 3.0.166
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/ACKNOWLEDGEMENTS.md +18 -0
- package/LICENSE +22 -0
- package/LICENSE.upstream +20 -0
- package/README.md +25 -2
- package/ReactNativeCaptureProtection.podspec +37 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +130 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/reactnativecaptureprotection/ReactNativeCaptureProtection.kt +285 -0
- package/android/src/main/java/com/margelo/nitro/reactnativecaptureprotection/ReactNativeCaptureProtectionPackage.kt +22 -0
- package/docs/SPEC.md +117 -0
- package/ios/ReactNativeCaptureProtection.swift +251 -0
- package/ios/tests/CaptureProtectionTests.swift +50 -0
- package/lib/module/ReactNativeCaptureProtection.nitro.js +4 -0
- package/lib/module/ReactNativeCaptureProtection.nitro.js.map +1 -0
- package/lib/module/index.js +61 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ReactNativeCaptureProtection.nitro.d.ts +10 -0
- package/lib/typescript/src/ReactNativeCaptureProtection.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +22 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeCaptureProtectionSpec.cpp +88 -0
- package/nitrogen/generated/android/c++/JHybridReactNativeCaptureProtectionSpec.hpp +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecaptureprotection/Func_void_double.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecaptureprotection/HybridReactNativeCaptureProtectionSpec.kt +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativecaptureprotection/reactnativecaptureprotectionOnLoad.kt +35 -0
- package/nitrogen/generated/android/reactnativecaptureprotection+autolinking.cmake +81 -0
- package/nitrogen/generated/android/reactnativecaptureprotection+autolinking.gradle +27 -0
- package/nitrogen/generated/android/reactnativecaptureprotectionOnLoad.cpp +56 -0
- package/nitrogen/generated/android/reactnativecaptureprotectionOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtection+autolinking.rb +62 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtection-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtection-Swift-Cxx-Bridge.hpp +157 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtection-Swift-Cxx-Umbrella.hpp +46 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtectionAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ReactNativeCaptureProtectionAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeCaptureProtectionSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeCaptureProtectionSpecSwift.hpp +98 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeCaptureProtectionSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridReactNativeCaptureProtectionSpec_cxx.swift +187 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeCaptureProtectionSpec.cpp +23 -0
- package/nitrogen/generated/shared/c++/HybridReactNativeCaptureProtectionSpec.hpp +66 -0
- package/package.json +173 -4
- package/src/ReactNativeCaptureProtection.nitro.ts +8 -0
- package/src/index.tsx +65 -0
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Capture protection contract
|
|
2
|
+
|
|
3
|
+
Status: Implemented. Native runtime acceptance is pending.
|
|
4
|
+
|
|
5
|
+
## Purpose and scope
|
|
6
|
+
|
|
7
|
+
Protect wallet-sensitive screens without photo-library or file access. Support
|
|
8
|
+
OneKey iOS/Android callers of `prevent()`, `allow()` and `addListener()`. This is
|
|
9
|
+
not the full upstream API: providers, hooks, custom text/image overlays,
|
|
10
|
+
permission requests and media-library screenshot detection are not exported.
|
|
11
|
+
Web/desktop callers use their existing platform hooks and do not load this module.
|
|
12
|
+
|
|
13
|
+
## Ownership and boundaries
|
|
14
|
+
|
|
15
|
+
The OS window and protection state belong to a process-wide native coordinator.
|
|
16
|
+
Each Nitro object has its own owner identifier and callback; main and background
|
|
17
|
+
JS runtimes have separate objects/heaps and initialize independently. One owner's
|
|
18
|
+
`allow()` must not release another owner's protection. JS callbacks remain scoped
|
|
19
|
+
to their owning object/runtime. No image bytes, album entries, file paths or
|
|
20
|
+
product secrets enter this module. Only the UI runtime uses it in the app.
|
|
21
|
+
|
|
22
|
+
## Public API and defaults
|
|
23
|
+
|
|
24
|
+
- `CaptureProtection.prevent(): Promise<void>` enables screenshot, recording and
|
|
25
|
+
app-switcher protection together. Repeated calls by one object are idempotent.
|
|
26
|
+
- `CaptureProtection.allow(): Promise<void>` releases only that object's protection.
|
|
27
|
+
- `CaptureProtection.addListener(callback): { remove(): void }` registers a local
|
|
28
|
+
subscriber; removal is idempotent. At most 128 JS subscribers per runtime.
|
|
29
|
+
- `CaptureEventType`: NONE=0, RECORDING=1, END_RECORDING=2, CAPTURED=3,
|
|
30
|
+
APP_SWITCHING=4, UNKNOWN=5, ALLOW=8, PREVENT_SCREEN_CAPTURE=16,
|
|
31
|
+
PREVENT_SCREEN_RECORDING=32, PREVENT_SCREEN_APP_SWITCHING=64.
|
|
32
|
+
- Nitro methods are `prevent`, `allow`, `setListener(callback | undefined)`.
|
|
33
|
+
Protection failures reject; missing native registration throws instead of
|
|
34
|
+
silently pretending sensitive screens are protected.
|
|
35
|
+
|
|
36
|
+
## Lifecycle and concurrency
|
|
37
|
+
|
|
38
|
+
All native state and window operations run on the UI thread in dispatch order.
|
|
39
|
+
There is at most one native subscription per object; JS fans events out to its
|
|
40
|
+
current subscribers. Removing the final subscriber releases its native callback.
|
|
41
|
+
Native observers start on first owner/subscriber and stop on the last. Callbacks
|
|
42
|
+
are weakly connected to objects; disposal releases that owner's state and listener.
|
|
43
|
+
Android resolves the foreground activity through the application UI ReactHost
|
|
44
|
+
and lifecycle tracking, never Nitro's last-initialized global React context.
|
|
45
|
+
Android reapplies FLAG_SECURE on activity resume while protection is requested,
|
|
46
|
+
unregisters screenshot callbacks on pause and unregisters activity/display
|
|
47
|
+
observers when no owners/subscribers remain. iOS reload notification resets
|
|
48
|
+
obsolete owners and observers. Protection is fail-closed across activity pause;
|
|
49
|
+
activity destruction does not clear the owners of still-running JS runtimes.
|
|
50
|
+
Events reflect OS notifications, not screenshot image contents. No deduplication
|
|
51
|
+
or 1-second artificial event reset is added; the existing app hook debounces warnings.
|
|
52
|
+
|
|
53
|
+
## Data, cache and identity
|
|
54
|
+
|
|
55
|
+
Only in-memory owner IDs, listeners, observer registrations and overlay windows
|
|
56
|
+
are retained. No storage, persistence, cache, migrations or network access.
|
|
57
|
+
|
|
58
|
+
## Platform contract
|
|
59
|
+
|
|
60
|
+
| Behavior | iOS | Android |
|
|
61
|
+
| --------------------- | --------------------------------------------------- | ----------------------------------------------------- |
|
|
62
|
+
| Screenshot protection | Upstream-derived secure UITextField layer technique | FLAG_SECURE |
|
|
63
|
+
| Recording protection | Secure layer plus opaque window while captured | FLAG_SECURE |
|
|
64
|
+
| App switcher | Opaque window before resign/background | FLAG_SECURE |
|
|
65
|
+
| Screenshot event | UIApplication notification | Android 14+ system callback |
|
|
66
|
+
| Recording event | UIScreen capture change notification | Upstream-derived virtual-display heuristic |
|
|
67
|
+
| Permissions | None | DETECT_SCREEN_CAPTURE, normal permission, Android 14+ |
|
|
68
|
+
|
|
69
|
+
Android below 14 intentionally has no screenshot notification; blocking remains
|
|
70
|
+
available. Android virtual-display events are a heuristic and may also represent
|
|
71
|
+
casting/external displays; they are not proof that a recording was made.
|
|
72
|
+
iOS secure-text-field wrapping is inherited from upstream, is not a documented
|
|
73
|
+
system-wide screenshot prevention API, and requires OS-version/device acceptance.
|
|
74
|
+
Existing recordings when subscribing emit RECORDING when identifiable. Existing
|
|
75
|
+
Android private virtual displays may not be visible to DisplayManager enumeration.
|
|
76
|
+
|
|
77
|
+
## Failure, fallback and safety
|
|
78
|
+
|
|
79
|
+
Missing foreground activity/window or unavailable secure layer rejects prevent.
|
|
80
|
+
Never fall back to MediaStore queries, filesystem scanning or storage permissions.
|
|
81
|
+
Screenshot observer errors must not disable FLAG_SECURE. Only FLAG_SECURE flags
|
|
82
|
+
introduced by this coordinator may be cleared; preserve pre-existing flags.
|
|
83
|
+
A native coordinator admits at most 32 owner/subscriber identities, rejecting
|
|
84
|
+
excess registration. No custom overlays, bytes or unbounded event queues.
|
|
85
|
+
|
|
86
|
+
## Performance and resources
|
|
87
|
+
|
|
88
|
+
UI work is limited to flags, notifications, layer configuration and at most two
|
|
89
|
+
opaque windows on iOS. No polling, I/O, image processing or detached coroutines.
|
|
90
|
+
Recording/display changes are event-driven. Listener registration synchronously marshals to the UI thread; Android rejects
|
|
91
|
+
a wait exceeding 5 seconds. Protection operations return promises. No performance
|
|
92
|
+
claim is made.
|
|
93
|
+
|
|
94
|
+
## Conformance and acceptance
|
|
95
|
+
|
|
96
|
+
Source: `src/index.tsx`, `ios/ReactNativeCaptureProtection.swift` and Android
|
|
97
|
+
`ReactNativeCaptureProtection.kt`. JS tests cover listener fan-out/removal and
|
|
98
|
+
missing registration. iOS XCTest is provided for independent object subscriptions,
|
|
99
|
+
disposal and secure-layer owner isolation; physical capture acceptance remains
|
|
100
|
+
separate.
|
|
101
|
+
Required runtime acceptance: prevent/allow, nested sensitive pages, repeated
|
|
102
|
+
navigation, recording started before/after subscription, background snapshots,
|
|
103
|
+
activity recreation, subscription cleanup, and main/background object isolation.
|
|
104
|
+
Inspect merged Android manifest and packaged iOS plist for media-read permissions.
|
|
105
|
+
Build/type/unit results do not prove screenshot prevention on physical devices.
|
|
106
|
+
|
|
107
|
+
## Template deviations and attribution
|
|
108
|
+
|
|
109
|
+
Generated from `yarn create:module react-native-capture-protection`. Remove the
|
|
110
|
+
hello example and unused Babel module-resolver alias only; retain template build
|
|
111
|
+
conventions. The alias is not used by this package and the template does not
|
|
112
|
+
declare its Babel plugin. Match the repository React Native patched dependency. Use the repository
|
|
113
|
+
FlatCompat ESLint pattern because the template calls an unavailable
|
|
114
|
+
createEslintConfig export. The XCTest spec requires an app host for UIKit tests.
|
|
115
|
+
Add SPEC, focused tests,
|
|
116
|
+
ACKNOWLEDGEMENTS and the upstream MIT notice. See ACKNOWLEDGEMENTS.md for source
|
|
117
|
+
provenance and the precise subset adapted.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Adapted from react-native-capture-protection 2.3.0 by lethe; see LICENSE.upstream.
|
|
2
|
+
import Foundation
|
|
3
|
+
import NitroModules
|
|
4
|
+
import React
|
|
5
|
+
import UIKit
|
|
6
|
+
|
|
7
|
+
class ReactNativeCaptureProtection: HybridReactNativeCaptureProtectionSpec {
|
|
8
|
+
private let owner = UUID()
|
|
9
|
+
private var listener: ((Double) -> Void)?
|
|
10
|
+
|
|
11
|
+
func prevent() throws -> Promise<Void> {
|
|
12
|
+
let promise = Promise<Void>()
|
|
13
|
+
DispatchQueue.main.async {
|
|
14
|
+
do {
|
|
15
|
+
try CaptureProtectionCoordinator.shared.prevent(owner: self.owner)
|
|
16
|
+
promise.resolve(withResult: ())
|
|
17
|
+
} catch {
|
|
18
|
+
promise.reject(withError: error)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return promise
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func allow() throws -> Promise<Void> {
|
|
25
|
+
let promise = Promise<Void>()
|
|
26
|
+
DispatchQueue.main.async {
|
|
27
|
+
CaptureProtectionCoordinator.shared.allow(owner: self.owner)
|
|
28
|
+
promise.resolve(withResult: ())
|
|
29
|
+
}
|
|
30
|
+
return promise
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func setListener(callback: ((Double) -> Void)?) throws {
|
|
34
|
+
try onMain {
|
|
35
|
+
let coordinator = CaptureProtectionCoordinator.shared
|
|
36
|
+
if let callback {
|
|
37
|
+
try coordinator.subscribe(owner: self.owner) { [weak self] event in
|
|
38
|
+
self?.listener?(event)
|
|
39
|
+
}
|
|
40
|
+
self.listener = callback
|
|
41
|
+
coordinator.emitRecordingState(owner: self.owner)
|
|
42
|
+
} else {
|
|
43
|
+
self.listener = nil
|
|
44
|
+
coordinator.unsubscribe(owner: self.owner)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func dispose() {
|
|
50
|
+
DispatchQueue.main.async {
|
|
51
|
+
self.listener = nil
|
|
52
|
+
CaptureProtectionCoordinator.shared.unsubscribe(owner: self.owner)
|
|
53
|
+
CaptureProtectionCoordinator.shared.allow(owner: self.owner)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
deinit {
|
|
58
|
+
let identifier = owner
|
|
59
|
+
DispatchQueue.main.async {
|
|
60
|
+
CaptureProtectionCoordinator.shared.unsubscribe(owner: identifier)
|
|
61
|
+
CaptureProtectionCoordinator.shared.allow(owner: identifier)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private func onMain(_ operation: () throws -> Void) rethrows {
|
|
66
|
+
if Thread.isMainThread {
|
|
67
|
+
try operation()
|
|
68
|
+
} else {
|
|
69
|
+
try DispatchQueue.main.sync(execute: operation)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private final class CaptureProtectionCoordinator {
|
|
75
|
+
static let shared = CaptureProtectionCoordinator()
|
|
76
|
+
private var owners = Set<UUID>()
|
|
77
|
+
private var listeners: [UUID: (Double) -> Void] = [:]
|
|
78
|
+
private var observers: [NSObjectProtocol] = []
|
|
79
|
+
private var secureField: UITextField?
|
|
80
|
+
private weak var protectedWindow: UIWindow?
|
|
81
|
+
private var recordWindow: UIWindow?
|
|
82
|
+
private var switcherWindow: UIWindow?
|
|
83
|
+
private var recording = false
|
|
84
|
+
|
|
85
|
+
private var window: UIWindow? {
|
|
86
|
+
if let delegated = UIApplication.shared.delegate?.window ?? nil { return delegated }
|
|
87
|
+
return UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
88
|
+
.flatMap { $0.windows }.first { $0.isKeyWindow }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func checkCapacity(_ owner: UUID) throws {
|
|
92
|
+
let identities = owners.union(listeners.keys)
|
|
93
|
+
if !identities.contains(owner) && identities.count >= 32 {
|
|
94
|
+
throw failure("Capture protection supports at most 32 native owners.")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func prevent(owner: UUID) throws {
|
|
99
|
+
try checkCapacity(owner)
|
|
100
|
+
guard let window else { throw failure("No application window is available.") }
|
|
101
|
+
if owners.isEmpty {
|
|
102
|
+
try secureScreenshot(window: window)
|
|
103
|
+
}
|
|
104
|
+
owners.insert(owner)
|
|
105
|
+
startObservers()
|
|
106
|
+
refreshRecording()
|
|
107
|
+
if UIApplication.shared.applicationState != .active { showSwitcher() }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func allow(owner: UUID) {
|
|
111
|
+
owners.remove(owner)
|
|
112
|
+
if owners.isEmpty {
|
|
113
|
+
secureField?.isSecureTextEntry = false
|
|
114
|
+
removeRecordWindow()
|
|
115
|
+
removeSwitcherWindow()
|
|
116
|
+
}
|
|
117
|
+
stopObserversIfIdle()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func subscribe(owner: UUID, callback: @escaping (Double) -> Void) throws {
|
|
121
|
+
try checkCapacity(owner)
|
|
122
|
+
listeners[owner] = callback
|
|
123
|
+
startObservers()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func unsubscribe(owner: UUID) {
|
|
127
|
+
listeners.removeValue(forKey: owner)
|
|
128
|
+
stopObserversIfIdle()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func emitRecordingState(owner: UUID) {
|
|
132
|
+
if UIScreen.main.isCaptured { listeners[owner]?(1) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private func emit(_ event: Double) {
|
|
136
|
+
for (owner, callback) in Array(listeners) where listeners[owner] != nil {
|
|
137
|
+
callback(event)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func startObservers() {
|
|
142
|
+
guard observers.isEmpty else { return }
|
|
143
|
+
let center = NotificationCenter.default
|
|
144
|
+
recording = UIScreen.main.isCaptured
|
|
145
|
+
observers.append(center.addObserver(
|
|
146
|
+
forName: UIApplication.userDidTakeScreenshotNotification, object: nil, queue: .main
|
|
147
|
+
) { [weak self] _ in self?.emit(3) })
|
|
148
|
+
observers.append(center.addObserver(
|
|
149
|
+
forName: UIScreen.capturedDidChangeNotification, object: nil, queue: .main
|
|
150
|
+
) { [weak self] _ in self?.refreshRecording() })
|
|
151
|
+
for name in [UIApplication.willResignActiveNotification, UIApplication.didEnterBackgroundNotification] {
|
|
152
|
+
observers.append(center.addObserver(forName: name, object: nil, queue: .main) {
|
|
153
|
+
[weak self] _ in
|
|
154
|
+
self?.showSwitcher()
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
observers.append(center.addObserver(
|
|
158
|
+
forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main
|
|
159
|
+
) { [weak self] _ in
|
|
160
|
+
self?.removeSwitcherWindow()
|
|
161
|
+
self?.refreshRecording()
|
|
162
|
+
})
|
|
163
|
+
observers.append(center.addObserver(
|
|
164
|
+
forName: NSNotification.Name.RCTTriggerReloadCommand, object: nil, queue: .main
|
|
165
|
+
) { [weak self] _ in self?.reset() })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private func stopObserversIfIdle() {
|
|
169
|
+
guard owners.isEmpty && listeners.isEmpty else { return }
|
|
170
|
+
for observer in observers { NotificationCenter.default.removeObserver(observer) }
|
|
171
|
+
observers.removeAll()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private func reset() {
|
|
175
|
+
owners.removeAll()
|
|
176
|
+
listeners.removeAll()
|
|
177
|
+
secureField?.isSecureTextEntry = false
|
|
178
|
+
removeRecordWindow()
|
|
179
|
+
removeSwitcherWindow()
|
|
180
|
+
stopObserversIfIdle()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private func refreshRecording() {
|
|
184
|
+
let captured = UIScreen.main.isCaptured
|
|
185
|
+
if captured != recording {
|
|
186
|
+
recording = captured
|
|
187
|
+
emit(captured ? 1 : 2)
|
|
188
|
+
}
|
|
189
|
+
if captured && !owners.isEmpty {
|
|
190
|
+
if recordWindow == nil { recordWindow = makeOverlay() }
|
|
191
|
+
} else {
|
|
192
|
+
removeRecordWindow()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private func secureScreenshot(window: UIWindow) throws {
|
|
197
|
+
if let secureField, protectedWindow === window {
|
|
198
|
+
secureField.isSecureTextEntry = true
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
let field = UITextField(frame: .zero)
|
|
202
|
+
field.isUserInteractionEnabled = false
|
|
203
|
+
field.isSecureTextEntry = true
|
|
204
|
+
field.layoutIfNeeded()
|
|
205
|
+
guard let canvas = field.layer.sublayers?.last else {
|
|
206
|
+
throw failure("The secure text layer is unavailable.")
|
|
207
|
+
}
|
|
208
|
+
// Keep the upstream secure-layer technique; no photo or file access is involved.
|
|
209
|
+
window.layer.superlayer?.addSublayer(field.layer)
|
|
210
|
+
canvas.addSublayer(window.layer)
|
|
211
|
+
secureField?.isSecureTextEntry = false
|
|
212
|
+
secureField = field
|
|
213
|
+
protectedWindow = window
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private func makeOverlay() -> UIWindow? {
|
|
217
|
+
guard let protectedWindow = protectedWindow ?? window else { return nil }
|
|
218
|
+
let overlay: UIWindow
|
|
219
|
+
if let scene = protectedWindow.windowScene {
|
|
220
|
+
overlay = UIWindow(windowScene: scene)
|
|
221
|
+
overlay.frame = protectedWindow.frame
|
|
222
|
+
} else {
|
|
223
|
+
overlay = UIWindow(frame: protectedWindow.frame)
|
|
224
|
+
}
|
|
225
|
+
let controller = UIViewController()
|
|
226
|
+
controller.view.backgroundColor = .white
|
|
227
|
+
overlay.rootViewController = controller
|
|
228
|
+
overlay.windowLevel = .alert + 1
|
|
229
|
+
overlay.isHidden = false
|
|
230
|
+
return overlay
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private func showSwitcher() {
|
|
234
|
+
guard !owners.isEmpty else { return }
|
|
235
|
+
if switcherWindow == nil { switcherWindow = makeOverlay() }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private func removeRecordWindow() {
|
|
239
|
+
recordWindow?.isHidden = true
|
|
240
|
+
recordWindow = nil
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private func removeSwitcherWindow() {
|
|
244
|
+
switcherWindow?.isHidden = true
|
|
245
|
+
switcherWindow = nil
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private func failure(_ message: String) -> NSError {
|
|
249
|
+
NSError(domain: "CaptureProtection", code: 1, userInfo: [NSLocalizedDescriptionKey: message])
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import NitroModules
|
|
2
|
+
import UIKit
|
|
3
|
+
import XCTest
|
|
4
|
+
@testable import ReactNativeCaptureProtection
|
|
5
|
+
|
|
6
|
+
@MainActor
|
|
7
|
+
final class CaptureProtectionTests: XCTestCase {
|
|
8
|
+
func testNativeSubscriptionsRemainScopedToTheirObjects() throws {
|
|
9
|
+
let first = ReactNativeCaptureProtection()
|
|
10
|
+
let second = ReactNativeCaptureProtection()
|
|
11
|
+
var firstEvents: [Double] = []
|
|
12
|
+
var secondEvents: [Double] = []
|
|
13
|
+
try first.setListener { firstEvents.append($0) }
|
|
14
|
+
try second.setListener { secondEvents.append($0) }
|
|
15
|
+
NotificationCenter.default.post(name: UIApplication.userDidTakeScreenshotNotification, object: nil)
|
|
16
|
+
XCTAssertEqual(firstEvents, [3])
|
|
17
|
+
XCTAssertEqual(secondEvents, [3])
|
|
18
|
+
try first.setListener(callback: nil)
|
|
19
|
+
NotificationCenter.default.post(name: UIApplication.userDidTakeScreenshotNotification, object: nil)
|
|
20
|
+
XCTAssertEqual(firstEvents, [3])
|
|
21
|
+
XCTAssertEqual(secondEvents, [3, 3])
|
|
22
|
+
try second.setListener(callback: nil)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func testDisposalStopsNativeEvents() async throws {
|
|
26
|
+
let instance = ReactNativeCaptureProtection()
|
|
27
|
+
var events: [Double] = []
|
|
28
|
+
try instance.setListener { events.append($0) }
|
|
29
|
+
instance.dispose()
|
|
30
|
+
// allow is queued after disposal and provides a deterministic UI-thread barrier.
|
|
31
|
+
try await instance.allow().await()
|
|
32
|
+
NotificationCenter.default.post(name: UIApplication.userDidTakeScreenshotNotification, object: nil)
|
|
33
|
+
XCTAssertTrue(events.isEmpty)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func testOneOwnerCannotReleaseAnotherOwnersSecureLayer() async throws {
|
|
37
|
+
let window = try XCTUnwrap(UIApplication.shared.delegate?.window ?? nil)
|
|
38
|
+
let first = ReactNativeCaptureProtection()
|
|
39
|
+
let second = ReactNativeCaptureProtection()
|
|
40
|
+
try await first.prevent().await()
|
|
41
|
+
try await second.prevent().await()
|
|
42
|
+
let field = window.layer.superlayer?.superlayer?.delegate as? UITextField
|
|
43
|
+
XCTAssertNotNil(field)
|
|
44
|
+
XCTAssertEqual(field?.isSecureTextEntry, true)
|
|
45
|
+
try await first.allow().await()
|
|
46
|
+
XCTAssertEqual(field?.isSecureTextEntry, true)
|
|
47
|
+
try await second.allow().await()
|
|
48
|
+
XCTAssertEqual(field?.isSecureTextEntry, false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["ReactNativeCaptureProtection.nitro.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { NitroModules } from 'react-native-nitro-modules';
|
|
4
|
+
// Adapted from react-native-capture-protection 2.3.0; see LICENSE.upstream.
|
|
5
|
+
export let CaptureEventType = /*#__PURE__*/function (CaptureEventType) {
|
|
6
|
+
CaptureEventType[CaptureEventType["NONE"] = 0] = "NONE";
|
|
7
|
+
CaptureEventType[CaptureEventType["RECORDING"] = 1] = "RECORDING";
|
|
8
|
+
CaptureEventType[CaptureEventType["END_RECORDING"] = 2] = "END_RECORDING";
|
|
9
|
+
CaptureEventType[CaptureEventType["CAPTURED"] = 3] = "CAPTURED";
|
|
10
|
+
CaptureEventType[CaptureEventType["APP_SWITCHING"] = 4] = "APP_SWITCHING";
|
|
11
|
+
CaptureEventType[CaptureEventType["UNKNOWN"] = 5] = "UNKNOWN";
|
|
12
|
+
CaptureEventType[CaptureEventType["ALLOW"] = 8] = "ALLOW";
|
|
13
|
+
CaptureEventType[CaptureEventType["PREVENT_SCREEN_CAPTURE"] = 16] = "PREVENT_SCREEN_CAPTURE";
|
|
14
|
+
CaptureEventType[CaptureEventType["PREVENT_SCREEN_RECORDING"] = 32] = "PREVENT_SCREEN_RECORDING";
|
|
15
|
+
CaptureEventType[CaptureEventType["PREVENT_SCREEN_APP_SWITCHING"] = 64] = "PREVENT_SCREEN_APP_SWITCHING";
|
|
16
|
+
return CaptureEventType;
|
|
17
|
+
}({});
|
|
18
|
+
let native;
|
|
19
|
+
const subscribers = new Map();
|
|
20
|
+
function getNative() {
|
|
21
|
+
native ??= NitroModules.createHybridObject('ReactNativeCaptureProtection');
|
|
22
|
+
return native;
|
|
23
|
+
}
|
|
24
|
+
export const CaptureProtection = {
|
|
25
|
+
prevent() {
|
|
26
|
+
return getNative().prevent();
|
|
27
|
+
},
|
|
28
|
+
allow() {
|
|
29
|
+
return getNative().allow();
|
|
30
|
+
},
|
|
31
|
+
addListener(callback) {
|
|
32
|
+
if (subscribers.size >= 128) {
|
|
33
|
+
throw new Error('Capture protection supports at most 128 subscribers.');
|
|
34
|
+
}
|
|
35
|
+
const id = Symbol();
|
|
36
|
+
subscribers.set(id, callback);
|
|
37
|
+
if (subscribers.size === 1) {
|
|
38
|
+
try {
|
|
39
|
+
getNative().setListener(event => {
|
|
40
|
+
// Read current subscriptions so queued events cannot reach removed callers.
|
|
41
|
+
for (const [key, listener] of Array.from(subscribers)) {
|
|
42
|
+
if (subscribers.has(key)) {
|
|
43
|
+
listener(event);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
subscribers.delete(id);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
remove() {
|
|
54
|
+
if (subscribers.delete(id) && subscribers.size === 0) {
|
|
55
|
+
getNative().setListener(undefined);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NitroModules","CaptureEventType","native","subscribers","Map","getNative","createHybridObject","CaptureProtection","prevent","allow","addListener","callback","size","Error","id","Symbol","set","setListener","event","key","listener","Array","from","has","error","delete","remove","undefined"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AAGzD;AACA,WAAYC,gBAAgB,0BAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAhBA,gBAAgB,CAAhBA,gBAAgB;EAAA,OAAhBA,gBAAgB;AAAA;AAc5B,IAAIC,MAAgD;AACpD,MAAMC,WAAW,GAAG,IAAIC,GAAG,CAA0B,CAAC;AAEtD,SAASC,SAASA,CAAA,EAAiC;EACjDH,MAAM,KAAKF,YAAY,CAACM,kBAAkB,CACxC,8BACF,CAAC;EACD,OAAOJ,MAAM;AACf;AAEA,OAAO,MAAMK,iBAAiB,GAAG;EAC/BC,OAAOA,CAAA,EAAkB;IACvB,OAAOH,SAAS,CAAC,CAAC,CAACG,OAAO,CAAC,CAAC;EAC9B,CAAC;EACDC,KAAKA,CAAA,EAAkB;IACrB,OAAOJ,SAAS,CAAC,CAAC,CAACI,KAAK,CAAC,CAAC;EAC5B,CAAC;EACDC,WAAWA,CAACC,QAAyB,EAAsB;IACzD,IAAIR,WAAW,CAACS,IAAI,IAAI,GAAG,EAAE;MAC3B,MAAM,IAAIC,KAAK,CAAC,sDAAsD,CAAC;IACzE;IACA,MAAMC,EAAE,GAAGC,MAAM,CAAC,CAAC;IACnBZ,WAAW,CAACa,GAAG,CAACF,EAAE,EAAEH,QAAQ,CAAC;IAC7B,IAAIR,WAAW,CAACS,IAAI,KAAK,CAAC,EAAE;MAC1B,IAAI;QACFP,SAAS,CAAC,CAAC,CAACY,WAAW,CAAEC,KAAK,IAAK;UACjC;UACA,KAAK,MAAM,CAACC,GAAG,EAAEC,QAAQ,CAAC,IAAIC,KAAK,CAACC,IAAI,CAACnB,WAAW,CAAC,EAAE;YACrD,IAAIA,WAAW,CAACoB,GAAG,CAACJ,GAAG,CAAC,EAAE;cACxBC,QAAQ,CAACF,KAAyB,CAAC;YACrC;UACF;QACF,CAAC,CAAC;MACJ,CAAC,CAAC,OAAOM,KAAK,EAAE;QACdrB,WAAW,CAACsB,MAAM,CAACX,EAAE,CAAC;QACtB,MAAMU,KAAK;MACb;IACF;IACA,OAAO;MACLE,MAAMA,CAAA,EAAG;QACP,IAAIvB,WAAW,CAACsB,MAAM,CAACX,EAAE,CAAC,IAAIX,WAAW,CAACS,IAAI,KAAK,CAAC,EAAE;UACpDP,SAAS,CAAC,CAAC,CAACY,WAAW,CAACU,SAAS,CAAC;QACpC;MACF;IACF,CAAC;EACH;AACF,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
export interface ReactNativeCaptureProtection extends HybridObject<{
|
|
3
|
+
ios: 'swift';
|
|
4
|
+
android: 'kotlin';
|
|
5
|
+
}> {
|
|
6
|
+
prevent(): Promise<void>;
|
|
7
|
+
allow(): Promise<void>;
|
|
8
|
+
setListener(callback: ((event: number) => void) | undefined): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=ReactNativeCaptureProtection.nitro.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReactNativeCaptureProtection.nitro.d.ts","sourceRoot":"","sources":["../../../src/ReactNativeCaptureProtection.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,4BACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC;CACpE"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare enum CaptureEventType {
|
|
2
|
+
NONE = 0,
|
|
3
|
+
RECORDING = 1,
|
|
4
|
+
END_RECORDING = 2,
|
|
5
|
+
CAPTURED = 3,
|
|
6
|
+
APP_SWITCHING = 4,
|
|
7
|
+
UNKNOWN = 5,
|
|
8
|
+
ALLOW = 8,
|
|
9
|
+
PREVENT_SCREEN_CAPTURE = 16,
|
|
10
|
+
PREVENT_SCREEN_RECORDING = 32,
|
|
11
|
+
PREVENT_SCREEN_APP_SWITCHING = 64
|
|
12
|
+
}
|
|
13
|
+
type CaptureCallback = (event: CaptureEventType) => void;
|
|
14
|
+
export declare const CaptureProtection: {
|
|
15
|
+
prevent(): Promise<void>;
|
|
16
|
+
allow(): Promise<void>;
|
|
17
|
+
addListener(callback: CaptureCallback): {
|
|
18
|
+
remove(): void;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAIA,oBAAY,gBAAgB;IAC1B,IAAI,IAAI;IACR,SAAS,IAAI;IACb,aAAa,IAAI;IACjB,QAAQ,IAAI;IACZ,aAAa,IAAI;IACjB,OAAO,IAAI;IACX,KAAK,IAAI;IACT,sBAAsB,KAAK;IAC3B,wBAAwB,KAAK;IAC7B,4BAA4B,KAAK;CAClC;AAED,KAAK,eAAe,GAAG,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;AAWzD,eAAO,MAAM,iBAAiB;eACjB,OAAO,CAAC,IAAI,CAAC;aAGf,OAAO,CAAC,IAAI,CAAC;0BAGA,eAAe,GAAG;QAAE,MAAM,IAAI,IAAI,CAAA;KAAE;CA6B3D,CAAC"}
|
package/nitro.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cxxNamespace": ["reactnativecaptureprotection"],
|
|
3
|
+
"ios": {
|
|
4
|
+
"iosModuleName": "ReactNativeCaptureProtection"
|
|
5
|
+
},
|
|
6
|
+
"android": {
|
|
7
|
+
"androidNamespace": ["reactnativecaptureprotection"],
|
|
8
|
+
"androidCxxLibName": "reactnativecaptureprotection"
|
|
9
|
+
},
|
|
10
|
+
"autolinking": {
|
|
11
|
+
"ReactNativeCaptureProtection": {
|
|
12
|
+
"swift": "ReactNativeCaptureProtection",
|
|
13
|
+
"kotlin": "ReactNativeCaptureProtection"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"ignorePaths": ["node_modules"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// JFunc_void_double.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include <functional>
|
|
12
|
+
|
|
13
|
+
#include <functional>
|
|
14
|
+
#include <NitroModules/JNICallable.hpp>
|
|
15
|
+
|
|
16
|
+
namespace margelo::nitro::reactnativecaptureprotection {
|
|
17
|
+
|
|
18
|
+
using namespace facebook;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Represents the Java/Kotlin callback `(event: Double) -> Unit`.
|
|
22
|
+
* This can be passed around between C++ and Java/Kotlin.
|
|
23
|
+
*/
|
|
24
|
+
struct JFunc_void_double: public jni::JavaClass<JFunc_void_double> {
|
|
25
|
+
public:
|
|
26
|
+
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/reactnativecaptureprotection/Func_void_double;";
|
|
27
|
+
|
|
28
|
+
public:
|
|
29
|
+
/**
|
|
30
|
+
* Invokes the function this `JFunc_void_double` instance holds through JNI.
|
|
31
|
+
*/
|
|
32
|
+
void invoke(double event) const {
|
|
33
|
+
static const auto method = javaClassStatic()->getMethod<void(double /* event */)>("invoke");
|
|
34
|
+
method(self(), event);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An implementation of Func_void_double that is backed by a C++ implementation (using `std::function<...>`)
|
|
40
|
+
*/
|
|
41
|
+
class JFunc_void_double_cxx final: public jni::HybridClass<JFunc_void_double_cxx, JFunc_void_double> {
|
|
42
|
+
public:
|
|
43
|
+
static jni::local_ref<JFunc_void_double::javaobject> fromCpp(const std::function<void(double /* event */)>& func) {
|
|
44
|
+
return JFunc_void_double_cxx::newObjectCxxArgs(func);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public:
|
|
48
|
+
/**
|
|
49
|
+
* Invokes the C++ `std::function<...>` this `JFunc_void_double_cxx` instance holds.
|
|
50
|
+
*/
|
|
51
|
+
void invoke_cxx(double event) {
|
|
52
|
+
_func(event);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public:
|
|
56
|
+
[[nodiscard]]
|
|
57
|
+
inline const std::function<void(double /* event */)>& getFunction() const {
|
|
58
|
+
return _func;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public:
|
|
62
|
+
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/reactnativecaptureprotection/Func_void_double_cxx;";
|
|
63
|
+
static void registerNatives() {
|
|
64
|
+
registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_double_cxx::invoke_cxx)});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private:
|
|
68
|
+
explicit JFunc_void_double_cxx(const std::function<void(double /* event */)>& func): _func(func) { }
|
|
69
|
+
|
|
70
|
+
private:
|
|
71
|
+
friend HybridBase;
|
|
72
|
+
std::function<void(double /* event */)> _func;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
} // namespace margelo::nitro::reactnativecaptureprotection
|