One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Screen Capture

Observe capture state, save PNGs, and block screenshots

One.ScreenCapture watches whether the screen is recorded or mirrored, notifies after a screenshot lands, saves a PNG of the app window or one view, and hides content from screenshots and recordings. Reach for it for sensitive screens: receipts, tickets, health data, and audit trails.

On iOS state comes from the scene capture state and screenshots hide behind a secure-layer window move, with rendering through UIGraphicsImageRenderer. On Android state comes from window recording visibility on API 35+, screenshots notify through the API 34 activity callback, prevention sets FLAG_SECURE, and captures copy through PixelCopy on API 26+. On web state reads unspecified, prevention does nothing, and captures reject.

Usage

import { useEffect, useState } from 'react'
import { One } from 'one'
function useCaptureState() {
const [captured, setCaptured] = useState(false)
useEffect(() => {
One.ScreenCapture.getState().then((state) => {
setCaptured(state === 'active')
})
return One.ScreenCapture.addStateListener((state) => {
setCaptured(state === 'active')
})
}, [])
return captured
}

State is active while recorded or mirrored, inactive otherwise, and unspecified where the platform cannot tell. The listener reports changes after registration; the one-shot read seeds the first render.

How it works

getState resolves from the active window scene and rejects without one. The screenshot listener receives a Unix millisecond timestamp stamped when the notification arrives, after the image is captured, so it can audit but never prevent or redact that shot. On Android the callback is activity-scoped: ADB and instrumentation captures do not trigger it, and below API 34 it stays silent.

captureWindow saves the current app window as a PNG while the app is foregrounded: a file:// URI under the screen-capture cache folder, pixel dimensions at the window’s native scale, and the PNG byte size. The PNG holds the window’s rendered content only, excluding system UI from other windows such as the status bar. It never triggers the screenshot listener. captureView renders one mounted view subtree at call time from its native tag: set collapsable={false} on the target, since Fabric can otherwise mount children beside it and silently omit them. On Android the tag must resolve to a view attached to the current activity’s window. Both reject without a scene, on render, encode, or file failures, and below API 26 on Android; the view call adds an unmounted-tag error. Delete the file when finished.

prevent hides content once every held key agrees: on iOS the window moves behind a secure layer so shots come out blank, with a black overlay while captured; on Android the window gains FLAG_SECURE, which also blanks the app switcher preview. Keys default to default; prevention arms on the first held key and releases when the last key is allowed, so overlapping screens never conflict. Allowing an unheld key does nothing. While mounted, prefer usePreventScreenCapture(key), which prevents on mount and allows on unmount.

API reference

Methods: observingTypeDescription
getState() => Promise<ScreenCaptureState>Active, inactive, or unspecified, right now.
() => Promise<ScreenCaptureState>Active, inactive, or unspecified, right now.
addStateListener(onChange: (state: ScreenCaptureState) => void) => () => voidHear state changes. Returns a remover.
(onChange: (state: ScreenCaptureState) => void) => () => voidHear state changes. Returns a remover.
addScreenshotListener(onScreenshot: (timestampMs: number) => void) => () => voidHear after a screenshot. Returns a remover.
(onScreenshot: (timestampMs: number) => void) => () => voidHear after a screenshot. Returns a remover.
Methods: capturingTypeDescription
captureWindow() => Promise<ScreenCaptureResult>Save the app window as a PNG file.
() => Promise<ScreenCaptureResult>Save the app window as a PNG file.
captureView(viewTag: number) => Promise<ScreenCaptureResult>Save one mounted view subtree as a PNG file.
(viewTag: number) => Promise<ScreenCaptureResult>Save one mounted view subtree as a PNG file.
Methods: preventingTypeDescription
prevent(key?: string) => Promise<void>Hide content. Arms on the first held key.
(key?: string) => Promise<void>Hide content. Arms on the first held key.
allow(key?: string) => Promise<void>Release one key. Last key shows content.
(key?: string) => Promise<void>Release one key. Last key shows content.
isPrevented() => Promise<boolean>Whether content hides right now.
() => Promise<boolean>Whether content hides right now.

The usePreventScreenCapture hook is a named export from one.

Capture resultTypeDescription
uristringThe PNG file, under the capture cache folder.
stringThe PNG file, under the capture cache folder.
widthnumberPixels at the native scale.
numberPixels at the native scale.
heightnumberPixels at the native scale.
numberPixels at the native scale.
sizenumberPNG file size in bytes.
numberPNG file size in bytes.

Platform differences

iOSAndroidWeb
StateScene valueAPI 35 plusUnspecified
Shot noticeAfter captureAPI 34 plusNever
CaptureWindow renderPixel copyRejects
PreventSecure layerSecure flagNo effect

Limits and gotchas

Examples

Guard a receipt screen

import { usePreventScreenCapture } from 'one'
function Receipt() {
usePreventScreenCapture('receipt')
return null
}

Mounting arms prevention for the receipt key and unmounting releases it. Overlapping guarded screens hold their own keys without conflict.

Audit screenshots

import { useEffect } from 'react'
import { One } from 'one'
function useScreenshotAudit(log: (at: number) => void) {
useEffect(() => {
return One.ScreenCapture.addScreenshotListener((timestampMs) => {
log(timestampMs)
})
}, [log])
}

The timestamp marks when the notification arrived, which trails the capture by a small margin. Pair the log with the visible screen name for a useful audit row.

Share a card as an image

import { useRef } from 'react'
import { Button, findNodeHandle, View } from 'react-native'
import { One } from 'one'
function ShareCard({ share }: { share: (uri: string) => Promise<void> }) {
const target = useRef<View>(null)
async function capture() {
const tag = findNodeHandle(target.current)
if (tag == null) throw new Error('card is not mounted')
const image = await One.ScreenCapture.captureView(tag)
try {
await share(image.uri)
} finally {
await One.FileSystem.delete(image.uri)
}
}
return (
<>
<View ref={target} collapsable={false}>
{/* card content */}
</View>
<Button title="Share card" onPress={capture} />
</>
)
}

collapsable={false} keeps Fabric from hoisting the children out of the captured subtree. The temp PNG is shared once, then deleted.

Edit this page on GitHub.