diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b713dea
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+lib/
+node_modules/
+android/build/
+*.tgz
+PROMOTION.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..494d3bf
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+## 0.2.0 - 2026-07-20
+
+- Add Android `FLAG_SECURE` capture protection.
+- Add best-effort Android content sensitivity controls.
+- Detect Galaxy S26 Ultra and Knox Service Plugin Privacy Display readiness.
+- Add typed KSP Privacy Display deployment-plan helpers for device-wide and work-profile policies.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5558627
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Lech Kalinowski and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 0db3bd9..b1a0876 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,15 @@
# react-native-private-screen
-React Native privacy toolkit for Android with a fast-track API focused on:
+Screen privacy for React Native on Android: block capture, mark sensitive content, and prepare Samsung Knox Privacy Display policies for managed Galaxy S26 Ultra devices.
-- Screen capture blocking (`FLAG_SECURE`)
-- Best-effort content sensitivity (`View.setContentSensitivity(...)` where available)
-- Samsung Privacy Display support detection and Settings guidance (no third-party Samsung toggle API assumed)
+## What it supports
-## Why This Project Exists
+- `FLAG_SECURE` screenshot and non-secure display protection
+- Best-effort Android content sensitivity
+- Galaxy S26 Ultra and Knox Service Plugin readiness detection
+- Typed Samsung Privacy Display deployment plans for UEM administrators
-Samsung Privacy Display on Galaxy S26 Ultra is a hardware privacy feature, but there is no documented public third-party API (yet) for apps to toggle it programmatically.
-
-This project exists to provide immediate value now:
-
-- expose Android privacy protections apps can use today (`FLAG_SECURE`, best-effort content sensitivity)
-- give apps a clean Samsung-specific integration path (support detection + Settings guidance)
-- preserve a stable API surface that can later grow into official Samsung Privacy Display controls if Samsung publishes supported APIs
-
-## Scope
-
-This library intentionally separates different privacy controls:
-
-- `FLAG_SECURE`: blocks screenshots and non-secure displays/casting at the window level
-- `setContentSensitivity`: best-effort selective screen-sharing privacy on newer Android versions
-- Samsung Privacy Display: hardware shoulder-surfing/privacy-angle feature on supported Samsung devices, exposed here only as support detection + Settings navigation until Samsung documents a public app API
+Samsung Privacy Display is not a normal app API. Samsung exposes it through Knox Service Plugin 26.05 as an enterprise OEMConfig policy. An app cannot toggle the hardware feature directly; a UEM administrator must deploy the policy.
## Install
@@ -30,18 +17,17 @@ This library intentionally separates different privacy controls:
npm install react-native-private-screen
```
-Rebuild your Android app after installation.
+Rebuild the Android app after installation. iOS is intentionally unsupported; protection calls are no-ops and support detection returns `unavailable`.
-## Usage
+## Protect sensitive screens
```tsx
-import React, { useEffect } from 'react';
-import { Button, Text, View } from 'react-native';
+import { useEffect } from 'react';
import { PrivateScreen } from 'react-native-private-screen';
export function SensitiveScreen() {
useEffect(() => {
- void PrivateScreen.setScreenCaptureProtection('flagSecureOnDemand');
+ void PrivateScreen.setScreenCaptureProtection('flagSecure');
void PrivateScreen.setContentSensitivity('sensitive');
return () => {
@@ -50,52 +36,87 @@ export function SensitiveScreen() {
};
}, []);
- return (
-
- Sensitive content
-
- );
+ return null;
}
```
+`FLAG_SECURE` blocks screenshots, screen recording, and presentation on non-secure displays at the window level. Android content sensitivity is best-effort and silently does nothing on unsupported OS builds.
+
+## Prepare Samsung Privacy Display
+
+First, inspect whether the current device is eligible and obtain the host application's package name:
+
+```ts
+import { PrivateScreen } from 'react-native-private-screen';
+
+const support = await PrivateScreen.getSamsungPrivacyDisplaySupport();
+
+console.log(support.availability);
+console.log(support.appPackageName);
+console.log(support.device?.kspVersionName);
+```
+
+Then generate the values an administrator should map into the Knox Service Plugin managed configuration:
+
+```ts
+import {
+ createSamsungPrivacyDisplayDeploymentPlan,
+} from 'react-native-private-screen';
+
+const plan = createSamsungPrivacyDisplayDeploymentPlan({
+ scope: 'deviceWide',
+ target: 'selectedApps',
+ packageNames: ['com.example.securebanking'],
+ enableCredentialEntry: true,
+ allowUserToChangePrivacyDisplay: false,
+ allowUserToChangeApps: false,
+ allowUserToChangeCredentialEntry: false,
+});
+
+console.log(plan.policyPath);
+console.log(plan.values);
+```
+
+The generated object is a typed deployment guide, not a raw Android Management API payload. KSP's managed-configuration keys are supplied dynamically to compatible UEM consoles, so the administrator should map the returned values in the KSP configuration UI.
+
+### Samsung requirements
+
+- Galaxy S26 Ultra (`SM-S948*`)
+- Android 16 and One UI 8.5
+- Knox 3.13 or newer
+- First One UI 8.5 maintenance release or newer
+- Knox Service Plugin 26.05 (`1.5.64`) or newer, deployed through a compatible UEM
+
+`getSamsungPrivacyDisplaySupport()` can verify the manufacturer, model, Android API level, and installed KSP version. It cannot prove that the required One UI maintenance release, Knox version, license, enrollment, or policy assignment is active.
+
## API
### `PrivateScreen.setScreenCaptureProtection(mode)`
-Modes:
-
-- `'off'`
-- `'flagSecure'`
-- `'flagSecureOnDemand'`
+Modes: `'off'`, `'flagSecure'`, and `'flagSecureOnDemand'`. The two secure modes currently apply the same Android window flag; `flagSecureOnDemand` is retained for API compatibility.
### `PrivateScreen.setContentSensitivity(mode)`
-Modes:
+Modes: `'notSensitive'`, `'sensitive'`, and `'auto'`.
-- `'notSensitive'`
-- `'sensitive'`
-- `'auto'`
+### `PrivateScreen.getSamsungPrivacyDisplaySupport()`
-On Android versions/builds without `setContentSensitivity`, this call is a no-op.
+Returns device eligibility, KSP installation/version information, the host package name, documented requirements, and `canToggleFromApp: false`.
-### `PrivateScreen.getSamsungPrivacyDisplaySupport()`
+### `PrivateScreen.openSamsungPrivacyDisplaySettings()`
-Returns:
+Opens the closest documented Android settings screen available. Samsung does not publish a dedicated Privacy Display intent.
-- `{ availability: { status: 'unavailable', reason: '...' } }` on non-Samsung/non-Android
-- `{ availability: { status: 'unknown', reason: 'No public app API...' } }` on Samsung devices
+### `createSamsungPrivacyDisplayDeploymentPlan(options)`
-### `PrivateScreen.openSamsungPrivacyDisplaySettings()`
+Validates package names and returns a device-wide or work-profile KSP configuration guide for all screens or selected applications.
+
+## Official Samsung documentation
-Best-effort navigation to Android settings screens (`Display`, app details, or general Settings) so users can enable Samsung Privacy Display manually when supported by their device/firmware.
+- [Knox Service Plugin 26.05 release notes](https://docs.samsungknox.com/admin/knox-platform-for-enterprise/knox-service-plugin/release-notes/26-05/)
+- [Advanced Restriction policies](https://docs.samsungknox.com/admin/knox-platform-for-enterprise/knox-service-plugin/configure-advanced-policies/advanced-restriction-policies/)
+- [Knox Service Plugin managed configurations](https://docs.samsungknox.com/dev/managed-configurations/knox-service-plugin/)
-## Notes
+## License
-- This library does **not** claim to programmatically toggle Samsung Privacy Display.
-- Samsung-specific programmatic controls should be added only after an official public API/intent contract is documented and verified on hardware.
+MIT
diff --git a/android/build.gradle b/android/build.gradle
index b3fc434..a73f2f4 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -48,5 +48,5 @@ repositories {
}
dependencies {
- implementation "com.facebook.react:react-native:+"
+ implementation "com.facebook.react:react-android:+"
}
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index d9e4887..dd61caf 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1 +1,5 @@
-
+
+
+
+
+
diff --git a/android/src/main/java/com/rnprivatescreen/RNPrivateScreenModule.kt b/android/src/main/java/com/rnprivatescreen/RNPrivateScreenModule.kt
index be7004b..84a71f5 100644
--- a/android/src/main/java/com/rnprivatescreen/RNPrivateScreenModule.kt
+++ b/android/src/main/java/com/rnprivatescreen/RNPrivateScreenModule.kt
@@ -1,6 +1,8 @@
package com.rnprivatescreen
import android.content.Intent
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
@@ -15,6 +17,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.UiThreadUtil
import java.lang.reflect.Method
+import java.util.Locale
class RNPrivateScreenModule(
reactContext: ReactApplicationContext
@@ -84,18 +87,76 @@ class RNPrivateScreenModule(
fun getSamsungPrivacyDisplaySupport(promise: Promise) {
try {
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
+ val isGalaxyS26Ultra = Build.MODEL.uppercase(Locale.US).startsWith("SM-S948")
+ val hasAndroid16 = Build.VERSION.SDK_INT >= MINIMUM_ANDROID_API_LEVEL
+ val kspPackageInfo = getPackageInfo(KNOX_SERVICE_PLUGIN_PACKAGE_NAME)
+ val kspVersionName = kspPackageInfo?.versionName
+ val hasSupportedKspVersion = isVersionAtLeast(
+ kspVersionName,
+ MINIMUM_KSP_VERSION
+ )
val result = Arguments.createMap()
val availability = Arguments.createMap()
- if (!isSamsung) {
- availability.putString("status", "unavailable")
- availability.putString("reason", "non-Samsung device")
- } else {
- availability.putString("status", "unknown")
- availability.putString("reason", "No public app API is available; use user Settings flow")
+ when {
+ !isSamsung -> {
+ availability.putString("status", "unavailable")
+ availability.putString("reason", "Privacy Display requires a Samsung device")
+ }
+ !isGalaxyS26Ultra -> {
+ availability.putString("status", "unavailable")
+ availability.putString("reason", "Privacy Display policy is supported only on Galaxy S26 Ultra")
+ }
+ !hasAndroid16 -> {
+ availability.putString("status", "unavailable")
+ availability.putString("reason", "Privacy Display policy requires Android 16 / One UI 8.5")
+ }
+ kspPackageInfo == null -> {
+ availability.putString("status", "unknown")
+ availability.putString(
+ "reason",
+ "Eligible hardware detected; Knox Service Plugin 26.05+ must be deployed by a UEM"
+ )
+ }
+ hasSupportedKspVersion == false -> {
+ availability.putString("status", "unavailable")
+ availability.putString("reason", "Knox Service Plugin 1.5.64 or newer is required")
+ }
+ hasSupportedKspVersion == null -> {
+ availability.putString("status", "unknown")
+ availability.putString("reason", "Unable to verify the Knox Service Plugin version")
+ }
+ else -> {
+ availability.putString("status", "available")
+ availability.putString(
+ "reason",
+ "Eligible for enterprise policy; a UEM admin must configure Privacy Display"
+ )
+ }
}
result.putMap("availability", availability)
+ result.putString("delivery", "knoxServicePlugin")
+ result.putBoolean("canToggleFromApp", false)
+ result.putString("appPackageName", reactApplicationContext.packageName)
+
+ val device = Arguments.createMap()
+ device.putString("manufacturer", Build.MANUFACTURER)
+ device.putString("model", Build.MODEL)
+ device.putInt("androidApiLevel", Build.VERSION.SDK_INT)
+ device.putBoolean("kspInstalled", kspPackageInfo != null)
+ kspVersionName?.let { device.putString("kspVersionName", it) }
+ result.putMap("device", device)
+
+ val requirements = Arguments.createMap()
+ requirements.putString("supportedModel", "Galaxy S26 Ultra (SM-S948*)")
+ requirements.putInt("minimumAndroidApiLevel", MINIMUM_ANDROID_API_LEVEL)
+ requirements.putString("minimumOneUiVersion", "8.5")
+ requirements.putString("minimumKnoxVersion", "3.13")
+ requirements.putString("minimumKspVersion", MINIMUM_KSP_VERSION)
+ requirements.putBoolean("maintenanceReleaseRequired", true)
+ result.putMap("requirements", requirements)
+
promise.resolve(result)
} catch (t: Throwable) {
promise.reject("E_INTERNAL", t.message, t)
@@ -200,4 +261,55 @@ class RNPrivateScreenModule(
null
}
}
+
+ @Suppress("DEPRECATION")
+ private fun getPackageInfo(packageName: String): PackageInfo? {
+ return try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ reactApplicationContext.packageManager.getPackageInfo(
+ packageName,
+ PackageManager.PackageInfoFlags.of(0)
+ )
+ } else {
+ reactApplicationContext.packageManager.getPackageInfo(packageName, 0)
+ }
+ } catch (_: PackageManager.NameNotFoundException) {
+ null
+ }
+ }
+
+ private fun isVersionAtLeast(versionName: String?, minimumVersion: String): Boolean? {
+ if (versionName == null) {
+ return null
+ }
+
+ val current = VERSION_NUMBER_PATTERN.findAll(versionName)
+ .map { it.value.toInt() }
+ .toList()
+ val minimum = VERSION_NUMBER_PATTERN.findAll(minimumVersion)
+ .map { it.value.toInt() }
+ .toList()
+
+ if (current.isEmpty() || minimum.isEmpty()) {
+ return null
+ }
+
+ val partCount = maxOf(current.size, minimum.size)
+ for (index in 0 until partCount) {
+ val currentPart = current.getOrElse(index) { 0 }
+ val minimumPart = minimum.getOrElse(index) { 0 }
+ if (currentPart != minimumPart) {
+ return currentPart > minimumPart
+ }
+ }
+
+ return true
+ }
+
+ companion object {
+ private const val KNOX_SERVICE_PLUGIN_PACKAGE_NAME = "com.samsung.android.knox.kpu"
+ private const val MINIMUM_ANDROID_API_LEVEL = 36
+ private const val MINIMUM_KSP_VERSION = "1.5.64"
+ private val VERSION_NUMBER_PATTERN = Regex("\\d+")
+ }
}
diff --git a/package.json b/package.json
index 3240401..291d7af 100644
--- a/package.json
+++ b/package.json
@@ -1,35 +1,67 @@
{
"name": "react-native-private-screen",
- "version": "0.1.0",
- "description": "React Native privacy toolkit with Android capture protection and Samsung Privacy Display guidance hooks.",
+ "version": "0.2.0",
+ "description": "React Native screen privacy for Android, including capture protection and Samsung Knox Privacy Display readiness.",
"license": "MIT",
- "main": "src/index.ts",
+ "main": "lib/index.js",
"react-native": "src/index.ts",
- "types": "src/index.ts",
+ "types": "lib/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/index.d.ts",
+ "react-native": "./src/index.ts",
+ "require": "./lib/index.js",
+ "default": "./lib/index.js"
+ }
+ },
"files": [
+ "lib",
"src",
- "android",
+ "android/src",
+ "android/build.gradle",
+ "android/consumer-rules.pro",
+ "CHANGELOG.md",
+ "LICENSE",
"README.md",
"react-native.config.js"
],
+ "sideEffects": false,
"scripts": {
+ "clean": "node -e \"require('node:fs').rmSync('lib', { recursive: true, force: true })\"",
"build": "tsc -p tsconfig.json",
- "typecheck": "tsc -p tsconfig.json --noEmit"
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "test": "npm run build && node --test test/*.test.cjs",
+ "prepack": "npm run clean && npm run build"
},
"keywords": [
"react-native",
"android",
"privacy",
"flag-secure",
- "samsung"
+ "samsung",
+ "samsung-knox",
+ "privacy-display",
+ "screen-capture"
],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/callstackincubator/react-native-private-screen.git"
+ },
+ "homepage": "https://github.com/callstackincubator/react-native-private-screen#readme",
+ "bugs": {
+ "url": "https://github.com/callstackincubator/react-native-private-screen/issues"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
"peerDependencies": {
"react": ">=18.0.0",
"react-native": ">=0.72.0"
},
"devDependencies": {
- "@types/react": "^18.3.24",
- "@types/react-native": "^0.73.0",
"typescript": "^5.8.2"
}
}
diff --git a/src/index.ts b/src/index.ts
index 96bbcc8..5bb19da 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,15 +7,30 @@ import type {
SamsungPrivacyDisplaySupport,
ScreenCaptureProtectionMode,
} from './types';
+import { SAMSUNG_PRIVACY_DISPLAY_REQUIREMENTS } from './samsungPrivacyDisplay';
export type {
Availability,
ContentSensitivityMode,
PrivateScreenModule,
+ SamsungPrivacyDisplayDeploymentOptions,
+ SamsungPrivacyDisplayDeploymentPlan,
+ SamsungPrivacyDisplayDevice,
+ SamsungPrivacyDisplayPolicyValues,
+ SamsungPrivacyDisplayRequirements,
+ SamsungPrivacyDisplayScope,
SamsungPrivacyDisplaySupport,
+ SamsungPrivacyDisplayTarget,
ScreenCaptureProtectionMode,
} from './types';
+export {
+ createSamsungPrivacyDisplayDeploymentPlan,
+ KNOX_SERVICE_PLUGIN_PACKAGE_NAME,
+ SAMSUNG_PRIVACY_DISPLAY_DOCUMENTATION_URL,
+ SAMSUNG_PRIVACY_DISPLAY_REQUIREMENTS,
+} from './samsungPrivacyDisplay';
+
const MODULE_NAME = 'RNPrivateScreen';
function getNativeModule(): NativePrivateScreenModule {
@@ -36,6 +51,9 @@ async function noop(): Promise {
async function getUnavailableSupport(reason: string): Promise {
return {
availability: { status: 'unavailable', reason },
+ delivery: 'knoxServicePlugin',
+ canToggleFromApp: false,
+ requirements: { ...SAMSUNG_PRIVACY_DISPLAY_REQUIREMENTS },
};
}
diff --git a/src/react-native.d.ts b/src/react-native.d.ts
new file mode 100644
index 0000000..3fb0f10
--- /dev/null
+++ b/src/react-native.d.ts
@@ -0,0 +1,6 @@
+declare module 'react-native' {
+ export const NativeModules: Record;
+ export const Platform: {
+ OS: string;
+ };
+}
diff --git a/src/samsungPrivacyDisplay.ts b/src/samsungPrivacyDisplay.ts
new file mode 100644
index 0000000..abb22f3
--- /dev/null
+++ b/src/samsungPrivacyDisplay.ts
@@ -0,0 +1,79 @@
+import type {
+ SamsungPrivacyDisplayDeploymentOptions,
+ SamsungPrivacyDisplayDeploymentPlan,
+ SamsungPrivacyDisplayRequirements,
+} from './types';
+
+export const KNOX_SERVICE_PLUGIN_PACKAGE_NAME =
+ 'com.samsung.android.knox.kpu';
+
+export const SAMSUNG_PRIVACY_DISPLAY_DOCUMENTATION_URL =
+ 'https://docs.samsungknox.com/admin/knox-platform-for-enterprise/knox-service-plugin/configure-advanced-policies/advanced-restriction-policies/';
+
+export const SAMSUNG_PRIVACY_DISPLAY_REQUIREMENTS: SamsungPrivacyDisplayRequirements = {
+ supportedModel: 'Galaxy S26 Ultra (SM-S948*)',
+ minimumAndroidApiLevel: 36,
+ minimumOneUiVersion: '8.5',
+ minimumKnoxVersion: '3.13',
+ minimumKspVersion: '1.5.64',
+ maintenanceReleaseRequired: true,
+};
+
+const PACKAGE_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/;
+
+function normalizePackageNames(packageNames: readonly string[]): string[] {
+ const normalized = Array.from(
+ new Set(packageNames.map((packageName) => packageName.trim())),
+ ).filter(Boolean);
+
+ const invalidPackageName = normalized.find(
+ (packageName) => !PACKAGE_NAME_PATTERN.test(packageName),
+ );
+ if (invalidPackageName) {
+ throw new TypeError(`Invalid Android package name: ${invalidPackageName}`);
+ }
+
+ return normalized;
+}
+
+export function createSamsungPrivacyDisplayDeploymentPlan(
+ options: SamsungPrivacyDisplayDeploymentOptions,
+): SamsungPrivacyDisplayDeploymentPlan {
+ const scope = options.scope ?? 'deviceWide';
+ const packageNames = normalizePackageNames(options.packageNames ?? []);
+
+ if (options.target === 'selectedApps' && packageNames.length === 0) {
+ throw new TypeError(
+ 'At least one Android package name is required when target is selectedApps.',
+ );
+ }
+ if (options.target === 'allScreens' && packageNames.length > 0) {
+ throw new TypeError(
+ 'packageNames must be empty when target is allScreens.',
+ );
+ }
+
+ return {
+ format: 'ksp-managed-configuration-guide',
+ isDirectlyDeployable: false,
+ kspPackageName: KNOX_SERVICE_PLUGIN_PACKAGE_NAME,
+ scope,
+ policyPath:
+ scope === 'deviceWide'
+ ? 'Device-wide policies > Advanced Restriction policies > Privacy Display operation'
+ : 'Work profile policies > Advanced Restriction policies > Privacy Display operation',
+ values: {
+ turnOnPrivacyDisplay: true,
+ allowUserToChangePrivacyDisplay:
+ options.allowUserToChangePrivacyDisplay ?? false,
+ addPackageNames:
+ options.target === 'selectedApps' ? packageNames : [],
+ allowUserToChangeApps: options.allowUserToChangeApps ?? false,
+ turnOnPinPatternPassword: options.enableCredentialEntry ?? true,
+ allowUserToChangePinPatternPassword:
+ options.allowUserToChangeCredentialEntry ?? false,
+ },
+ requirements: { ...SAMSUNG_PRIVACY_DISPLAY_REQUIREMENTS },
+ documentationUrl: SAMSUNG_PRIVACY_DISPLAY_DOCUMENTATION_URL,
+ };
+}
diff --git a/src/types.ts b/src/types.ts
index 9ca14d3..7871838 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,5 +1,5 @@
export type Availability =
- | { status: 'available' }
+ | { status: 'available'; reason?: string }
| { status: 'unavailable'; reason: string }
| { status: 'unknown'; reason: string };
@@ -13,8 +13,64 @@ export type ContentSensitivityMode =
| 'sensitive'
| 'auto';
+export type SamsungPrivacyDisplayScope = 'deviceWide' | 'workProfile';
+
+export type SamsungPrivacyDisplayTarget = 'allScreens' | 'selectedApps';
+
+export interface SamsungPrivacyDisplayRequirements {
+ supportedModel: string;
+ minimumAndroidApiLevel: number;
+ minimumOneUiVersion: string;
+ minimumKnoxVersion: string;
+ minimumKspVersion: string;
+ maintenanceReleaseRequired: boolean;
+}
+
+export interface SamsungPrivacyDisplayDevice {
+ manufacturer: string;
+ model: string;
+ androidApiLevel: number;
+ kspInstalled: boolean;
+ kspVersionName?: string;
+}
+
export interface SamsungPrivacyDisplaySupport {
availability: Availability;
+ delivery: 'knoxServicePlugin';
+ canToggleFromApp: false;
+ requirements: SamsungPrivacyDisplayRequirements;
+ appPackageName?: string;
+ device?: SamsungPrivacyDisplayDevice;
+}
+
+export interface SamsungPrivacyDisplayDeploymentOptions {
+ scope?: SamsungPrivacyDisplayScope;
+ target: SamsungPrivacyDisplayTarget;
+ packageNames?: readonly string[];
+ enableCredentialEntry?: boolean;
+ allowUserToChangePrivacyDisplay?: boolean;
+ allowUserToChangeApps?: boolean;
+ allowUserToChangeCredentialEntry?: boolean;
+}
+
+export interface SamsungPrivacyDisplayPolicyValues {
+ turnOnPrivacyDisplay: true;
+ allowUserToChangePrivacyDisplay: boolean;
+ addPackageNames: readonly string[];
+ allowUserToChangeApps: boolean;
+ turnOnPinPatternPassword: boolean;
+ allowUserToChangePinPatternPassword: boolean;
+}
+
+export interface SamsungPrivacyDisplayDeploymentPlan {
+ format: 'ksp-managed-configuration-guide';
+ isDirectlyDeployable: false;
+ kspPackageName: string;
+ scope: SamsungPrivacyDisplayScope;
+ policyPath: string;
+ values: SamsungPrivacyDisplayPolicyValues;
+ requirements: SamsungPrivacyDisplayRequirements;
+ documentationUrl: string;
}
export interface PrivateScreenModule {
diff --git a/test/samsungPrivacyDisplay.test.cjs b/test/samsungPrivacyDisplay.test.cjs
new file mode 100644
index 0000000..c1b1a7d
--- /dev/null
+++ b/test/samsungPrivacyDisplay.test.cjs
@@ -0,0 +1,61 @@
+const assert = require('node:assert/strict');
+const test = require('node:test');
+
+const {
+ createSamsungPrivacyDisplayDeploymentPlan,
+ KNOX_SERVICE_PLUGIN_PACKAGE_NAME,
+} = require('../lib/samsungPrivacyDisplay.js');
+
+test('creates a locked selected-app deployment plan', () => {
+ const plan = createSamsungPrivacyDisplayDeploymentPlan({
+ target: 'selectedApps',
+ packageNames: ['com.example.secure', 'com.example.secure'],
+ });
+
+ assert.equal(plan.kspPackageName, KNOX_SERVICE_PLUGIN_PACKAGE_NAME);
+ assert.equal(plan.scope, 'deviceWide');
+ assert.deepEqual(plan.values.addPackageNames, ['com.example.secure']);
+ assert.equal(plan.values.turnOnPrivacyDisplay, true);
+ assert.equal(plan.values.allowUserToChangePrivacyDisplay, false);
+ assert.equal(plan.values.turnOnPinPatternPassword, true);
+ assert.equal(plan.isDirectlyDeployable, false);
+});
+
+test('creates a work-profile all-screen plan', () => {
+ const plan = createSamsungPrivacyDisplayDeploymentPlan({
+ scope: 'workProfile',
+ target: 'allScreens',
+ enableCredentialEntry: false,
+ allowUserToChangePrivacyDisplay: true,
+ allowUserToChangeApps: true,
+ allowUserToChangeCredentialEntry: true,
+ });
+
+ assert.match(plan.policyPath, /^Work profile policies/);
+ assert.deepEqual(plan.values.addPackageNames, []);
+ assert.equal(plan.values.allowUserToChangePrivacyDisplay, true);
+ assert.equal(plan.values.allowUserToChangeApps, true);
+ assert.equal(plan.values.turnOnPinPatternPassword, false);
+ assert.equal(plan.values.allowUserToChangePinPatternPassword, true);
+});
+
+test('rejects selected-app plans without package names', () => {
+ assert.throws(
+ () =>
+ createSamsungPrivacyDisplayDeploymentPlan({
+ target: 'selectedApps',
+ }),
+ /At least one Android package name/,
+ );
+});
+
+test('rejects invalid Android package names', () => {
+ assert.throws(
+ () =>
+ createSamsungPrivacyDisplayDeploymentPlan({
+ target: 'selectedApps',
+ packageNames: ['not-a-package'],
+ }),
+ /Invalid Android package name/,
+ );
+});
diff --git a/tsconfig.json b/tsconfig.json
index fe3fd74..ba38034 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,17 +1,20 @@
{
"compilerOptions": {
"target": "ES2019",
- "module": "ESNext",
+ "module": "CommonJS",
"moduleResolution": "Node",
"lib": ["ES2019"],
"strict": true,
"declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "rootDir": "src",
+ "outDir": "lib",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
- "resolveJsonModule": true,
- "types": ["react", "react-native"]
+ "resolveJsonModule": true
},
"include": ["src/**/*"]
}