Screenshots for NativePHP Mobile#
Lock down sensitive screens. Catch capture attempts. Respond instantly.#
The privacy layer your mobile app was missing. Block screenshots and screen recordings on screens showing account balances, medical records, exam questions or premium content — and get a real-time event the moment a user tries to capture something they shouldn't.
Why this plugin?#
- 🔒 Block screenshots & screen recordings —
FLAG_SECUREon Android, privacy overlay on iOS. One line of PHP. - 🛎️ Detect every capture attempt — fire a Laravel event the instant a user screenshots or starts recording.
- 📸 Capture programmatically — grab the current screen from server code for bug reports, feedback, or audit trails.
- 📱 Battle-tested on both platforms — iOS 17+ and Android 8+ with platform-native implementations.
Features at a glance#
| Feature | Android | iOS |
|---|---|---|
| Programmatic screenshot capture | ✅ | ✅ |
| Screenshot detection | ✅ | ✅ |
| Screenshot blocking | ✅ | ✅ privacy overlay |
| Screen recording detection | ✅ Android 15+ | ✅ |
| Screen recording blocking | ✅ | ✅ privacy overlay |
Perfect for#
Banking & fintech · Healthcare portals · Exam proctoring · Enterprise document viewers · Premium video & streaming · Legal & confidential communications
Installation#
composer require srwiez/nativephp-mobile-screenshots php artisan vendor:publish --tag=nativephp-plugins-provider php artisan native:plugin:register srwiez/nativephp-mobile-screenshots
Quick Start#
use SRWieZ\NativePHP\Mobile\Screenshots\Facades\Screenshots; // Protect a sensitive screenScreenshots::block(); // Detect when users take screenshotsScreenshots::startDetection(); // Capture the screen programmatically$captureId = Screenshots::capture();
API Reference#
Screenshot Detection#
Detect when users take screenshots of your app.
// Start listening for screenshotsScreenshots::startDetection(); // Stop listeningScreenshots::stopDetection();
| Platform | Behavior |
|---|---|
| Android | Uses ScreenCaptureCallback (API 34+, no permission prompt). Pre-14 falls back to a MediaStore observer that reports the file path — your app must declare READ_MEDIA_IMAGES (API 33+) or READ_EXTERNAL_STORAGE itself for that. |
| iOS | Uses userDidTakeScreenshotNotification. Path not available. |
Note
Blocking and detection compose: with block() active on Android 14+,
ScreenCaptureCallback still fires when the user attempts a screenshot —
the system saves a blanked image and shows its own "app detected this
screenshot" notice (verified on Android 16). So you can block sensitive
screens and still audit capture attempts.
Screenshot Blocking#
Prevent screenshots from being captured.
// Block screenshotsScreenshots::block(); // Customize or disable the iOS recording overlayScreenshots::block(privacyText: 'Recording is disabled on this screen');Screenshots::block(privacyOverlay: false); // Allow screenshots againScreenshots::allow(); // Keep blocking after an app restart (see "Blocking from the first frame")Screenshots::block(persist: true); // Check current status$blocked = Screenshots::isBlocked();
| Platform | Behavior |
|---|---|
| Android | Uses FLAG_SECURE - completely prevents screenshots and screen recordings. Screenshots appear black. |
| iOS | Secure text field technique: the OS compositor blanks the window in screenshots and recordings. Optionally shows a privacy overlay with a configurable message while the screen is recorded or mirrored. |
Caution
iOS Limitation: Apple provides no official API to prevent screenshots. This plugin uses the secure text field technique (the same approach Expo ships) — it relies on undocumented OS behavior, works only on physical devices, and should be re-tested on each new iOS release. For sensitive data on iOS, combine blocking with detection to log and respond to capture attempts.
Blocking from the first frame#
block() takes effect when PHP calls it. After an app restart, everything shown
before your code calls block() again can be captured: the splash screen, the
first render of a sensitive screen. Pass persist: true to close that gap.
// Block now, and again on every app startScreenshots::block(persist: true); // Allow now, and forget the persisted blockScreenshots::allow(persist: true);
The plugin stores the choice on the device and restores the block while the app starts, before the PHP runtime boots and before the web view shows anything:
| Platform | When the block comes back |
|---|---|
| Android | When NativePHP registers its plugins. On NativePHP 3.x that happens while the main activity is created, before its first frame. On NativePHP 4.x it happens right after the splash screen's first frame, so the splash itself is not covered. |
| iOS | At app launch, through NativePHP's launch hook, with the overlay options you passed to block(), before your first page is shown. |
Without persist, block() and allow() behave as before and leave the stored
choice alone. A plain allow() therefore lifts a persisted block for the
current session only; it comes back at the next start.
Programmatic Screen Capture#
Capture the current screen from your app code.
// Returns a capture ID to correlate with events$captureId = Screenshots::capture();
Screen Recording Detection iOS 17+ · Android 15+#
Detect and respond to screen recording attempts.
// Check if currently being recorded (on Android, start detection first)$recording = Screenshots::isRecording(); // bool // Start listening for recording changesScreenshots::startRecordingDetection(); // Stop listeningScreenshots::stopRecordingDetection();
Checking availability#
PHP-only checks tell you what the running app provides. Both return false
outside a NativePHP app (tests, web preview) and have no JavaScript equivalent.
Screenshots::isAvailable(); // the plugin's native side is installedScreenshots::canDetectRecording(); // recording detection works on this device
canDetectRecording() is false on Android 14 and older, where recording
detection is registered but unsupported; it asks isRecording() for that. On
NativePHP v4 you can test the fallback path with
FakeBridge::enable()->withoutCapability('MobileScreenshots.StartRecordingDetection').
Events#
| Event | Fired when | Payload keys |
|---|---|---|
ScreenshotDetected |
The user takes a screenshot (after startDetection()) |
path (Android MediaStore detection only), timestamp, method (Android only: ScreenCaptureCallback or ContentObserver) |
ScreenshotCaptured |
A capture() finished |
path, id (matches the value capture() returned), success |
ScreenshotCaptureFailed |
A capture() failed |
error, id |
ScreenRecordingStarted iOS 17+ · Android 15+ |
Recording or mirroring starts (after startRecordingDetection()) |
timestamp |
ScreenRecordingStopped iOS 17+ · Android 15+ |
Recording or mirroring stops | timestamp |
All events live in the SRWieZ\NativePHP\Mobile\Screenshots\Events namespace and
timestamps are Unix seconds.
Pick the listener style that matches where your code runs. Handler parameters are bound by payload key name, so name them after the keys above.
NativePHP v4 native screens. Put Native\Mobile\Attributes\On on a method of
a Native\Mobile\Edge\NativeComponent and pass the event class. The attribute adds
the native: prefix for you.
use Native\Mobile\Attributes\On;use Native\Mobile\Edge\NativeComponent;use SRWieZ\NativePHP\Mobile\Screenshots\Events\ScreenshotDetected; class AccountScreen extends NativeComponent{ #[On(ScreenshotDetected::class)] public function screenshotDetected(?string $path = null, ?int $timestamp = null): void { // $path is only set by Android's MediaStore detection }}
Web view screens with Livewire. Use Livewire's own On attribute with the
native: prefix in front of the event class.
use Livewire\Attributes\On;use Livewire\Component;use SRWieZ\NativePHP\Mobile\Screenshots\Events\ScreenshotCaptured; class AccountScreen extends Component{ #[On('native:'.ScreenshotCaptured::class)] public function screenshotCaptured(string $path, ?string $id = null): void { // Process the captured image at $path }}
Plain Laravel listeners. In web view apps, Event::listen receives the
event object. It is not tied to a screen, which makes it the right place for
audit logging. On NativePHP 4.6.1+ and 3.3.11+ this needs plugin 2.1.0 or later.
On v4 native screens, plugin events only reach the screen's #[On] handlers.
use Illuminate\Support\Facades\Event;use Illuminate\Support\Facades\Log;use SRWieZ\NativePHP\Mobile\Screenshots\Events\ScreenshotDetected; Event::listen(function (ScreenshotDetected $event) { Log::warning('User took a screenshot', ['at' => $event->timestamp]);});
Note
#[OnNative(ScreenshotDetected::class)] (Native\Mobile\Attributes\OnNative)
is the pre-v4 form. It still works wherever Livewire is installed, but it
extends Livewire's attribute, so a v4 app without Livewire never reads it.
JavaScript API#
import { mobileScreenshots } from '../../vendor/srwiez/nativephp-mobile-screenshots/resources/js/mobileScreenshots.js'; // Detectionawait mobileScreenshots.startDetection();await mobileScreenshots.stopDetection(); // Blockingawait mobileScreenshots.block();await mobileScreenshots.block({ privacy_overlay: false });await mobileScreenshots.block({ persist: true }); // also after an app restartawait mobileScreenshots.allow();await mobileScreenshots.allow({ persist: true }); // forget the persisted blockconst { blocked } = await mobileScreenshots.isBlocked(); // Captureconst { id } = await mobileScreenshots.capture(); // Recording detection (iOS 17+, Android 15+)const { recording } = await mobileScreenshots.isRecording();await mobileScreenshots.startRecordingDetection();await mobileScreenshots.stopRecordingDetection();
Platform Comparison#
Android#
Android provides robust, system-level protection through FLAG_SECURE:
- Screenshots: Completely blocked - appear as black images
- Screen Recording: Completely blocked - shows black screen
- Screen Mirroring: Blocked
- Reliability: Very high - enforced at the window manager level
iOS#
iOS has more limited APIs, so this plugin uses creative workarounds:
- Screenshots: Blocked best-effort via the secure text field technique (undocumented OS behavior, device-only); always detectable instantly
- Screen Recording: Detected via
capturedDidChangeNotification+UIScreen.isCaptured(the newer sceneCaptureState trait lags the notification, so it is deliberately not used);block()can also show a privacy overlay while recording - Privacy Overlay: Optional black screen with a configurable message during recording/mirroring
- Reliability: Best-effort - determined users may still capture content
Recommended Strategy#
// For maximum protection on both platforms: // 1. Always block when showing sensitive contentScreenshots::block(); // 2. Start detection to log/audit attemptsScreenshots::startDetection(); // 3. Detect screen recording (iOS 17+, Android 15+; fires events only)Screenshots::startRecordingDetection(); // block() handles privacy screen during recording on iOS
Use Cases#
- Banking & Finance - Protect account balances, transaction history, card details
- Healthcare - Secure patient records, lab results, prescriptions
- Enterprise - Guard confidential documents, internal communications
- Education - Prevent cheating during online exams
- Media & Entertainment - Protect premium content from piracy
- Legal - Secure sensitive case documents and communications
Version Support#
| Platform | Minimum Version |
|---|---|
| Android | 8.0 (API 26) |
| iOS | 17.0 |
Features requiring higher versions:
- Android 15+ (API 35): Screen recording detection via
addScreenRecordingCallback - Android 14+ (API 34): Screenshot detection via
ScreenCaptureCallback, no permission needed - Pre-Android 14: Screenshot detection falls back to a
MediaStoreobserver — declareREAD_MEDIA_IMAGES(API 33+) orREAD_EXTERNAL_STORAGEin your own app if you need it; the plugin no longer injects these Play-policy-sensitive permissions - NativePHP Mobile v4 apps require Android 8.0 (API 26) and target iOS 18.2
The DETECT_SCREEN_CAPTURE and DETECT_SCREEN_RECORDING permissions are
install-time (no user prompt) and added automatically by the plugin.
Testing your app#
On NativePHP Mobile v4 the plugin registers testing macros on the
FakeBridge, so your app tests can assert in domain terms:
use Native\Mobile\Testing\FakeBridge; $bridge = FakeBridge::enable()->withRecording(); // ... exercise your component ... $bridge->assertBlocked();$bridge->assertDetectionStarted();
Support#
Bugs, questions, and feature requests should be reported at github.com/SRWieZ/nativephp-mobile-packages.