expo-privacy-image 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ['universe/native', 'universe/web'],
4
+ ignorePatterns: ['build'],
5
+ };
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # expo-privacy-image
2
+
3
+ A React Native module that overlays a privacy image on the screen whenever the app goes to the background — preventing sensitive content from appearing in the app switcher (iOS) or recent apps (Android).
4
+
5
+ Built with [Expo Modules](https://docs.expo.dev/modules/overview/).
6
+
7
+ ---
8
+
9
+ ## Platform support
10
+
11
+ | Android | iOS |
12
+ | ------- | --- |
13
+ | ✅ | ✅ |
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ npm install expo-privacy-image
21
+ ```
22
+
23
+ or
24
+
25
+ ```sh
26
+ yarn add expo-privacy-image
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Setup
32
+
33
+ ### Expo managed / bare workflow (with `expo prebuild`)
34
+
35
+ Add the plugin to your `app.config.ts` (or `app.json`) and point it at your privacy image:
36
+
37
+ ```ts
38
+ // app.config.ts
39
+ export default {
40
+ plugins: [
41
+ [
42
+ "expo-privacy-image",
43
+ {
44
+ imagePath: "./assets/images/privacy.png",
45
+ },
46
+ ],
47
+ ],
48
+ };
49
+ ```
50
+
51
+ Then run prebuild to apply the native changes:
52
+
53
+ ```sh
54
+ npx expo prebuild
55
+ ```
56
+
57
+ The plugin automatically:
58
+ - **Android** — copies the image to `android/app/src/main/res/drawable/privacy_image.png`
59
+ - **iOS** — adds the image as `PrivacyImage.imageset` inside `Images.xcassets`
60
+
61
+ ### Bare React Native (manual setup)
62
+
63
+ Ensure [`expo-modules-core`](https://docs.expo.dev/bare/installing-expo-modules/) is installed before proceeding — it is required for autolinking to work.
64
+
65
+ **Android**
66
+
67
+ Copy your privacy image to:
68
+
69
+ ```
70
+ android/app/src/main/res/drawable/privacy_image.png
71
+ ```
72
+
73
+ **iOS**
74
+
75
+ Create the imageset directory and add your image:
76
+
77
+ ```
78
+ ios/<ProjectName>/Images.xcassets/PrivacyImage.imageset/
79
+ privacy_image.png
80
+ Contents.json
81
+ ```
82
+
83
+ `Contents.json` should contain:
84
+
85
+ ```json
86
+ {
87
+ "images": [
88
+ { "idiom": "universal", "filename": "privacy_image.png", "scale": "1x" }
89
+ ],
90
+ "info": { "version": 1, "author": "xcode" }
91
+ }
92
+ ```
93
+
94
+ Then run:
95
+
96
+ ```sh
97
+ npx pod-install
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Usage
103
+
104
+ Call `usePrivacyImage()` once — typically at the root of your app or inside a top-level component. Once called, the module is permanently enabled for the lifetime of the app session.
105
+
106
+ ```ts
107
+ import { usePrivacyImage } from "expo-privacy-image";
108
+
109
+ export default function App() {
110
+ usePrivacyImage();
111
+
112
+ return (
113
+ // ...
114
+ );
115
+ }
116
+ ```
117
+
118
+ When the app moves to the background, the image you configured is shown as a full-screen overlay. It is removed automatically when the app returns to the foreground.
119
+
120
+ ---
121
+
122
+ ## API
123
+
124
+ ### `usePrivacyImage(): void`
125
+
126
+ Enables the privacy overlay for the current app session. Safe to call multiple times — subsequent calls are no-ops.
127
+
128
+ | Parameter | Type | Description |
129
+ | --------- | ---- | ----------- |
130
+ | — | — | No parameters |
131
+
132
+ ---
133
+
134
+ ## Requirements
135
+
136
+ - React Native ≥ 0.71
137
+ - expo
138
+ - iOS ≥ 15.1
139
+
@@ -0,0 +1,18 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'expo.modules.privacyimage'
7
+ version = '0.1.0'
8
+
9
+ android {
10
+ namespace "expo.modules.privacyimage"
11
+ defaultConfig {
12
+ versionCode 1
13
+ versionName "0.1.0"
14
+ }
15
+ lintOptions {
16
+ abortOnError false
17
+ }
18
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,83 @@
1
+ package expo.modules.privacyimage
2
+
3
+ import android.view.ViewGroup
4
+ import android.widget.ImageView
5
+ import expo.modules.kotlin.exception.Exceptions
6
+ import expo.modules.kotlin.modules.Module
7
+ import expo.modules.kotlin.modules.ModuleDefinition
8
+
9
+ class PrivacyImageModule : Module() {
10
+
11
+ private var isPrivacyEnabled: Boolean = false
12
+ // Each module class must implement the definition function. The definition consists of components
13
+ // that describes the module's functionality and behavior.
14
+ // See https://docs.expo.dev/modules/module-api for more details about available components.
15
+ private val safeCurrentActivity
16
+ get() = appContext.currentActivity
17
+ private val currentActivity
18
+ get() = safeCurrentActivity ?: throw Exceptions.MissingActivity()
19
+
20
+ private var overlayView: ImageView? = null
21
+
22
+ private fun showOverlay() {
23
+ currentActivity.runOnUiThread {
24
+ if (overlayView != null) return@runOnUiThread
25
+
26
+ val overlay =
27
+ ImageView(currentActivity).apply {
28
+ val drawableId =
29
+ currentActivity.resources.getIdentifier(
30
+ "privacy_image",
31
+ "drawable",
32
+ currentActivity.packageName
33
+ )
34
+ setImageResource(drawableId)
35
+ scaleType = ImageView.ScaleType.CENTER_CROP
36
+ layoutParams =
37
+ ViewGroup.LayoutParams(
38
+ ViewGroup.LayoutParams.MATCH_PARENT,
39
+ ViewGroup.LayoutParams.MATCH_PARENT
40
+ )
41
+ }
42
+
43
+ val rootView = currentActivity.window.decorView as ViewGroup
44
+ rootView.addView(overlay)
45
+ overlayView = overlay
46
+ }
47
+ }
48
+
49
+ private fun hideOverlay() {
50
+ currentActivity.runOnUiThread {
51
+ val rootView = currentActivity.window.decorView as ViewGroup
52
+ overlayView?.let {
53
+ rootView.removeView(it)
54
+ overlayView = null
55
+ }
56
+ }
57
+ }
58
+
59
+ override fun definition() = ModuleDefinition {
60
+ // Sets the name of the module that JavaScript code will use to refer to the module. Takes a
61
+ // string as an argument.
62
+ // Can be inferred from module's class name, but it's recommended to set it explicitly for
63
+ // clarity.
64
+ // The module will be accessible from `requireNativeModule('PrivacyImage')` in JavaScript.
65
+ Name("PrivacyImage")
66
+
67
+ Function("usePrivacyImage") { isPrivacyEnabled = true }
68
+
69
+ OnActivityEntersBackground {
70
+ // Show privacy overlay
71
+ if (!isPrivacyEnabled) return@OnActivityEntersBackground
72
+
73
+ showOverlay()
74
+ }
75
+
76
+ OnActivityEntersForeground {
77
+ // Remove privacy overlay
78
+ if (!isPrivacyEnabled) return@OnActivityEntersForeground
79
+
80
+ hideOverlay()
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,7 @@
1
+ import { NativeModule } from "expo";
2
+ declare class PrivacyImageModule extends NativeModule {
3
+ usePrivacyImage: () => void;
4
+ }
5
+ declare const _default: PrivacyImageModule;
6
+ export default _default;
7
+ //# sourceMappingURL=PrivacyImageModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PrivacyImageModule.d.ts","sourceRoot":"","sources":["../src/PrivacyImageModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,OAAO,kBAAmB,SAAQ,YAAY;IACnD,eAAe,EAAE,MAAM,IAAI,CAAC;CAC7B;;AAGD,wBAAuE"}
@@ -0,0 +1,4 @@
1
+ import { requireNativeModule } from "expo";
2
+ // This call loads the native module object from the JSI.
3
+ export default requireNativeModule("PrivacyImage");
4
+ //# sourceMappingURL=PrivacyImageModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PrivacyImageModule.js","sourceRoot":"","sources":["../src/PrivacyImageModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAMzD,yDAAyD;AACzD,eAAe,mBAAmB,CAAqB,cAAc,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\n\ndeclare class PrivacyImageModule extends NativeModule {\n usePrivacyImage: () => void;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<PrivacyImageModule>(\"PrivacyImage\");\n"]}
@@ -0,0 +1,2 @@
1
+ export declare function usePrivacyImage(): void;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,wBAAgB,eAAe,SAE9B"}
package/build/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import PrivacyImageModule from "./PrivacyImageModule";
2
+ export function usePrivacyImage() {
3
+ PrivacyImageModule.usePrivacyImage();
4
+ }
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AAEtD,MAAM,UAAU,eAAe;IAC7B,kBAAkB,CAAC,eAAe,EAAE,CAAC;AACvC,CAAC","sourcesContent":["import PrivacyImageModule from \"./PrivacyImageModule\";\n\nexport function usePrivacyImage() {\n PrivacyImageModule.usePrivacyImage();\n}\n"]}
@@ -0,0 +1,10 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["PrivacyImageModule"],
5
+ "appDelegateSubscribers": ["AppLifecycleDelegate"]
6
+ },
7
+ "android": {
8
+ "modules": ["expo.modules.privacyimage.PrivacyImageModule"]
9
+ }
10
+ }
@@ -0,0 +1,46 @@
1
+ import ExpoModulesCore
2
+
3
+ public class AppLifecycleDelegate: ExpoAppDelegateSubscriber {
4
+
5
+ private var overlayView: UIImageView?
6
+
7
+ public func applicationWillResignActive(_ application: UIApplication) {
8
+ guard PrivacyImageModule.isPrivacyEnabled else { return }
9
+ self.showOverlay()
10
+ }
11
+
12
+ public func applicationDidBecomeActive(_ application: UIApplication) {
13
+ guard PrivacyImageModule.isPrivacyEnabled else { return }
14
+ self.hideOverlay()
15
+ }
16
+
17
+ private func showOverlay() {
18
+ DispatchQueue.main.async {
19
+ guard self.overlayView == nil,
20
+ let window = self.getKeyWindow()
21
+ else { return }
22
+
23
+ let overlay = UIImageView(frame: window.bounds)
24
+ overlay.image = UIImage(named: "PrivacyImage")
25
+ overlay.contentMode = .scaleAspectFill
26
+ overlay.tag = 999
27
+
28
+ window.addSubview(overlay)
29
+ self.overlayView = overlay
30
+ }
31
+ }
32
+
33
+ private func hideOverlay() {
34
+ DispatchQueue.main.async {
35
+ self.overlayView?.removeFromSuperview()
36
+ self.overlayView = nil
37
+ }
38
+ }
39
+
40
+ private func getKeyWindow() -> UIWindow? {
41
+ UIApplication.shared.connectedScenes
42
+ .compactMap { $0 as? UIWindowScene }
43
+ .flatMap { $0.windows }
44
+ .first { $0.isKeyWindow }
45
+ }
46
+ }
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'PrivacyImage'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platforms = {
14
+ :ios => '15.1',
15
+ }
16
+ s.swift_version = '5.9'
17
+ s.source = { git: 'https://github.com/ntphu1808/expo-privacy-image' }
18
+ s.static_framework = true
19
+
20
+ s.dependency 'ExpoModulesCore'
21
+
22
+ # Swift/Objective-C compatibility
23
+ s.pod_target_xcconfig = {
24
+ 'DEFINES_MODULE' => 'YES',
25
+ }
26
+
27
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
28
+ end
@@ -0,0 +1,20 @@
1
+ import ExpoModulesCore
2
+
3
+ public class PrivacyImageModule: Module {
4
+ // Each module class must implement the definition function. The definition consists of components
5
+ // that describes the module's functionality and behavior.
6
+ // See https://docs.expo.dev/modules/module-api for more details about available components.
7
+
8
+ static var isPrivacyEnabled: Bool = false
9
+
10
+ public func definition() -> ModuleDefinition {
11
+ // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
12
+ // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
13
+ // The module will be accessible from `requireNativeModule('PrivacyImage')` in JavaScript.
14
+ Name("PrivacyImage")
15
+
16
+ Function("usePrivacyImage") {
17
+ PrivacyImageModule.isPrivacyEnabled = true
18
+ }
19
+ }
20
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "expo-privacy-image",
3
+ "version": "1.0.0",
4
+ "description": "Overlays a privacy image on the screen when the app goes to the background.",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "prepare": "expo-module prepare",
13
+ "prepublishOnly": "expo-module prepublishOnly",
14
+ "expo-module": "expo-module"
15
+ },
16
+ "keywords": [
17
+ "react-native",
18
+ "expo",
19
+ "expo-privacy-image",
20
+ "PrivacyImage"
21
+ ],
22
+ "repository": "https://github.com/ntphu1808/expo-privacy-image",
23
+ "bugs": {
24
+ "url": "https://github.com/ntphu1808/expo-privacy-image/issues"
25
+ },
26
+ "author": "Nguyen Tran Phu <ntphu081995@gmail.com> (https://github.com/ntphu1808)",
27
+ "license": "MIT",
28
+ "homepage": "https://github.com/ntphu1808/expo-privacy-image#readme",
29
+ "dependencies": {},
30
+ "devDependencies": {
31
+ "@types/react": "~19.1.1",
32
+ "expo-module-scripts": "^55.0.2",
33
+ "expo": "^55.0.14",
34
+ "react-native": "0.82.1"
35
+ },
36
+ "peerDependencies": {
37
+ "expo": "*",
38
+ "react": "*",
39
+ "react-native": "*"
40
+ }
41
+ }
@@ -0,0 +1,6 @@
1
+ import { ConfigPlugin } from "expo/config-plugins";
2
+ type PrivacyImageProps = {
3
+ imagePath: string;
4
+ };
5
+ declare const withPrivacyImage: ConfigPlugin<PrivacyImageProps>;
6
+ export default withPrivacyImage;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ // plugins/withPrivacyImage.ts
37
+ const config_plugins_1 = require("expo/config-plugins");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const withPrivacyImage = (config, { imagePath }) => {
41
+ // Android - copy to res/drawable
42
+ config = (0, config_plugins_1.withDangerousMod)(config, [
43
+ "android",
44
+ async (config) => {
45
+ const src = path.resolve(config.modRequest.projectRoot, imagePath);
46
+ const dest = path.join(config.modRequest.platformProjectRoot, "app/src/main/res/drawable/privacy_image.png");
47
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
48
+ fs.copyFileSync(src, dest);
49
+ return config;
50
+ },
51
+ ]);
52
+ // iOS - copy into Images.xcassets as an imageset so UIImage(named:) works
53
+ config = (0, config_plugins_1.withDangerousMod)(config, [
54
+ "ios",
55
+ async (config) => {
56
+ const src = path.resolve(config.modRequest.projectRoot, imagePath);
57
+ const projectName = config.modRequest.projectName;
58
+ const imagesetDir = path.join(config.modRequest.platformProjectRoot, projectName, "Images.xcassets", "PrivacyImage.imageset");
59
+ fs.mkdirSync(imagesetDir, { recursive: true });
60
+ fs.copyFileSync(src, path.join(imagesetDir, "privacy_image.png"));
61
+ const contents = {
62
+ images: [{ idiom: "universal", filename: "privacy_image.png", scale: "1x" }],
63
+ info: { version: 1, author: "xcode" },
64
+ };
65
+ fs.writeFileSync(path.join(imagesetDir, "Contents.json"), JSON.stringify(contents, null, 2));
66
+ return config;
67
+ },
68
+ ]);
69
+ return config;
70
+ };
71
+ exports.default = withPrivacyImage;
@@ -0,0 +1,49 @@
1
+ // plugins/withPrivacyImage.ts
2
+ import { ConfigPlugin, withDangerousMod } from "expo/config-plugins";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+
6
+ type PrivacyImageProps = {
7
+ imagePath: string;
8
+ };
9
+
10
+ const withPrivacyImage: ConfigPlugin<PrivacyImageProps> = (config, { imagePath }) => {
11
+ // Android - copy to res/drawable
12
+ config = withDangerousMod(config, [
13
+ "android",
14
+ async (config) => {
15
+ const src = path.resolve(config.modRequest.projectRoot, imagePath);
16
+ const dest = path.join(config.modRequest.platformProjectRoot, "app/src/main/res/drawable/privacy_image.png");
17
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
18
+ fs.copyFileSync(src, dest);
19
+ return config;
20
+ },
21
+ ]);
22
+
23
+ // iOS - copy into Images.xcassets as an imageset so UIImage(named:) works
24
+ config = withDangerousMod(config, [
25
+ "ios",
26
+ async (config) => {
27
+ const src = path.resolve(config.modRequest.projectRoot, imagePath);
28
+ const projectName = config.modRequest.projectName!;
29
+ const imagesetDir = path.join(
30
+ config.modRequest.platformProjectRoot,
31
+ projectName,
32
+ "Images.xcassets",
33
+ "PrivacyImage.imageset",
34
+ );
35
+ fs.mkdirSync(imagesetDir, { recursive: true });
36
+ fs.copyFileSync(src, path.join(imagesetDir, "privacy_image.png"));
37
+ const contents = {
38
+ images: [{ idiom: "universal", filename: "privacy_image.png", scale: "1x" }],
39
+ info: { version: 1, author: "xcode" },
40
+ };
41
+ fs.writeFileSync(path.join(imagesetDir, "Contents.json"), JSON.stringify(contents, null, 2));
42
+ return config;
43
+ },
44
+ ]);
45
+
46
+ return config;
47
+ };
48
+
49
+ export default withPrivacyImage;
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "expo-module-scripts/tsconfig.plugin",
3
+ "compilerOptions": {
4
+ "outDir": "build",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/index.ts"],"version":"5.9.3"}
@@ -0,0 +1,8 @@
1
+ import { NativeModule, requireNativeModule } from "expo";
2
+
3
+ declare class PrivacyImageModule extends NativeModule {
4
+ usePrivacyImage: () => void;
5
+ }
6
+
7
+ // This call loads the native module object from the JSI.
8
+ export default requireNativeModule<PrivacyImageModule>("PrivacyImage");
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import PrivacyImageModule from "./PrivacyImageModule";
2
+
3
+ export function usePrivacyImage() {
4
+ PrivacyImageModule.usePrivacyImage();
5
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ // @generated by expo-module-scripts
2
+ {
3
+ "extends": "expo-module-scripts/tsconfig.base",
4
+ "compilerOptions": {
5
+ "outDir": "./build"
6
+ },
7
+ "include": ["./src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
9
+ }