●MAX — Rork Max generates native Swift apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●NATIVE — It reaches AR/LiDAR scanning, Metal 3D games, widgets, Live Activities, and on-device Core ML●FUNDING — Rork raised $2.8M from a16z, with 743K monthly visits and 85% growth●PRICING — It's free to start, with paid plans beginning at $25 per month●FLOW — Describe your idea in plain English to get working code, a shareable test link, and iOS/Android builds●COMPARE — The original Rork builds cross-platform apps on Expo/React Native; choose the right tool per goal●MAX — Rork Max generates native Swift apps across iPhone, iPad, Watch, TV, Vision Pro, and iMessage●NATIVE — It reaches AR/LiDAR scanning, Metal 3D games, widgets, Live Activities, and on-device Core ML●FUNDING — Rork raised $2.8M from a16z, with 743K monthly visits and 85% growth●PRICING — It's free to start, with paid plans beginning at $25 per month●FLOW — Describe your idea in plain English to get working code, a shareable test link, and iOS/Android builds●COMPARE — The original Rork builds cross-platform apps on Expo/React Native; choose the right tool per goal
Before a Free Preview Walks Out via Screenshot: Detecting Screenshots and Screen Recording in Rork/Expo
How to protect paid preview images from screenshots and screen recording in a Rork/Expo app: the limits of expo-screen-capture, native isCaptured monitoring, and an iOS/Android-aware blur design.
A paid wallpaper you only meant to show as a single free preview gets carried off at full resolution with one screenshot. If you run an image-first app as an indie developer for any length of time, this casual leak becomes a problem you cannot route around. I have personally tried lowering preview quality, only to find that a Retina-resolution screenshot is still perfectly usable, which made the effort almost pointless.
What matters here is not perfect defense. Technically sealing off capture is impossible, at least on iOS. But simply changing the state from "grab everything in one tap" to "this takes a bit of effort" cuts most of the leakage. This article walks through a screenshot and screen-recording detection and blur implementation you can bolt onto a Rork-generated Expo app — including the spots where it tripped me up in production.
Decide up front: you can only stop the casual grab
Before touching code, setting expectations correctly is what matters most. This is not DRM. Anyone seriously determined to extract an image can photograph the screen with a second device, and nothing you do will stop that.
So the goal narrows to "discouraging the careless or only mildly motivated grab." In my case, what I want to protect is a few percent of revenue leaking out, not to build a fortress. Without drawing that line first, you keep imagining ways around your own detection, the implementation bloats, and your indie-developer hours melt away. Keep the work inside the range where deterrence still pays for itself.
What's possible differs sharply between iOS and Android
The first thing that confused me was that the tools available on iOS and Android are not symmetric. Laid out, it looks like this.
What you want
iOS
Android
Block capture/recording itself
Not possible (OS forbids it)
Possible (FLAG_SECURE)
Receive the "screenshot taken" fact
Possible (system notification)
Generally not possible
Detect active recording/mirroring
Possible (isCaptured)
Limited
In other words, Android leans naturally toward "make it un-capturable," while iOS can only choose "notice it happened and react." Given this asymmetry, trying to force both platforms into one shared code path is the wrong move; playing each on its own field ends up being the simplest.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can blur in real time during recording and mirroring — the exact case expo-screen-capture cannot catch — using native isCaptured monitoring
✦You'll be able to split what iOS and Android can and cannot detect, and apply FLAG_SECURE and a detection overlay where each actually fits
✦You'll gain a decision framework for treating preview leakage as realistic deterrence, not DRM, in an indie premium-image app
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Screenshot detection: expo-screen-capture is enough
Reacting to screenshots, and blocking capture on Android, can both be done with the official expo-screen-capture package alone. Starting here is the solid path.
import * as ScreenCapture from 'expo-screen-capture';import { useEffect } from 'react';// Call only on the screen that shows the premium previewexport function useScreenshotGuard(onShot: () => void) { useEffect(() => { // Android: block capture/recording at the OS level (screen goes black) ScreenCapture.preventScreenCaptureAsync('premium-preview'); // iOS: capture cannot be blocked, so only receive the "it happened" fact const sub = ScreenCapture.addScreenshotListener(() => { onShot(); // e.g. show the blur / log one leakage event }); return () => { sub.remove(); // Release on leaving. Never leave FLAG_SECURE on permanently ScreenCapture.allowScreenCaptureAsync('premium-preview'); }; }, [onShot]);}
The key is to scope preventScreenCaptureAsync to only while the preview screen is shown. Apply it app-wide and, on Android, legitimate users get blocked from sharing even their own settings screen, which boomerangs into support load. Block capture only when you enter the screen you truly want to protect, and always release on the way out.
The iOS addScreenshotListener only fires after the shot. The first frame cannot be prevented. So the common landing point is: on detection, drop a blur instantly, and use it as a signal of intent ("no more from here") and as leakage measurement, rather than pretending the saved image will contain only the preview.
Recording and mirroring are invisible to the official package
This is where production tripped me up the most. expo-screen-capture reacts to screenshots, but it does not tell you in real time that a screen recording is in progress or that the device is mirroring to an external display. Unlike a single screenshot, a recording keeps capturing the content for the entire few seconds the preview is visible. If you only defend against still images, the leakiest path stays wide open.
iOS provides a mechanism for exactly this. UIScreen.main.isCaptured returns whether the screen is currently being captured, and UIScreen.capturedDidChangeNotification reports changes to that state. Since the official package has no hook for this, adding a thin native module of your own turned out to be the realistic option.
A native module to read isCaptured in real time
With the Expo Modules API, a native module of a few dozen lines can bridge isCaptured monitoring to JS. You can keep it as a local module inside your development build without ejecting.
import ExpoModulesCoreimport UIKitpublic class CaptureGuardModule: Module { public func definition() -> ModuleDefinition { Name("CaptureGuard") // Event subscribed from JS Events("onCaptureChange") // Start monitoring and return the isCaptured value at start time Function("start") { () -> Bool in NotificationCenter.default.addObserver( forName: UIScreen.capturedDidChangeNotification, object: nil, queue: .main ) { [weak self] _ in self?.sendEvent("onCaptureChange", [ "isCaptured": UIScreen.main.isCaptured ]) } return UIScreen.main.isCaptured } }}
Having start()return the value at start time matters more than it looks. If a recording began before the app launched, or right after returning from the background, the notification may not fire. Re-reading the current value on every resume closes the "recording is on but the blur is off" gap. I once watched a leak slip through during testing precisely because I had not added this re-read on background resume.
The JS wrapper and hook look like this.
import { useEffect, useState } from 'react';import { requireNativeModule } from 'expo-modules-core';const CaptureGuard = requireNativeModule('CaptureGuard');export function useScreenCaptured(): boolean { const [captured, setCaptured] = useState(false); useEffect(() => { // start() returns the value at monitoring start (resume gap fix) setCaptured(CaptureGuard.start()); const sub = CaptureGuard.addListener( 'onCaptureChange', (e: { isCaptured: boolean }) => setCaptured(e.isCaptured), ); return () => sub.remove(); }, []); return captured;}
On the JS side, just toggle an overlay by state
Once the native side hands you a boolean for "am I being recorded right now," the React side's job becomes surprisingly simple. While captured is true, drop a blur over the preview. That is all.
import { View, Text, StyleSheet } from 'react-native';import { BlurView } from 'expo-blur';import { useScreenCaptured } from './useScreenCaptured';export function PremiumPreview({ children }: { children: React.ReactNode }) { const captured = useScreenCaptured(); return ( <View style={StyleSheet.absoluteFill}> {children} {captured && ( <BlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill}> <View style={styles.center}> <Text style={styles.note}> The preview is hidden while recording or mirroring is active </Text> </View> </BlurView> )} </View> );}const styles = StyleSheet.create({ center: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 }, note: { color: '#fff', fontSize: 15, textAlign: 'center' },});
The single line over the blur is there to explain to the user. A screen that goes dark with no warning reads as a bug and turns into a one-star review. Just saying "this is hidden on purpose" changes the impression enormously. Image-protecting behavior is one step away from a complaint, so I always attach wording that makes the reason obvious at a glance.
One caveat: when layering a <View> over children, relying on absoluteFill z-order alone can break stacking inside list-style components. Splitting the preview into its own dedicated (modal) screen and placing the overlay at its very front proved stable. Note that protecting against the snapshot taken the instant you send the app to the background needs a separate mechanism, covered in hiding the app-switcher snapshot.
On Android, "make it un-capturable" is the honest path
Android generally has no official hook for "receive the screenshot-taken fact" like iOS does. Instead, raising FLAG_SECURE makes the OS refuse capture, recording, and mirroring by blacking out the screen. Blocking it at the root is more reliable than detecting and reacting.
The earlier preventScreenCaptureAsync wraps this FLAG_SECURE. On Android you need no detection-overlay logic at all; toggling prevent/allow as you enter and leave the preview screen is the whole job. I do not run the native module I wrote for iOS on Android. This per-platform split is what keeps the code shortest.
One thing to watch: FLAG_SECURE also affects some legitimate features (accessibility tools and certain screen-sharing). The same principle applies — scope it to the screen you truly want to protect rather than the whole app.
The small calls that paid off in production
Finally, a few judgments that felt worth keeping after running this in the wild. Treating detection as a signal rather than a wall turned out to be the healthiest stance.
First, do not use screenshot detection to block immediately; use it to measure first. Knowing which preview images get captured, and how often, becomes the basis for redesigning what you even show for free. Pulling the images that hurt to lose out of the free tier moved revenue more directly than hardening detection ever did.
Second, keep the blur reversible. When recording ends, isCaptured returns to false and the overlay disappears on its own. A design that permanently locks once it detects something strands the user on a false positive. Lean toward state that flows in and out naturally.
Third, position this as part of the paid funnel. The experience of a protected preview itself is a quiet message: "this is not for carrying off — it is something you buy and keep." Selling images on the App Store, I have seen this "sense of being protected" nudge a purchase more than once. The ideal is when the defensive implementation doubles as a presentation of value. For where to store sensitive data such as auth tokens, the expo-secure-store and biometric-gate design is worth reading alongside this.
If you are starting out, add screenshot detection and Android's FLAG_SECURE with expo-screen-capture first, and just run the measurement. Once you confirm the recording path leaks more than you can ignore, add the isCaptured native module. That order wastes the least effort for an indie developer.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.