expo-app-lifecycle-plus 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright 2025 Adem Hatay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,269 @@
1
+ <a id="readme-top"></a>
2
+
3
+ [![Contributors][contributors-shield]][contributors-url]
4
+ [![Forks][forks-shield]][forks-url]
5
+ [![Stargazers][stars-shield]][stars-url]
6
+ [![Issues][issues-shield]][issues-url]
7
+ [![MIT License][license-shield]][license-url]
8
+
9
+ <br />
10
+ <div align="center">
11
+ <h3 align="center">expo-app-lifecycle-plus</h3>
12
+
13
+ <p align="center">
14
+ Native app lifecycle event module for Expo / React Native on Android and iOS.
15
+ <br />
16
+ Android: Process lifecycle + optional activity focus/blur signals
17
+ <br />
18
+ iOS: UIApplication + UIScene notifications with inferred termination support
19
+ <br />
20
+ <a href="https://github.com/ademhatay/expo-app-lifecycle-plus"><strong>Explore the docs »</strong></a>
21
+ <br />
22
+ <br />
23
+ <a href="https://github.com/ademhatay/expo-app-lifecycle-plus/issues/new?labels=bug">Report Bug</a>
24
+ ·
25
+ <a href="https://github.com/ademhatay/expo-app-lifecycle-plus/issues/new?labels=enhancement">Request Feature</a>
26
+ </p>
27
+ </div>
28
+
29
+ <details>
30
+ <summary>Table of Contents</summary>
31
+ <ol>
32
+ <li>
33
+ <a href="#about-the-project">About The Project</a>
34
+ <ul>
35
+ <li><a href="#built-with">Built With</a></li>
36
+ </ul>
37
+ </li>
38
+ <li>
39
+ <a href="#getting-started">Getting Started</a>
40
+ <ul>
41
+ <li><a href="#prerequisites">Prerequisites</a></li>
42
+ <li><a href="#installation">Installation</a></li>
43
+ </ul>
44
+ </li>
45
+ <li><a href="#usage">Usage</a></li>
46
+ <li><a href="#event-reference">Event Reference</a></li>
47
+ <li><a href="#api-reference">API Reference</a></li>
48
+ <li><a href="#platform-notes">Platform Notes</a></li>
49
+ <li><a href="#troubleshooting">Troubleshooting</a></li>
50
+ <li><a href="#contributing">Contributing</a></li>
51
+ <li><a href="#license">License</a></li>
52
+ </ol>
53
+ </details>
54
+
55
+ ## About The Project
56
+
57
+ `expo-app-lifecycle-plus` exposes a single event channel and typed payloads so you can observe app state transitions consistently from JavaScript.
58
+
59
+ It is designed for real-world lifecycle analytics and debugging:
60
+
61
+ - `addListener((event) => ...)` to stream lifecycle events
62
+ - `getCurrentState()` to read current native state snapshot
63
+ - iOS launch/scene signals and inferred termination support
64
+ - Android process foreground/background and optional activity focus/blur
65
+
66
+ ### Built With
67
+
68
+ - [Expo Modules](https://docs.expo.dev/modules/overview/)
69
+ - [React Native](https://reactnative.dev/)
70
+ - [AndroidX Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle)
71
+ - [UIKit UIApplication Notifications](https://developer.apple.com/documentation/uikit/uiapplication)
72
+ - [UIKit UIScene Notifications](https://developer.apple.com/documentation/uikit/uiscene)
73
+
74
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
75
+
76
+ ## Getting Started
77
+
78
+ ### Prerequisites
79
+
80
+ - Node.js LTS (`20` or `22` recommended)
81
+ - Expo / React Native app
82
+
83
+ ### Installation
84
+
85
+ Expo managed or prebuild:
86
+
87
+ ```bash
88
+ npx expo install expo-app-lifecycle-plus
89
+ ```
90
+
91
+ Bare React Native / Expo bare:
92
+
93
+ ```bash
94
+ npm install expo-app-lifecycle-plus
95
+ # or
96
+ bun add expo-app-lifecycle-plus
97
+ ```
98
+
99
+ Rebuild native apps after install:
100
+
101
+ ```bash
102
+ npx expo run:android
103
+ npx expo run:ios
104
+ ```
105
+
106
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
107
+
108
+ ## Usage
109
+
110
+ ```ts
111
+ import * as Lifecycle from 'expo-app-lifecycle-plus';
112
+
113
+ const current = Lifecycle.getCurrentState();
114
+ console.log('current state', current);
115
+
116
+ const sub = Lifecycle.addListener((event) => {
117
+ console.log('lifecycle event', event);
118
+ });
119
+
120
+ // later
121
+ sub.remove();
122
+ ```
123
+
124
+ ### Example Event Payload
125
+
126
+ ```json
127
+ {
128
+ "type": "foreground",
129
+ "state": "foreground",
130
+ "timestamp": 1700000000000,
131
+ "platform": "ios"
132
+ }
133
+ ```
134
+
135
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
136
+
137
+ ## Event Reference
138
+
139
+ ### Common Events
140
+
141
+ - `jsReload`
142
+ - `coldStart`
143
+ - `foreground`
144
+ - `background`
145
+
146
+ ### iOS Events
147
+
148
+ - `appLaunch`
149
+ - `active`
150
+ - `inactive`
151
+ - `sceneActive`
152
+ - `sceneInactive`
153
+ - `willTerminate` (best-effort; not guaranteed)
154
+ - `inferredTermination` (emitted on next launch if app was previously backgrounded and appears to have been killed)
155
+
156
+ ### Android Events
157
+
158
+ - `focusActivity`
159
+ - `blurActivity`
160
+
161
+ ### Event Type
162
+
163
+ ```ts
164
+ type LifecycleEvent = {
165
+ type:
166
+ | 'jsReload'
167
+ | 'coldStart'
168
+ | 'appLaunch'
169
+ | 'inferredTermination'
170
+ | 'foreground'
171
+ | 'background'
172
+ | 'active'
173
+ | 'inactive'
174
+ | 'willTerminate'
175
+ | 'sceneActive'
176
+ | 'sceneInactive'
177
+ | 'focusActivity'
178
+ | 'blurActivity';
179
+ state: 'unknown' | 'foreground' | 'background' | 'active' | 'inactive';
180
+ timestamp: number;
181
+ platform: 'ios' | 'android';
182
+ activity?: string;
183
+ source?: 'didFinishLaunching' | 'observerStart';
184
+ inferredFrom?: 'previousBackground';
185
+ previousBackgroundTimestamp?: number;
186
+ elapsedSinceBackgroundMs?: number;
187
+ };
188
+ ```
189
+
190
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
191
+
192
+ ## API Reference
193
+
194
+ ### `addListener(listener): EventSubscription`
195
+
196
+ Subscribes to native lifecycle updates on `onLifecycleEvent`.
197
+
198
+ ```ts
199
+ addListener(listener: (event: LifecycleEvent) => void): EventSubscription
200
+ ```
201
+
202
+ ### `getCurrentState(): LifecycleState`
203
+
204
+ Returns current native lifecycle state snapshot.
205
+
206
+ ```ts
207
+ getCurrentState(): 'unknown' | 'foreground' | 'background' | 'active' | 'inactive'
208
+ ```
209
+
210
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
211
+
212
+ ## Platform Notes
213
+
214
+ - `willTerminate` is not guaranteed on mobile OSes.
215
+ - On iOS, app kills in background may not emit a terminate callback. Use `inferredTermination` for practical analytics.
216
+ - On Android, process lifecycle (`foreground`/`background`) is generally the most reliable signal.
217
+ - `jsReload` can happen during development due to fast refresh/reload and does not always mean a fresh process launch.
218
+
219
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
220
+
221
+ ## Troubleshooting
222
+
223
+ ### I only see `inactive -> background -> foreground -> active`
224
+
225
+ This is normal lifecycle flow on iOS when app moves between foreground/background.
226
+
227
+ ### `willTerminate` is not emitted
228
+
229
+ Expected on many real-device scenarios. Mobile OS may kill apps without firing terminate callbacks.
230
+
231
+ ### `coldStart` appears more than once in development
232
+
233
+ Development reload / fast refresh can recreate JS/runtime state. Use `jsReload` and `appLaunch` together to interpret startup behavior.
234
+
235
+ ### TypeScript import errors
236
+
237
+ Rebuild package and restart Metro:
238
+
239
+ ```bash
240
+ npm run build
241
+ ```
242
+
243
+ <p align="right">(<a href="#readme-top">back to top</a>)</p>
244
+
245
+ ## Contributing
246
+
247
+ PRs and issues are welcome.
248
+
249
+ 1. Fork the project
250
+ 2. Create branch
251
+ 3. Commit changes
252
+ 4. Push branch
253
+ 5. Open PR
254
+
255
+ ## License
256
+
257
+ MIT © Adem Hatay
258
+
259
+ <!-- MARKDOWN LINKS & IMAGES -->
260
+ [contributors-shield]: https://img.shields.io/github/contributors/ademhatay/expo-app-lifecycle-plus.svg?style=for-the-badge
261
+ [contributors-url]: https://github.com/ademhatay/expo-app-lifecycle-plus/graphs/contributors
262
+ [forks-shield]: https://img.shields.io/github/forks/ademhatay/expo-app-lifecycle-plus.svg?style=for-the-badge
263
+ [forks-url]: https://github.com/ademhatay/expo-app-lifecycle-plus/network/members
264
+ [stars-shield]: https://img.shields.io/github/stars/ademhatay/expo-app-lifecycle-plus.svg?style=for-the-badge
265
+ [stars-url]: https://github.com/ademhatay/expo-app-lifecycle-plus/stargazers
266
+ [issues-shield]: https://img.shields.io/github/issues/ademhatay/expo-app-lifecycle-plus.svg?style=for-the-badge
267
+ [issues-url]: https://github.com/ademhatay/expo-app-lifecycle-plus/issues
268
+ [license-shield]: https://img.shields.io/github/license/ademhatay/expo-app-lifecycle-plus.svg?style=for-the-badge
269
+ [license-url]: https://github.com/ademhatay/expo-app-lifecycle-plus/blob/main/LICENSE
@@ -0,0 +1,43 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ group = 'expo.modules.applifecycleplus'
4
+ version = '0.1.0'
5
+
6
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
+ apply from: expoModulesCorePlugin
8
+ applyKotlinExpoModulesCorePlugin()
9
+ useCoreDependencies()
10
+ useExpoPublishing()
11
+
12
+ // If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
13
+ // The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
14
+ // Most of the time, you may like to manage the Android SDK versions yourself.
15
+ def useManagedAndroidSdkVersions = false
16
+ if (useManagedAndroidSdkVersions) {
17
+ useDefaultAndroidSdkVersions()
18
+ } else {
19
+ buildscript {
20
+ // Simple helper that allows the root project to override versions declared by this library.
21
+ ext.safeExtGet = { prop, fallback ->
22
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
23
+ }
24
+ }
25
+ project.android {
26
+ compileSdkVersion safeExtGet("compileSdkVersion", 36)
27
+ defaultConfig {
28
+ minSdkVersion safeExtGet("minSdkVersion", 24)
29
+ targetSdkVersion safeExtGet("targetSdkVersion", 36)
30
+ }
31
+ }
32
+ }
33
+
34
+ android {
35
+ namespace "expo.modules.applifecycleplus"
36
+ defaultConfig {
37
+ versionCode 1
38
+ versionName "0.1.0"
39
+ }
40
+ lintOptions {
41
+ abortOnError false
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,91 @@
1
+ package expo.modules.applifecycleplus
2
+
3
+ import android.app.Activity
4
+ import android.app.Application
5
+ import android.os.Bundle
6
+ import androidx.lifecycle.DefaultLifecycleObserver
7
+ import androidx.lifecycle.LifecycleOwner
8
+ import androidx.lifecycle.ProcessLifecycleOwner
9
+ import expo.modules.kotlin.modules.Module
10
+ import expo.modules.kotlin.modules.ModuleDefinition
11
+
12
+ class ExpoAppLifecyclePlusModule : Module(), DefaultLifecycleObserver, Application.ActivityLifecycleCallbacks {
13
+ private var didSendStartupEvents = false
14
+ private var currentState: String = "unknown"
15
+ private var application: Application? = null
16
+
17
+ override fun definition() = ModuleDefinition {
18
+ Name("ExpoAppLifecyclePlus")
19
+
20
+ Events("onLifecycleEvent")
21
+
22
+ OnCreate {
23
+ ProcessLifecycleOwner.get().lifecycle.addObserver(this@ExpoAppLifecyclePlusModule)
24
+
25
+ val app = appContext.reactContext?.applicationContext as? Application
26
+ application = app
27
+ app?.registerActivityLifecycleCallbacks(this@ExpoAppLifecyclePlusModule)
28
+ }
29
+
30
+ OnStartObserving {
31
+ if (!didSendStartupEvents) {
32
+ didSendStartupEvents = true
33
+ send("jsReload")
34
+ send("coldStart")
35
+ }
36
+ }
37
+
38
+ Function("getCurrentState") {
39
+ currentState
40
+ }
41
+
42
+ OnDestroy {
43
+ ProcessLifecycleOwner.get().lifecycle.removeObserver(this@ExpoAppLifecyclePlusModule)
44
+ application?.unregisterActivityLifecycleCallbacks(this@ExpoAppLifecyclePlusModule)
45
+ application = null
46
+ }
47
+ }
48
+
49
+ private fun send(type: String, extra: Map<String, Any?> = emptyMap()) {
50
+ currentState = when (type) {
51
+ "foreground", "active" -> "foreground"
52
+ "background", "inactive" -> "background"
53
+ else -> currentState
54
+ }
55
+
56
+ val payload = mutableMapOf<String, Any?>(
57
+ "type" to type,
58
+ "state" to currentState,
59
+ "timestamp" to System.currentTimeMillis(),
60
+ "platform" to "android"
61
+ )
62
+ payload.putAll(extra)
63
+ sendEvent("onLifecycleEvent", payload)
64
+ }
65
+
66
+ override fun onStart(owner: LifecycleOwner) {
67
+ send("foreground")
68
+ }
69
+
70
+ override fun onStop(owner: LifecycleOwner) {
71
+ send("background")
72
+ }
73
+
74
+ override fun onActivityResumed(activity: Activity) {
75
+ send("focusActivity", mapOf("activity" to activity.javaClass.simpleName))
76
+ }
77
+
78
+ override fun onActivityPaused(activity: Activity) {
79
+ send("blurActivity", mapOf("activity" to activity.javaClass.simpleName))
80
+ }
81
+
82
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
83
+
84
+ override fun onActivityStarted(activity: Activity) {}
85
+
86
+ override fun onActivityStopped(activity: Activity) {}
87
+
88
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
89
+
90
+ override fun onActivityDestroyed(activity: Activity) {}
91
+ }
@@ -0,0 +1,17 @@
1
+ export type LifecycleEventType = 'jsReload' | 'coldStart' | 'appLaunch' | 'inferredTermination' | 'foreground' | 'background' | 'active' | 'inactive' | 'willTerminate' | 'sceneActive' | 'sceneInactive' | 'focusActivity' | 'blurActivity';
2
+ export type LifecycleState = 'unknown' | 'foreground' | 'background' | 'active' | 'inactive';
3
+ export type LifecycleEvent = {
4
+ type: LifecycleEventType;
5
+ state: LifecycleState;
6
+ timestamp: number;
7
+ platform: 'ios' | 'android';
8
+ activity?: string;
9
+ source?: 'didFinishLaunching' | 'observerStart';
10
+ inferredFrom?: 'previousBackground';
11
+ previousBackgroundTimestamp?: number;
12
+ elapsedSinceBackgroundMs?: number;
13
+ };
14
+ export type ExpoAppLifecyclePlusModuleEvents = {
15
+ onLifecycleEvent: (event: LifecycleEvent) => void;
16
+ };
17
+ //# sourceMappingURL=ExpoAppLifecyclePlus.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAppLifecyclePlus.types.d.ts","sourceRoot":"","sources":["../src/ExpoAppLifecyclePlus.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAC1B,UAAU,GACV,WAAW,GACX,WAAW,GACX,qBAAqB,GACrB,YAAY,GACZ,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,eAAe,GACf,aAAa,GACb,eAAe,GACf,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,YAAY,GAAG,YAAY,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE7F,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,cAAc,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,oBAAoB,GAAG,eAAe,CAAC;IAChD,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,gBAAgB,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CACnD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ExpoAppLifecyclePlus.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAppLifecyclePlus.types.js","sourceRoot":"","sources":["../src/ExpoAppLifecyclePlus.types.ts"],"names":[],"mappings":"","sourcesContent":["export type LifecycleEventType =\n | 'jsReload'\n | 'coldStart'\n | 'appLaunch'\n | 'inferredTermination'\n | 'foreground'\n | 'background'\n | 'active'\n | 'inactive'\n | 'willTerminate'\n | 'sceneActive'\n | 'sceneInactive'\n | 'focusActivity'\n | 'blurActivity';\n\nexport type LifecycleState = 'unknown' | 'foreground' | 'background' | 'active' | 'inactive';\n\nexport type LifecycleEvent = {\n type: LifecycleEventType;\n state: LifecycleState;\n timestamp: number;\n platform: 'ios' | 'android';\n activity?: string;\n source?: 'didFinishLaunching' | 'observerStart';\n inferredFrom?: 'previousBackground';\n previousBackgroundTimestamp?: number;\n elapsedSinceBackgroundMs?: number;\n};\n\nexport type ExpoAppLifecyclePlusModuleEvents = {\n onLifecycleEvent: (event: LifecycleEvent) => void;\n};\n"]}
@@ -0,0 +1,8 @@
1
+ import { NativeModule } from 'expo';
2
+ import { ExpoAppLifecyclePlusModuleEvents, LifecycleState } from './ExpoAppLifecyclePlus.types';
3
+ declare class ExpoAppLifecyclePlusModule extends NativeModule<ExpoAppLifecyclePlusModuleEvents> {
4
+ getCurrentState(): LifecycleState;
5
+ }
6
+ declare const _default: ExpoAppLifecyclePlusModule;
7
+ export default _default;
8
+ //# sourceMappingURL=ExpoAppLifecyclePlusModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAppLifecyclePlusModule.d.ts","sourceRoot":"","sources":["../src/ExpoAppLifecyclePlusModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EAAE,gCAAgC,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEhG,OAAO,OAAO,0BAA2B,SAAQ,YAAY,CAAC,gCAAgC,CAAC;IAC7F,eAAe,IAAI,cAAc;CAClC;;AAED,wBAAuF"}
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from 'expo';
2
+ export default requireNativeModule('ExpoAppLifecyclePlus');
3
+ //# sourceMappingURL=ExpoAppLifecyclePlusModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAppLifecyclePlusModule.js","sourceRoot":"","sources":["../src/ExpoAppLifecyclePlusModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAQzD,eAAe,mBAAmB,CAA6B,sBAAsB,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport { ExpoAppLifecyclePlusModuleEvents, LifecycleState } from './ExpoAppLifecyclePlus.types';\n\ndeclare class ExpoAppLifecyclePlusModule extends NativeModule<ExpoAppLifecyclePlusModuleEvents> {\n getCurrentState(): LifecycleState;\n}\n\nexport default requireNativeModule<ExpoAppLifecyclePlusModule>('ExpoAppLifecyclePlus');\n"]}
@@ -0,0 +1,6 @@
1
+ import { EventSubscription } from 'expo-modules-core';
2
+ import { LifecycleEvent, LifecycleState } from './ExpoAppLifecyclePlus.types';
3
+ export * from './ExpoAppLifecyclePlus.types';
4
+ export declare function addListener(listener: (event: LifecycleEvent) => void): EventSubscription;
5
+ export declare function getCurrentState(): LifecycleState;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE9E,cAAc,8BAA8B,CAAC;AAE7C,wBAAgB,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,iBAAiB,CAExF;AAED,wBAAgB,eAAe,IAAI,cAAc,CAEhD"}
package/build/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import Module from './ExpoAppLifecyclePlusModule';
2
+ export * from './ExpoAppLifecyclePlus.types';
3
+ export function addListener(listener) {
4
+ return Module.addListener('onLifecycleEvent', listener);
5
+ }
6
+ export function getCurrentState() {
7
+ return Module.getCurrentState();
8
+ }
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,MAAM,MAAM,8BAA8B,CAAC;AAGlD,cAAc,8BAA8B,CAAC;AAE7C,MAAM,UAAU,WAAW,CAAC,QAAyC;IACnE,OAAO,MAAM,CAAC,WAAW,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,MAAM,CAAC,eAAe,EAAE,CAAC;AAClC,CAAC","sourcesContent":["import { EventSubscription } from 'expo-modules-core';\n\nimport Module from './ExpoAppLifecyclePlusModule';\nimport { LifecycleEvent, LifecycleState } from './ExpoAppLifecyclePlus.types';\n\nexport * from './ExpoAppLifecyclePlus.types';\n\nexport function addListener(listener: (event: LifecycleEvent) => void): EventSubscription {\n return Module.addListener('onLifecycleEvent', listener);\n}\n\nexport function getCurrentState(): LifecycleState {\n return Module.getCurrentState();\n}\n"]}
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android", "web"],
3
+ "apple": {
4
+ "modules": ["ExpoAppLifecyclePlusModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.applifecycleplus.ExpoAppLifecyclePlusModule"]
8
+ }
9
+ }
@@ -0,0 +1,29 @@
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 = 'ExpoAppLifecyclePlus'
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
+ :tvos => '15.1'
16
+ }
17
+ s.swift_version = '5.9'
18
+ s.source = { git: 'https://github.com/ademhatay/expo-app-lifecycle-plus' }
19
+ s.static_framework = true
20
+
21
+ s.dependency 'ExpoModulesCore'
22
+
23
+ # Swift/Objective-C compatibility
24
+ s.pod_target_xcconfig = {
25
+ 'DEFINES_MODULE' => 'YES',
26
+ }
27
+
28
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
29
+ end
@@ -0,0 +1,167 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+
4
+ public class ExpoAppLifecyclePlusModule: Module {
5
+ private var didSendStartupEvents = false
6
+ private var didSendAppLaunch = false
7
+ private var observers: [NSObjectProtocol] = []
8
+ private var currentState = "unknown"
9
+ private let defaults = UserDefaults.standard
10
+ private let pendingBackgroundKey = "expo.appLifecyclePlus.pendingBackground"
11
+ private let backgroundTimestampKey = "expo.appLifecyclePlus.backgroundTimestamp"
12
+
13
+ public func definition() -> ModuleDefinition {
14
+ Name("ExpoAppLifecyclePlus")
15
+
16
+ Events("onLifecycleEvent")
17
+
18
+ OnCreate {
19
+ self.currentState = self.readApplicationState()
20
+ let center = NotificationCenter.default
21
+
22
+ self.observers.append(center.addObserver(
23
+ forName: UIApplication.didBecomeActiveNotification,
24
+ object: nil,
25
+ queue: .main
26
+ ) { _ in
27
+ self.clearBackgroundMarker()
28
+ self.send(type: "active", nextState: "active")
29
+ })
30
+
31
+ self.observers.append(center.addObserver(
32
+ forName: UIApplication.willResignActiveNotification,
33
+ object: nil,
34
+ queue: .main
35
+ ) { _ in self.send(type: "inactive", nextState: "inactive") })
36
+
37
+ self.observers.append(center.addObserver(
38
+ forName: UIApplication.willEnterForegroundNotification,
39
+ object: nil,
40
+ queue: .main
41
+ ) { _ in self.send(type: "foreground", nextState: "foreground") })
42
+
43
+ self.observers.append(center.addObserver(
44
+ forName: UIApplication.didEnterBackgroundNotification,
45
+ object: nil,
46
+ queue: .main
47
+ ) { _ in
48
+ self.markEnteredBackground()
49
+ self.send(type: "background", nextState: "background")
50
+ })
51
+
52
+ self.observers.append(center.addObserver(
53
+ forName: UIApplication.willTerminateNotification,
54
+ object: nil,
55
+ queue: .main
56
+ ) { _ in
57
+ self.clearBackgroundMarker()
58
+ self.send(type: "willTerminate")
59
+ })
60
+
61
+ self.observers.append(center.addObserver(
62
+ forName: UIApplication.didFinishLaunchingNotification,
63
+ object: nil,
64
+ queue: .main
65
+ ) { _ in
66
+ self.emitAppLaunch(source: "didFinishLaunching")
67
+ })
68
+
69
+ self.observers.append(center.addObserver(
70
+ forName: UIScene.didActivateNotification,
71
+ object: nil,
72
+ queue: .main
73
+ ) { _ in self.send(type: "sceneActive") })
74
+
75
+ self.observers.append(center.addObserver(
76
+ forName: UIScene.willDeactivateNotification,
77
+ object: nil,
78
+ queue: .main
79
+ ) { _ in self.send(type: "sceneInactive") })
80
+ }
81
+
82
+ OnStartObserving {
83
+ if !self.didSendStartupEvents {
84
+ self.didSendStartupEvents = true
85
+ self.send(type: "jsReload")
86
+ self.send(type: "coldStart")
87
+ self.emitAppLaunch(source: "observerStart")
88
+ }
89
+ }
90
+
91
+ Function("getCurrentState") {
92
+ self.currentState
93
+ }
94
+
95
+ OnDestroy {
96
+ let center = NotificationCenter.default
97
+ self.observers.forEach { center.removeObserver($0) }
98
+ self.observers.removeAll()
99
+ }
100
+ }
101
+
102
+ private func emitAppLaunch(source: String) {
103
+ if didSendAppLaunch {
104
+ return
105
+ }
106
+ didSendAppLaunch = true
107
+ send(type: "appLaunch", extra: ["source": source])
108
+ emitInferredTerminationIfNeeded()
109
+ }
110
+
111
+ private func markEnteredBackground() {
112
+ defaults.set(true, forKey: pendingBackgroundKey)
113
+ defaults.set(Int(Date().timeIntervalSince1970 * 1000), forKey: backgroundTimestampKey)
114
+ }
115
+
116
+ private func clearBackgroundMarker() {
117
+ defaults.set(false, forKey: pendingBackgroundKey)
118
+ }
119
+
120
+ private func emitInferredTerminationIfNeeded() {
121
+ guard defaults.bool(forKey: pendingBackgroundKey) else {
122
+ return
123
+ }
124
+
125
+ let nowMs = Int(Date().timeIntervalSince1970 * 1000)
126
+ let lastBackgroundAt = defaults.integer(forKey: backgroundTimestampKey)
127
+ let elapsedMs = lastBackgroundAt > 0 ? nowMs - lastBackgroundAt : nil
128
+
129
+ send(
130
+ type: "inferredTermination",
131
+ extra: [
132
+ "inferredFrom": "previousBackground",
133
+ "previousBackgroundTimestamp": lastBackgroundAt > 0 ? lastBackgroundAt : nil,
134
+ "elapsedSinceBackgroundMs": elapsedMs
135
+ ]
136
+ )
137
+ clearBackgroundMarker()
138
+ }
139
+
140
+ private func readApplicationState() -> String {
141
+ switch UIApplication.shared.applicationState {
142
+ case .active:
143
+ return "active"
144
+ case .inactive:
145
+ return "inactive"
146
+ case .background:
147
+ return "background"
148
+ @unknown default:
149
+ return "unknown"
150
+ }
151
+ }
152
+
153
+ private func send(type: String, nextState: String? = nil, extra: [String: Any?] = [:]) {
154
+ if let nextState {
155
+ self.currentState = nextState
156
+ }
157
+
158
+ var payload: [String: Any?] = [
159
+ "type": type,
160
+ "state": self.currentState,
161
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000),
162
+ "platform": "ios"
163
+ ]
164
+ extra.forEach { payload[$0.key] = $0.value }
165
+ sendEvent("onLifecycleEvent", payload)
166
+ }
167
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "expo-app-lifecycle-plus",
3
+ "version": "0.1.0",
4
+ "description": "this module handle manage app states",
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
+ "open:ios": "xed example/ios",
16
+ "open:android": "open -a \"Android Studio\" example/android"
17
+ },
18
+ "keywords": [
19
+ "react-native",
20
+ "expo",
21
+ "expo-app-lifecycle-plus",
22
+ "ExpoAppLifecyclePlus"
23
+ ],
24
+ "repository": "https://github.com/ademhatay/expo-app-lifecycle-plus",
25
+ "bugs": {
26
+ "url": "https://github.com/ademhatay/expo-app-lifecycle-plus/issues"
27
+ },
28
+ "author": "Adem Hatay <hatayadem5@gmail.com> (https://github.com/ademhatay)",
29
+ "license": "MIT",
30
+ "homepage": "https://github.com/ademhatay/expo-app-lifecycle-plus#readme",
31
+ "dependencies": {},
32
+ "devDependencies": {
33
+ "@types/react": "~19.1.0",
34
+ "expo-module-scripts": "^5.0.8",
35
+ "expo": "^54.0.27",
36
+ "react-native": "0.81.5"
37
+ },
38
+ "peerDependencies": {
39
+ "expo": "*",
40
+ "react": "*",
41
+ "react-native": "*"
42
+ }
43
+ }
@@ -0,0 +1,32 @@
1
+ export type LifecycleEventType =
2
+ | 'jsReload'
3
+ | 'coldStart'
4
+ | 'appLaunch'
5
+ | 'inferredTermination'
6
+ | 'foreground'
7
+ | 'background'
8
+ | 'active'
9
+ | 'inactive'
10
+ | 'willTerminate'
11
+ | 'sceneActive'
12
+ | 'sceneInactive'
13
+ | 'focusActivity'
14
+ | 'blurActivity';
15
+
16
+ export type LifecycleState = 'unknown' | 'foreground' | 'background' | 'active' | 'inactive';
17
+
18
+ export type LifecycleEvent = {
19
+ type: LifecycleEventType;
20
+ state: LifecycleState;
21
+ timestamp: number;
22
+ platform: 'ios' | 'android';
23
+ activity?: string;
24
+ source?: 'didFinishLaunching' | 'observerStart';
25
+ inferredFrom?: 'previousBackground';
26
+ previousBackgroundTimestamp?: number;
27
+ elapsedSinceBackgroundMs?: number;
28
+ };
29
+
30
+ export type ExpoAppLifecyclePlusModuleEvents = {
31
+ onLifecycleEvent: (event: LifecycleEvent) => void;
32
+ };
@@ -0,0 +1,9 @@
1
+ import { NativeModule, requireNativeModule } from 'expo';
2
+
3
+ import { ExpoAppLifecyclePlusModuleEvents, LifecycleState } from './ExpoAppLifecyclePlus.types';
4
+
5
+ declare class ExpoAppLifecyclePlusModule extends NativeModule<ExpoAppLifecyclePlusModuleEvents> {
6
+ getCurrentState(): LifecycleState;
7
+ }
8
+
9
+ export default requireNativeModule<ExpoAppLifecyclePlusModule>('ExpoAppLifecyclePlus');
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { EventSubscription } from 'expo-modules-core';
2
+
3
+ import Module from './ExpoAppLifecyclePlusModule';
4
+ import { LifecycleEvent, LifecycleState } from './ExpoAppLifecyclePlus.types';
5
+
6
+ export * from './ExpoAppLifecyclePlus.types';
7
+
8
+ export function addListener(listener: (event: LifecycleEvent) => void): EventSubscription {
9
+ return Module.addListener('onLifecycleEvent', listener);
10
+ }
11
+
12
+ export function getCurrentState(): LifecycleState {
13
+ return Module.getCurrentState();
14
+ }
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
+ }