@joinflux/capacitor-sensitive-screen
v0.2.1
Published
Capacitor 6 plugin for per-route screen-capture and snapshot protection on Android and iOS.
Maintainers
Readme
capacitor-sensitive-screen
Capacitor 6 plugin for per-route screen-capture and snapshot protection on Android and iOS.
Expose two calls from your app — enable() when you navigate into a sensitive
route and disable() when you leave — and the plugin will:
- Android: set
WindowManager.LayoutParams.FLAG_SECUREon the activity window, which blocks screenshots, screen recording, and the Recents thumbnail. - iOS: when the app resigns active, cover the key window with an opaque overlay so the snapshot the OS stores for the App Switcher doesn't leak the sensitive view.
- Web: no-op. (Browsers don't expose anything analogous to FLAG_SECURE, and there's no OS snapshot to protect.)
Install
yarn add @joinflux/capacitor-sensitive-screen
npx cap syncRequirements:
- Capacitor
^6.0.0 - iOS
13.0+ - Android
minSdk 22
Usage
import { SensitiveScreen } from '@joinflux/capacitor-sensitive-screen';
import { useEffect } from 'react';
export function AccountStatementRoute() {
useEffect(() => {
SensitiveScreen.enable();
return () => {
SensitiveScreen.disable();
};
}, []);
// ...render sensitive content
}The plugin is stateful: a call to enable() keeps protection on until
disable() runs. If your app crashes or is force-killed while enabled,
Android's FLAG_SECURE is scoped to the window and is gone on relaunch; the
iOS overlay is only inserted on willResignActive, so there's nothing to clean
up on restart.
Customizing the iOS overlay
The iOS overlay defaults to opaque black. Pass options into enable() to
change it:
// Branded solid color
await SensitiveScreen.enable({
style: 'solid',
backgroundColor: '#0A0F1C',
imageName: 'LaunchLogo', // optional, rendered centered
});
// Or a system blur effect over whatever is underneath
await SensitiveScreen.enable({
style: 'blur',
blurStyle: 'regular',
});Options are applied on each enable() call — there is no separate configure
step. imageName must resolve via UIImage(named:), so ship the asset in
your iOS app target's asset catalog. Android and Web ignore these options:
Android's protection is drawn by the OS via FLAG_SECURE, and Web has no
overlay.
Because the overlay is inserted synchronously inside willResignActive, all
styling has to be handed to the native side up front — the plugin
deliberately does not round-trip to JS at snapshot time, which would be too
late to cover the App Switcher preview.
API
enable(...)
enable(options?: SensitiveScreenEnableOptions | undefined) => Promise<void>Turn on screen-capture / snapshot protection for the current app session.
On Android this adds FLAG_SECURE to the activity window, which blocks
screenshots, screen recording, and the Recents thumbnail.
On iOS this arms an overlay that is shown when the app resigns active,
covering the system snapshot used in the App Switcher. The overlay is
not inserted until willResignActive fires.
The options argument configures the iOS overlay's appearance. It is
ignored on Android (protection is drawn by the OS via FLAG_SECURE) and
Web (no-op). Each call replaces the previous options wholesale — pass
every field you want set.
Stateful: stays on until {@link disable} is called.
| Param | Type |
| ------------- | ------------------------------------------------------------------------------------- |
| options | SensitiveScreenEnableOptions |
disable()
disable() => Promise<void>Turn off screen-capture / snapshot protection.
On Android this clears FLAG_SECURE. On iOS this disarms the overlay
(any overlay currently on screen is torn down when the app next becomes
active).
Interfaces
SensitiveScreenEnableOptions
| Prop | Type | Description |
| --------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| style | SensitiveScreenOverlayStyle | iOS only. Visual style of the snapshot-protection overlay. - 'solid': fills the window with {@link backgroundColor}. - 'blur': uses a UIVisualEffectView with a UIBlurEffect of {@link blurStyle}. Default: 'solid'. |
| backgroundColor | string | iOS only. Hex color string ('#RRGGBB' or '#RRGGBBAA') used as the overlay background when {@link style} is 'solid'. Default: '#000000'. |
| blurStyle | SensitiveScreenBlurStyle | iOS only. UIBlurEffect style used when {@link style} is 'blur'. Default: 'regular'. |
| imageName | string | iOS only. Name of an image in the app's main bundle / asset catalog to render centered on top of the overlay (e.g. a logo). The consuming app is responsible for shipping the asset natively. |
Type Aliases
SensitiveScreenOverlayStyle
'solid' | 'blur'
SensitiveScreenBlurStyle
'light' | 'dark' | 'regular' | 'prominent' | 'extraLight'
iOS: why willResignActive, not didEnterBackground
iOS takes the screenshot it uses in the App Switcher between
willResignActive and didEnterBackground — by the time didEnterBackground
fires, the snapshot has already been captured with your real UI visible. If we
only inserted the overlay on didEnterBackground, the App Switcher preview
would still leak the sensitive view.
So the plugin inserts the overlay in willResignActive (covers the snapshot
window) and removes it in didBecomeActive (so the user never sees it during
normal foreground use). A transient resign-active event (a pulled-down Control
Center, an incoming call banner) will flash the overlay briefly — that's
intentional; the alternative leaks the view.
enable() and disable() only flip an internal flag (and, on iOS, record
the overlay options). They deliberately do not manipulate the view
hierarchy on their own — only the notification handlers do — so there is one
code path that owns the overlay lifecycle.
Development
npm install
npm run buildReleasing
Releases are cut from main and published to npm by a GitHub Actions workflow
(.github/workflows/publish.yml) that triggers on any pushed tag matching
v*. The tag and the version bump are produced by the same npm version
command, so it's not possible to tag a release without also bumping
package.json.
Prerequisites (one-time):
- Repo secret
NPM_TOKENset to an npm automation token with publish access to the@joinfluxscope. - Local
mainclean and up to date withorigin/main.
To release:
# pick the bump that fits the change
npm version patch # 0.1.0 → 0.1.1 — bug fixes, docs
npm version minor # 0.1.0 → 0.2.0 — new API, additive
npm version major # 0.1.0 → 1.0.0 — breaking change
git push --follow-tagsnpm version bumps package.json and package-lock.json, commits the change
with the version as the message, and creates a matching vX.Y.Z tag.
git push --follow-tags pushes the commit and the tag in one step.
What the workflow does on seeing the tag:
- Verifies the tag (e.g.
v0.2.0) matchespackage.json.version. If not, it fails fast — this catches tags created by hand without a version bump. - Runs
npm ciandnpm run build. - Runs
npm publish --access publicusingTrusted Publisher. - Creates a GitHub Release for the tag with auto-generated release notes from the commits since the previous tag.
If something goes wrong:
- Tag pushed but workflow failed before publish — fix the problem, delete
the tag (
git tag -d vX.Y.Z && git push --delete origin vX.Y.Z), and re-runnpm versionwith the same version using--allow-same-versionafter resetting, or cut the next patch version. Don't try to reuse a version once it's been published to npm — npm rejects republishing the same version even afternpm unpublish. - Published to npm but Release step failed — run
gh release create vX.Y.Z --generate-noteslocally to create the release after the fact. - Wrong version published — bump again (
npm version patch) and release the fix forward; never rewrite history onmain.
