react-native-app-device-info 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,401 @@
1
+ # react-native-app-device-info
2
+
3
+ Minimal, fast, synchronous app and device information for React Native.
4
+
5
+ Every value is read from native constants that are computed once when the app
6
+ starts. From JavaScript there is no `Promise`, no `await`, and no bridge
7
+ round-trip. Every getter is a plain synchronous function call. The library
8
+ works on iOS and Android, on both the Old and the New Architecture.
9
+
10
+ ```ts
11
+ import AppDeviceInfo from 'react-native-app-device-info';
12
+
13
+ AppDeviceInfo.getVersion(); // "1.2.3"
14
+ AppDeviceInfo.getBuildNumber(); // "42"
15
+ AppDeviceInfo.getUniqueId(); // a stable id for this device
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Table of contents
21
+
22
+ - [Why this library](#why-this-library)
23
+ - [Requirements](#requirements)
24
+ - [Installation](#installation)
25
+ - [iOS](#ios)
26
+ - [Android](#android)
27
+ - [Expo](#expo)
28
+ - [Quick start](#quick-start)
29
+ - [API reference](#api-reference)
30
+ - [App information](#app-information)
31
+ - [Device and OS information](#device-and-os-information)
32
+ - [Install information](#install-information)
33
+ - [Get everything at once](#get-everything-at-once)
34
+ - [Field notes](#field-notes)
35
+ - [getUniqueId](#getuniqueid)
36
+ - [getBundleId vs getBaseBundleId](#getbundleid-vs-getbasebundleid)
37
+ - [getInstallerSource](#getinstallersource)
38
+ - [Install and update times](#install-and-update-times)
39
+ - [isEmulator](#isemulator)
40
+ - [How it works](#how-it-works)
41
+ - [TypeScript](#typescript)
42
+ - [Platform support matrix](#platform-support-matrix)
43
+ - [Troubleshooting](#troubleshooting)
44
+ - [License](#license)
45
+
46
+ ---
47
+
48
+ ## Why this library
49
+
50
+ - **Small.** One native module, about twenty getters, zero runtime
51
+ dependencies.
52
+ - **Fast.** Values are exposed as native constants, so reading them is just a
53
+ property access. There is no message sent over the bridge and no startup
54
+ penalty.
55
+ - **Simple.** Synchronous getters. No callbacks, no promises, nothing to wait
56
+ for.
57
+ - **Consistent.** The same API and the same return types on iOS and Android.
58
+ - **Future proof.** Identical behaviour on the legacy bridge and the New
59
+ Architecture (bridgeless / TurboModules interop).
60
+
61
+ ---
62
+
63
+ ## Requirements
64
+
65
+ - React Native 0.64 or newer (Old or New Architecture).
66
+ - iOS 12.0 or newer.
67
+ - Android API level 21 (Android 5.0) or newer.
68
+
69
+ ---
70
+
71
+ ## Installation
72
+
73
+ ```sh
74
+ npm install react-native-app-device-info
75
+ ```
76
+
77
+ or
78
+
79
+ ```sh
80
+ yarn add react-native-app-device-info
81
+ ```
82
+
83
+ This package contains native code, so you must rebuild the app after installing
84
+ it. A Fast Refresh / Metro reload is not enough.
85
+
86
+ ### iOS
87
+
88
+ Install the CocoaPods dependency, then rebuild:
89
+
90
+ ```sh
91
+ cd ios && pod install && cd ..
92
+ npx react-native run-ios
93
+ ```
94
+
95
+ ### Android
96
+
97
+ Autolinking handles everything. Just rebuild the app:
98
+
99
+ ```sh
100
+ npx react-native run-android
101
+ ```
102
+
103
+ ### Expo
104
+
105
+ This is a native module, so it does not run in Expo Go. Use a development
106
+ build:
107
+
108
+ ```sh
109
+ npx expo install react-native-app-device-info
110
+ npx expo prebuild
111
+ npx expo run:ios # or: npx expo run:android
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Quick start
117
+
118
+ You can import the default object and call methods on it:
119
+
120
+ ```ts
121
+ import AppDeviceInfo from 'react-native-app-device-info';
122
+
123
+ console.log(AppDeviceInfo.getReadableVersion()); // "1.2.3 (42)"
124
+ console.log(AppDeviceInfo.getOsName(), AppDeviceInfo.getOsVersion()); // "iOS" "17.4"
125
+ console.log(AppDeviceInfo.getUniqueId());
126
+ ```
127
+
128
+ Or import only the functions you need:
129
+
130
+ ```ts
131
+ import { getVersion, getBuildNumber, getInfo } from 'react-native-app-device-info';
132
+
133
+ const version = getVersion();
134
+ const build = getBuildNumber();
135
+ const everything = getInfo();
136
+ ```
137
+
138
+ Both styles are equivalent and fully typed.
139
+
140
+ ---
141
+
142
+ ## API reference
143
+
144
+ Every function is synchronous and returns immediately. The tables show the
145
+ return type and the native source on each platform.
146
+
147
+ ### App information
148
+
149
+ | Function | Returns | iOS | Android |
150
+ | ---------------------- | -------- | ---------------------------- | ---------------------------------------------------- |
151
+ | `getVersion()` | `string` | `CFBundleShortVersionString` | `versionName` |
152
+ | `getBuildNumber()` | `string` | `CFBundleVersion` | `versionCode` |
153
+ | `getBundleId()` | `string` | `bundleIdentifier` | `packageName` (runtime applicationId, incl. suffix) |
154
+ | `getBaseBundleId()` | `string` | bundle id without suffix | applicationId without suffix |
155
+ | `getAppName()` | `string` | display name | application label |
156
+ | `getReadableVersion()` | `string` | `"1.2.3 (42)"` | `"1.2.3 (42)"` |
157
+
158
+ `getVersion()` is the human-facing version (the "version name" / "build name").
159
+ `getBuildNumber()` is the internal incrementing number (the "build number" /
160
+ "version code").
161
+
162
+ ### Device and OS information
163
+
164
+ | Function | Returns | iOS | Android |
165
+ | ------------------- | -------------------- | --------------------- | ------------------------ |
166
+ | `getOsName()` | `string` | `"iOS"` | `"Android"` |
167
+ | `getOsVersion()` | `string` | `systemVersion` | `Build.VERSION.RELEASE` |
168
+ | `getDeviceType()` | `'ios' \| 'android'` | `"ios"` | `"android"` |
169
+ | `getDeviceModel()` | `string` | e.g. `"iPhone15,2"` | `Build.MODEL` |
170
+ | `getDeviceBrand()` | `string` | `"Apple"` | `Build.MANUFACTURER` |
171
+ | `isTablet()` | `boolean` | iPad? | large screen? |
172
+ | `getUniqueId()` | `string` | `identifierForVendor` | `Settings.Secure.ANDROID_ID` |
173
+ | `isEmulator()` | `boolean` | running on simulator? | running on emulator? |
174
+ | `getDeviceLocale()` | `string` | e.g. `"en-US"` | e.g. `"en-US"` |
175
+ | `getTimezone()` | `string` | e.g. `"America/New_York"` | e.g. `"America/New_York"` |
176
+ | `getApiLevel()` | `number` | `0` (not applicable) | `Build.VERSION.SDK_INT` |
177
+
178
+ ### Install information
179
+
180
+ | Function | Returns | iOS | Android |
181
+ | ----------------------- | -------- | ------------------------------------------ | ----------------------------------------------------------------- |
182
+ | `getInstallerSource()` | `string` | `"AppStore"` / `"TestFlight"` / `"Other"` | installing package, e.g. `"com.android.vending"`, or `""` |
183
+ | `getFirstInstallTime()` | `number` | epoch milliseconds (heuristic) | epoch milliseconds (exact) |
184
+ | `getLastUpdateTime()` | `number` | epoch milliseconds (heuristic) | epoch milliseconds (exact) |
185
+
186
+ Times are epoch **milliseconds**. Wrap a value with `new Date(ms)` to get a
187
+ `Date`. A value of `0` means the time could not be determined.
188
+
189
+ ### Get everything at once
190
+
191
+ `getInfo()` returns every value in a single object. The result is computed once
192
+ and cached, so calling it repeatedly is free.
193
+
194
+ ```ts
195
+ import { getInfo } from 'react-native-app-device-info';
196
+
197
+ const info = getInfo();
198
+ ```
199
+
200
+ The returned object has this exact shape:
201
+
202
+ ```ts
203
+ interface AppDeviceInfoConstants {
204
+ // App
205
+ appVersion: string; // "1.2.3"
206
+ buildNumber: string; // "42"
207
+ bundleId: string; // "com.acme.app"
208
+ appName: string; // "Acme"
209
+
210
+ // Device and OS
211
+ osName: string; // "iOS" | "Android"
212
+ osVersion: string; // "17.4"
213
+ deviceType: 'ios' | 'android';
214
+ deviceModel: string; // "iPhone15,2" | "Pixel 8"
215
+ deviceBrand: string; // "Apple" | "Google"
216
+ isTablet: boolean;
217
+ deviceId: string; // stable per-device id
218
+ isEmulator: boolean;
219
+ deviceLocale: string; // "en-US"
220
+ timezone: string; // "America/New_York"
221
+ apiLevel: number; // 34 on Android, 0 on iOS
222
+
223
+ // Install
224
+ installerSource: string; // "AppStore" | "com.android.vending" | ...
225
+ firstInstallTime: number; // epoch ms, 0 if unknown
226
+ lastUpdateTime: number; // epoch ms, 0 if unknown
227
+ }
228
+ ```
229
+
230
+ Note that `getInfo().appVersion` is the value behind `getVersion()`,
231
+ `getInfo().buildNumber` is the value behind `getBuildNumber()`, and so on. The
232
+ individual getters are thin convenience wrappers around this object.
233
+
234
+ ---
235
+
236
+ ## Field notes
237
+
238
+ This section explains the values that need context so you choose the right one
239
+ and know exactly what it guarantees.
240
+
241
+ ### getUniqueId
242
+
243
+ A stable id for the device that persists across app launches and app updates.
244
+
245
+ - **iOS** returns `identifierForVendor`. It stays the same while at least one
246
+ app from the same vendor (the same Apple Developer account) is installed. If
247
+ the user deletes every app from that vendor and reinstalls, a new id is
248
+ generated.
249
+ - **Android** returns `Settings.Secure.ANDROID_ID`. It stays the same until a
250
+ factory reset. On Android 8.0 and newer the value is scoped to your app's
251
+ signing key, so different apps see different ids on the same device.
252
+
253
+ This is the identifier that both platforms recommend for ordinary app use. It
254
+ needs no extra permissions and does not expose a hardware serial number. It is
255
+ not designed for cross-vendor tracking and is not guaranteed to be globally
256
+ unique across the entire world, only stable for your app on a given device.
257
+
258
+ ### getBundleId vs getBaseBundleId
259
+
260
+ - `getBundleId()` returns the identifier of the build that is actually running.
261
+ On Android this includes any `applicationIdSuffix` from your Gradle config,
262
+ so a debug build can report `com.acme.app.debug`. On iOS it is whatever
263
+ bundle identifier the running build was signed with, including any `.dev` or
264
+ `.staging` suffix.
265
+ - `getBaseBundleId()` returns the same value with one recognised build-variant
266
+ suffix removed. For example `com.acme.app.debug` becomes `com.acme.app`.
267
+
268
+ The suffixes that are stripped are: `debug`, `dev`, `development`, `staging`,
269
+ `stg`, `qa`, `test`, `alpha`, `beta`, `release`, `internal`. Only the final
270
+ segment is checked, and only if it matches that list, so a normal id such as
271
+ `com.acme.app` is never shortened by mistake. Use `getBaseBundleId()` when you
272
+ want one identity for an app across all of its build variants, for example as a
273
+ key in analytics or backend records.
274
+
275
+ ### getInstallerSource
276
+
277
+ Tells you where the installed build came from.
278
+
279
+ - **Android** returns the package name of the installer. Common values are
280
+ `com.android.vending` (Google Play) and `com.amazon.venezia` (Amazon
281
+ Appstore). A build installed with `adb`, a file manager, or a CI device farm
282
+ usually returns an empty string.
283
+ - **iOS** has no public installer API, so the library infers the source from
284
+ the App Store receipt: `"TestFlight"` for a TestFlight build, `"AppStore"`
285
+ for a build delivered through the App Store, and `"Other"` for development,
286
+ simulator, or sideloaded builds.
287
+
288
+ ### Install and update times
289
+
290
+ `getFirstInstallTime()` and `getLastUpdateTime()` return epoch milliseconds.
291
+
292
+ - **Android** reads exact values from `PackageInfo.firstInstallTime` and
293
+ `PackageInfo.lastUpdateTime`.
294
+ - **iOS** has no public API for these, so the library uses a filesystem
295
+ heuristic: the creation date of the app's Documents directory approximates
296
+ the first install, and the modification date of the app executable
297
+ approximates the last update. Treat the iOS numbers as a best effort rather
298
+ than an exact record.
299
+
300
+ ### isEmulator
301
+
302
+ Returns `true` when the app is running on a simulator (iOS) or an emulator
303
+ (Android).
304
+
305
+ - **iOS** uses a compile-time simulator flag, so the result is reliable.
306
+ - **Android** has no single official flag, so the library inspects standard
307
+ `Build` properties (fingerprint, model, hardware, manufacturer, product).
308
+ This detects the common emulators, including the Android Studio AVDs and
309
+ Genymotion. Treat it as a strong best effort rather than a guarantee.
310
+
311
+ ---
312
+
313
+ ## How it works
314
+
315
+ The native module implements the classic React Native constants API
316
+ (`constantsToExport` on iOS, `getConstants()` on Android). The platform builds
317
+ a single dictionary of values when the module is created and hands it to
318
+ JavaScript. The JavaScript layer reads that dictionary once, normalises every
319
+ value, caches the result, and returns it from each getter.
320
+
321
+ Because the values are constants rather than method calls, there is no
322
+ asynchronous message and no serialization cost at call time. This is why every
323
+ function in the API can be synchronous, and why the library adds effectively
324
+ nothing to startup beyond reading a handful of system properties.
325
+
326
+ The JavaScript layer also supports both shapes that React Native can hand it:
327
+ constants attached directly to the module object (legacy bridge) and a
328
+ `getConstants()` method (New Architecture interop). You do not need to configure
329
+ anything for either case.
330
+
331
+ ---
332
+
333
+ ## TypeScript
334
+
335
+ The package ships its own type definitions. The default export, every named
336
+ function, and the `AppDeviceInfoConstants` interface are fully typed. No
337
+ `@types` package is required.
338
+
339
+ ```ts
340
+ import AppDeviceInfo, {
341
+ getInfo,
342
+ type AppDeviceInfoConstants,
343
+ } from 'react-native-app-device-info';
344
+
345
+ const info: AppDeviceInfoConstants = getInfo();
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Platform support matrix
351
+
352
+ | Value | iOS | Android |
353
+ | ------------------ | ------------------------- | ---------------------- |
354
+ | App version | Yes | Yes |
355
+ | Build number | Yes | Yes |
356
+ | Bundle / package id| Yes | Yes |
357
+ | Base bundle id | Yes | Yes |
358
+ | App name | Yes | Yes |
359
+ | OS name / version | Yes | Yes |
360
+ | Device type | Yes | Yes |
361
+ | Device model | Yes | Yes |
362
+ | Device brand | `"Apple"` | Yes |
363
+ | Is tablet | Yes | Yes |
364
+ | Unique id | Yes (`identifierForVendor`) | Yes (`ANDROID_ID`) |
365
+ | Is emulator | Yes (reliable) | Yes (heuristic) |
366
+ | Locale | Yes | Yes |
367
+ | Timezone | Yes | Yes |
368
+ | API level | `0` (not applicable) | Yes |
369
+ | Installer source | Yes (inferred) | Yes |
370
+ | First install time | Heuristic | Yes (exact) |
371
+ | Last update time | Heuristic | Yes (exact) |
372
+
373
+ ---
374
+
375
+ ## Troubleshooting
376
+
377
+ **The module is not linked / values throw an error.**
378
+ Rebuild the native app after installing. A Metro reload does not include new
379
+ native code. On iOS, run `pod install` first.
380
+
381
+ **It does not work in Expo Go.**
382
+ Expo Go cannot load custom native modules. Use a development build with
383
+ `npx expo prebuild` and `npx expo run:ios` or `npx expo run:android`.
384
+
385
+ **`getBuildNumber()` returns a different value than I set.**
386
+ It returns the value of the build that is actually running. Make sure you are
387
+ looking at the same build configuration (debug vs release) that you edited.
388
+
389
+ **`getApiLevel()` returns `0`.**
390
+ That is expected on iOS, which has no Android-style API level. Use
391
+ `getOsVersion()` on iOS instead.
392
+
393
+ **`getFirstInstallTime()` looks wrong on iOS.**
394
+ iOS does not expose an install timestamp, so the value is a filesystem-based
395
+ estimate. Use the Android values when you need exact timing.
396
+
397
+ ---
398
+
399
+ ## License
400
+
401
+ MIT
@@ -0,0 +1,48 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = { name, fallback ->
3
+ rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
4
+ }
5
+ repositories {
6
+ google()
7
+ mavenCentral()
8
+ }
9
+ dependencies {
10
+ classpath "com.android.tools.build:gradle:8.1.1"
11
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion', '1.8.0')}"
12
+ }
13
+ }
14
+
15
+ apply plugin: "com.android.library"
16
+ apply plugin: "kotlin-android"
17
+
18
+ def getExtOrIntegerDefault(name, fallback) {
19
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback
20
+ }
21
+
22
+ android {
23
+ namespace "com.appdeviceinfo"
24
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion", 34)
25
+
26
+ defaultConfig {
27
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion", 21)
28
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion", 34)
29
+ }
30
+
31
+ compileOptions {
32
+ sourceCompatibility JavaVersion.VERSION_17
33
+ targetCompatibility JavaVersion.VERSION_17
34
+ }
35
+
36
+ kotlinOptions {
37
+ jvmTarget = "17"
38
+ }
39
+ }
40
+
41
+ repositories {
42
+ google()
43
+ mavenCentral()
44
+ }
45
+
46
+ dependencies {
47
+ implementation "com.facebook.react:react-native:+"
48
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,120 @@
1
+ package com.appdeviceinfo
2
+
3
+ import android.content.Context
4
+ import android.content.pm.PackageInfo
5
+ import android.os.Build
6
+ import android.provider.Settings
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
9
+
10
+ class AppDeviceInfoModule(reactContext: ReactApplicationContext) :
11
+ ReactContextBaseJavaModule(reactContext) {
12
+
13
+ override fun getName(): String = NAME
14
+
15
+ // Constants are computed natively and read synchronously from JS at load
16
+ // time. No async bridge call is ever made — fastest possible path.
17
+ override fun getConstants(): Map<String, Any> {
18
+ val ctx: Context = reactApplicationContext
19
+ val map = HashMap<String, Any>()
20
+
21
+ var appVersion = ""
22
+ var buildNumber = ""
23
+ var appName = ""
24
+ var firstInstallTime = 0.0
25
+ var lastUpdateTime = 0.0
26
+ var installerSource = ""
27
+ val bundleId = ctx.packageName ?: ""
28
+
29
+ try {
30
+ val pm = ctx.packageManager
31
+ val info: PackageInfo = pm.getPackageInfo(bundleId, 0)
32
+ appVersion = info.versionName ?: ""
33
+ buildNumber =
34
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
35
+ info.longVersionCode.toString()
36
+ } else {
37
+ @Suppress("DEPRECATION")
38
+ info.versionCode.toString()
39
+ }
40
+ appName = ctx.applicationInfo.loadLabel(pm).toString()
41
+ firstInstallTime = info.firstInstallTime.toDouble()
42
+ lastUpdateTime = info.lastUpdateTime.toDouble()
43
+ installerSource =
44
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
45
+ pm.getInstallSourceInfo(bundleId).installingPackageName ?: ""
46
+ } else {
47
+ @Suppress("DEPRECATION")
48
+ pm.getInstallerPackageName(bundleId) ?: ""
49
+ }
50
+ } catch (e: Exception) {
51
+ // Leave defaults; never crash app startup over metadata.
52
+ }
53
+
54
+ val locale =
55
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
56
+ ctx.resources.configuration.locales[0]
57
+ } else {
58
+ @Suppress("DEPRECATION")
59
+ ctx.resources.configuration.locale
60
+ }
61
+
62
+ val deviceId =
63
+ try {
64
+ Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID) ?: ""
65
+ } catch (e: Exception) {
66
+ ""
67
+ }
68
+
69
+ map["appVersion"] = appVersion
70
+ map["buildNumber"] = buildNumber
71
+ map["bundleId"] = bundleId
72
+ map["appName"] = appName
73
+
74
+ map["osName"] = "Android"
75
+ map["osVersion"] = Build.VERSION.RELEASE ?: ""
76
+ map["deviceType"] = "android"
77
+
78
+ map["deviceModel"] = Build.MODEL ?: ""
79
+ map["deviceBrand"] = Build.MANUFACTURER ?: ""
80
+ map["isTablet"] = isTablet(ctx)
81
+
82
+ map["deviceId"] = deviceId
83
+
84
+ map["isEmulator"] = isEmulator()
85
+ map["deviceLocale"] = locale.toLanguageTag()
86
+ map["timezone"] = java.util.TimeZone.getDefault().id ?: ""
87
+ map["apiLevel"] = Build.VERSION.SDK_INT
88
+
89
+ map["installerSource"] = installerSource
90
+ map["firstInstallTime"] = firstInstallTime
91
+ map["lastUpdateTime"] = lastUpdateTime
92
+
93
+ return map
94
+ }
95
+
96
+ private fun isEmulator(): Boolean {
97
+ return (Build.FINGERPRINT.startsWith("generic") ||
98
+ Build.FINGERPRINT.startsWith("unknown") ||
99
+ Build.MODEL.contains("google_sdk") ||
100
+ Build.MODEL.contains("Emulator") ||
101
+ Build.MODEL.contains("Android SDK built for") ||
102
+ Build.MANUFACTURER.contains("Genymotion") ||
103
+ Build.HARDWARE.contains("goldfish") ||
104
+ Build.HARDWARE.contains("ranchu") ||
105
+ Build.PRODUCT == "google_sdk" ||
106
+ Build.PRODUCT == "sdk" ||
107
+ Build.PRODUCT.contains("sdk_gphone") ||
108
+ (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")))
109
+ }
110
+
111
+ private fun isTablet(ctx: Context): Boolean {
112
+ val layout = ctx.resources.configuration.screenLayout and
113
+ android.content.res.Configuration.SCREENLAYOUT_SIZE_MASK
114
+ return layout >= android.content.res.Configuration.SCREENLAYOUT_SIZE_LARGE
115
+ }
116
+
117
+ companion object {
118
+ const val NAME = "AppDeviceInfo"
119
+ }
120
+ }
@@ -0,0 +1,16 @@
1
+ package com.appdeviceinfo
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class AppDeviceInfoPackage : ReactPackage {
9
+ override fun createNativeModules(
10
+ reactContext: ReactApplicationContext
11
+ ): List<NativeModule> = listOf(AppDeviceInfoModule(reactContext))
12
+
13
+ override fun createViewManagers(
14
+ reactContext: ReactApplicationContext
15
+ ): List<ViewManager<*, *>> = emptyList()
16
+ }
@@ -0,0 +1,4 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface AppDeviceInfo : NSObject <RCTBridgeModule>
4
+ @end
@@ -0,0 +1,108 @@
1
+ #import "AppDeviceInfo.h"
2
+ #import <sys/utsname.h>
3
+ #import <UIKit/UIKit.h>
4
+
5
+ @implementation AppDeviceInfo
6
+
7
+ RCT_EXPORT_MODULE();
8
+
9
+ // Constants are computed natively and read synchronously from JS at load time.
10
+ // No async bridge call is ever made — this is the fastest possible path.
11
+ + (BOOL)requiresMainQueueSetup
12
+ {
13
+ // We touch UIDevice, so build the constants on the main queue.
14
+ return YES;
15
+ }
16
+
17
+ - (NSString *)hardwareModelIdentifier
18
+ {
19
+ struct utsname systemInfo;
20
+ uname(&systemInfo);
21
+ return [NSString stringWithCString:systemInfo.machine
22
+ encoding:NSUTF8StringEncoding];
23
+ }
24
+
25
+ - (NSDictionary *)constantsToExport
26
+ {
27
+ NSBundle *bundle = [NSBundle mainBundle];
28
+ NSDictionary *info = [bundle infoDictionary];
29
+
30
+ NSString *appVersion = info[@"CFBundleShortVersionString"] ?: @"";
31
+ NSString *buildNumber = info[@"CFBundleVersion"] ?: @"";
32
+ NSString *bundleId = [bundle bundleIdentifier] ?: @"";
33
+ NSString *appName = info[@"CFBundleDisplayName"]
34
+ ?: (info[@"CFBundleName"] ?: @"");
35
+
36
+ UIDevice *device = [UIDevice currentDevice];
37
+ NSString *osName = [device systemName] ?: @"iOS";
38
+ NSString *osVersion = [device systemVersion] ?: @"";
39
+
40
+ NSString *deviceId = [[device identifierForVendor] UUIDString] ?: @"";
41
+ BOOL isTablet = ([device userInterfaceIdiom] == UIUserInterfaceIdiomPad);
42
+
43
+ BOOL isSimulator = NO;
44
+ #if TARGET_OS_SIMULATOR
45
+ isSimulator = YES;
46
+ #endif
47
+
48
+ NSString *locale = [[NSLocale preferredLanguages] firstObject]
49
+ ?: [[NSLocale currentLocale] localeIdentifier] ?: @"";
50
+ NSString *timezone = [[NSTimeZone localTimeZone] name] ?: @"";
51
+
52
+ // Installer source: TestFlight builds carry a "sandboxReceipt"; App Store
53
+ // builds carry a "receipt"; dev/sideloaded builds usually have neither.
54
+ NSString *installerSource = @"Other";
55
+ NSURL *receiptURL = [bundle appStoreReceiptURL];
56
+ NSString *receiptName = [receiptURL lastPathComponent];
57
+ if ([receiptName isEqualToString:@"sandboxReceipt"]) {
58
+ installerSource = @"TestFlight";
59
+ } else if (receiptURL &&
60
+ [[NSFileManager defaultManager] fileExistsAtPath:[receiptURL path]]) {
61
+ installerSource = @"AppStore";
62
+ }
63
+
64
+ // iOS has no public install/update timestamps; approximate with filesystem
65
+ // dates (Documents creation = first install, executable mtime = last update).
66
+ NSFileManager *fm = [NSFileManager defaultManager];
67
+ double firstInstallTime = 0;
68
+ double lastUpdateTime = 0;
69
+ NSArray *docDirs =
70
+ NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
71
+ if (docDirs.count > 0) {
72
+ NSDate *d = [[fm attributesOfItemAtPath:docDirs[0] error:nil] fileCreationDate];
73
+ if (d) firstInstallTime = [d timeIntervalSince1970] * 1000.0;
74
+ }
75
+ NSString *execPath = [bundle executablePath];
76
+ if (execPath) {
77
+ NSDate *d = [[fm attributesOfItemAtPath:execPath error:nil] fileModificationDate];
78
+ if (d) lastUpdateTime = [d timeIntervalSince1970] * 1000.0;
79
+ }
80
+
81
+ return @{
82
+ @"appVersion": appVersion,
83
+ @"buildNumber": buildNumber,
84
+ @"bundleId": bundleId,
85
+ @"appName": appName,
86
+
87
+ @"osName": osName,
88
+ @"osVersion": osVersion,
89
+ @"deviceType": @"ios",
90
+
91
+ @"deviceModel": [self hardwareModelIdentifier],
92
+ @"deviceBrand": @"Apple",
93
+ @"isTablet": @(isTablet),
94
+
95
+ @"deviceId": deviceId,
96
+
97
+ @"isEmulator": @(isSimulator),
98
+ @"deviceLocale": locale,
99
+ @"timezone": timezone,
100
+ @"apiLevel": @0,
101
+
102
+ @"installerSource": installerSource,
103
+ @"firstInstallTime": @(firstInstallTime),
104
+ @"lastUpdateTime": @(lastUpdateTime),
105
+ };
106
+ }
107
+
108
+ @end
package/lib/index.d.ts ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shape of the constants exported synchronously by the native module.
3
+ * These are read once, at module load time — there is no async bridge call.
4
+ */
5
+ export interface AppDeviceInfoConstants {
6
+ /** App marketing version, a.k.a. version name. iOS: CFBundleShortVersionString, Android: versionName. e.g. "1.2.3" */
7
+ appVersion: string;
8
+ /** App build number / version code. iOS: CFBundleVersion, Android: versionCode. e.g. "42" */
9
+ buildNumber: string;
10
+ /** Application/bundle identifier. e.g. "com.acme.app" */
11
+ bundleId: string;
12
+ /** Human-readable app display name. */
13
+ appName: string;
14
+ /** OS name. "iOS" or "Android". */
15
+ osName: string;
16
+ /** OS version. e.g. "17.4" or "14". */
17
+ osVersion: string;
18
+ /** Platform type, handy for branching. "ios" or "android". */
19
+ deviceType: 'ios' | 'android';
20
+ /** Hardware model identifier. iOS: e.g. "iPhone15,2". Android: Build.MODEL e.g. "Pixel 8". */
21
+ deviceModel: string;
22
+ /** Device brand / manufacturer. iOS: "Apple". Android: Build.MANUFACTURER e.g. "Google". */
23
+ deviceBrand: string;
24
+ /** Whether the device is a tablet / iPad. */
25
+ isTablet: boolean;
26
+ /**
27
+ * A stable unique id for this device, persistent across app launches.
28
+ * iOS: identifierForVendor (stable while at least one app from the same
29
+ * vendor stays installed; may change if all are uninstalled).
30
+ * Android: Settings.Secure.ANDROID_ID (stable until factory reset; scoped
31
+ * to the app signing key on Android 8+).
32
+ */
33
+ deviceId: string;
34
+ /** True when running on a simulator (iOS) or emulator (Android). */
35
+ isEmulator: boolean;
36
+ /** Active locale at launch in BCP-47 form, e.g. "en-US". */
37
+ deviceLocale: string;
38
+ /** IANA time zone id, e.g. "America/New_York". */
39
+ timezone: string;
40
+ /** Android API level (Build.VERSION.SDK_INT), e.g. 34. Always 0 on iOS. */
41
+ apiLevel: number;
42
+ /**
43
+ * Where the app was installed from.
44
+ * Android: installing package, e.g. "com.android.vending" (Play Store) or
45
+ * "" when sideloaded.
46
+ * iOS: "AppStore", "TestFlight", or "Other" (dev/sideloaded builds).
47
+ */
48
+ installerSource: string;
49
+ /** First-install time as epoch milliseconds. 0 if unknown (iOS is a heuristic). */
50
+ firstInstallTime: number;
51
+ /** Last-update time as epoch milliseconds. 0 if unknown (iOS is a heuristic). */
52
+ lastUpdateTime: number;
53
+ }
54
+ /**
55
+ * Returns every value at once. Synchronous — no Promise, no bridge round-trip.
56
+ * The result is computed once and cached.
57
+ */
58
+ export declare function getInfo(): AppDeviceInfoConstants;
59
+ /** App marketing version / version name, e.g. "1.2.3". */
60
+ export declare const getVersion: () => string;
61
+ /** App build number / version code, e.g. "42". */
62
+ export declare const getBuildNumber: () => string;
63
+ /** Bundle / application id, e.g. "com.acme.app". On Android this is the
64
+ * runtime applicationId, including any build-variant suffix (".debug" etc). */
65
+ export declare const getBundleId: () => string;
66
+ /** Bundle id with a known build-variant suffix removed, e.g.
67
+ * "com.acme.app.debug" -> "com.acme.app". Only strips recognised suffixes
68
+ * (see KNOWN_BUNDLE_SUFFIXES); otherwise returns the id unchanged. */
69
+ export declare const getBaseBundleId: () => string;
70
+ /** App display name. */
71
+ export declare const getAppName: () => string;
72
+ /** Convenience: "1.2.3 (42)". */
73
+ export declare const getReadableVersion: () => string;
74
+ /** OS name: "iOS" or "Android". */
75
+ export declare const getOsName: () => string;
76
+ /** OS version, e.g. "17.4". */
77
+ export declare const getOsVersion: () => string;
78
+ /** Platform type: "ios" or "android". */
79
+ export declare const getDeviceType: () => "ios" | "android";
80
+ /** Hardware model identifier, e.g. "iPhone15,2" or "Pixel 8". */
81
+ export declare const getDeviceModel: () => string;
82
+ /** Device brand / manufacturer, e.g. "Apple" or "Google". */
83
+ export declare const getDeviceBrand: () => string;
84
+ /** True on iPad / Android tablets. */
85
+ export declare const isTablet: () => boolean;
86
+ /** Stable, persistent unique device id. See `AppDeviceInfoConstants.deviceId`. */
87
+ export declare const getUniqueId: () => string;
88
+ /** True on a simulator (iOS) / emulator (Android). */
89
+ export declare const isEmulator: () => boolean;
90
+ /** Active locale at launch, BCP-47, e.g. "en-US". */
91
+ export declare const getDeviceLocale: () => string;
92
+ /** IANA time zone id, e.g. "America/New_York". */
93
+ export declare const getTimezone: () => string;
94
+ /** Android API level (e.g. 34); 0 on iOS. */
95
+ export declare const getApiLevel: () => number;
96
+ /** Where the app was installed from. See `AppDeviceInfoConstants.installerSource`. */
97
+ export declare const getInstallerSource: () => string;
98
+ /** First-install time, epoch ms (0 if unknown). */
99
+ export declare const getFirstInstallTime: () => number;
100
+ /** Last-update time, epoch ms (0 if unknown). */
101
+ export declare const getLastUpdateTime: () => number;
102
+ declare const AppDeviceInfo: {
103
+ getInfo: typeof getInfo;
104
+ getVersion: () => string;
105
+ getBuildNumber: () => string;
106
+ getBundleId: () => string;
107
+ getBaseBundleId: () => string;
108
+ getAppName: () => string;
109
+ getReadableVersion: () => string;
110
+ getOsName: () => string;
111
+ getOsVersion: () => string;
112
+ getDeviceType: () => "ios" | "android";
113
+ getDeviceModel: () => string;
114
+ getDeviceBrand: () => string;
115
+ isTablet: () => boolean;
116
+ getUniqueId: () => string;
117
+ isEmulator: () => boolean;
118
+ getDeviceLocale: () => string;
119
+ getTimezone: () => string;
120
+ getApiLevel: () => number;
121
+ getInstallerSource: () => string;
122
+ getFirstInstallTime: () => number;
123
+ getLastUpdateTime: () => number;
124
+ };
125
+ export default AppDeviceInfo;
package/lib/index.js ADDED
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var _a;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.getLastUpdateTime = exports.getFirstInstallTime = exports.getInstallerSource = exports.getApiLevel = exports.getTimezone = exports.getDeviceLocale = exports.isEmulator = exports.getUniqueId = exports.isTablet = exports.getDeviceBrand = exports.getDeviceModel = exports.getDeviceType = exports.getOsVersion = exports.getOsName = exports.getReadableVersion = exports.getAppName = exports.getBaseBundleId = exports.getBundleId = exports.getBuildNumber = exports.getVersion = void 0;
5
+ exports.getInfo = getInfo;
6
+ const react_native_1 = require("react-native");
7
+ const LINKING_ERROR = `The package 'react-native-app-device-info' doesn't seem to be linked. Make sure:\n\n` +
8
+ react_native_1.Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
9
+ '- You rebuilt the app after installing the package\n' +
10
+ '- You are not using Expo Go (use a development build instead)\n';
11
+ /** Build-variant suffixes stripped by `getBaseBundleId()`. */
12
+ const KNOWN_BUNDLE_SUFFIXES = [
13
+ 'debug',
14
+ 'dev',
15
+ 'development',
16
+ 'staging',
17
+ 'stg',
18
+ 'qa',
19
+ 'test',
20
+ 'alpha',
21
+ 'beta',
22
+ 'release',
23
+ 'internal',
24
+ ];
25
+ const Native = (_a = react_native_1.NativeModules.AppDeviceInfo) !== null && _a !== void 0 ? _a : null;
26
+ function readConstants() {
27
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
28
+ if (!Native) {
29
+ throw new Error(LINKING_ERROR);
30
+ }
31
+ // New Architecture / TurboModule interop exposes a getConstants() method;
32
+ // legacy bridge exposes the values directly on the module object. Support both.
33
+ const source = typeof Native.getConstants === 'function' ? Native.getConstants() : Native;
34
+ return {
35
+ appVersion: String((_a = source.appVersion) !== null && _a !== void 0 ? _a : ''),
36
+ buildNumber: String((_b = source.buildNumber) !== null && _b !== void 0 ? _b : ''),
37
+ bundleId: String((_c = source.bundleId) !== null && _c !== void 0 ? _c : ''),
38
+ appName: String((_d = source.appName) !== null && _d !== void 0 ? _d : ''),
39
+ osName: String((_e = source.osName) !== null && _e !== void 0 ? _e : ''),
40
+ osVersion: String((_f = source.osVersion) !== null && _f !== void 0 ? _f : ''),
41
+ deviceType: (source.deviceType === 'android' ? 'android' : 'ios'),
42
+ deviceModel: String((_g = source.deviceModel) !== null && _g !== void 0 ? _g : ''),
43
+ deviceBrand: String((_h = source.deviceBrand) !== null && _h !== void 0 ? _h : ''),
44
+ isTablet: Boolean(source.isTablet),
45
+ deviceId: String((_j = source.deviceId) !== null && _j !== void 0 ? _j : ''),
46
+ isEmulator: Boolean(source.isEmulator),
47
+ deviceLocale: String((_k = source.deviceLocale) !== null && _k !== void 0 ? _k : ''),
48
+ timezone: String((_l = source.timezone) !== null && _l !== void 0 ? _l : ''),
49
+ apiLevel: Number((_m = source.apiLevel) !== null && _m !== void 0 ? _m : 0),
50
+ installerSource: String((_o = source.installerSource) !== null && _o !== void 0 ? _o : ''),
51
+ firstInstallTime: Number((_p = source.firstInstallTime) !== null && _p !== void 0 ? _p : 0),
52
+ lastUpdateTime: Number((_q = source.lastUpdateTime) !== null && _q !== void 0 ? _q : 0),
53
+ };
54
+ }
55
+ let cached = null;
56
+ /**
57
+ * Returns every value at once. Synchronous — no Promise, no bridge round-trip.
58
+ * The result is computed once and cached.
59
+ */
60
+ function getInfo() {
61
+ if (cached === null) {
62
+ cached = readConstants();
63
+ }
64
+ return cached;
65
+ }
66
+ // ---- App ----
67
+ /** App marketing version / version name, e.g. "1.2.3". */
68
+ const getVersion = () => getInfo().appVersion;
69
+ exports.getVersion = getVersion;
70
+ /** App build number / version code, e.g. "42". */
71
+ const getBuildNumber = () => getInfo().buildNumber;
72
+ exports.getBuildNumber = getBuildNumber;
73
+ /** Bundle / application id, e.g. "com.acme.app". On Android this is the
74
+ * runtime applicationId, including any build-variant suffix (".debug" etc). */
75
+ const getBundleId = () => getInfo().bundleId;
76
+ exports.getBundleId = getBundleId;
77
+ /** Bundle id with a known build-variant suffix removed, e.g.
78
+ * "com.acme.app.debug" -> "com.acme.app". Only strips recognised suffixes
79
+ * (see KNOWN_BUNDLE_SUFFIXES); otherwise returns the id unchanged. */
80
+ const getBaseBundleId = () => {
81
+ const id = getInfo().bundleId;
82
+ const lastDot = id.lastIndexOf('.');
83
+ if (lastDot === -1)
84
+ return id;
85
+ const last = id.slice(lastDot + 1).toLowerCase();
86
+ return KNOWN_BUNDLE_SUFFIXES.includes(last) ? id.slice(0, lastDot) : id;
87
+ };
88
+ exports.getBaseBundleId = getBaseBundleId;
89
+ /** App display name. */
90
+ const getAppName = () => getInfo().appName;
91
+ exports.getAppName = getAppName;
92
+ /** Convenience: "1.2.3 (42)". */
93
+ const getReadableVersion = () => `${getInfo().appVersion} (${getInfo().buildNumber})`;
94
+ exports.getReadableVersion = getReadableVersion;
95
+ // ---- Device / OS ----
96
+ /** OS name: "iOS" or "Android". */
97
+ const getOsName = () => getInfo().osName;
98
+ exports.getOsName = getOsName;
99
+ /** OS version, e.g. "17.4". */
100
+ const getOsVersion = () => getInfo().osVersion;
101
+ exports.getOsVersion = getOsVersion;
102
+ /** Platform type: "ios" or "android". */
103
+ const getDeviceType = () => getInfo().deviceType;
104
+ exports.getDeviceType = getDeviceType;
105
+ /** Hardware model identifier, e.g. "iPhone15,2" or "Pixel 8". */
106
+ const getDeviceModel = () => getInfo().deviceModel;
107
+ exports.getDeviceModel = getDeviceModel;
108
+ /** Device brand / manufacturer, e.g. "Apple" or "Google". */
109
+ const getDeviceBrand = () => getInfo().deviceBrand;
110
+ exports.getDeviceBrand = getDeviceBrand;
111
+ /** True on iPad / Android tablets. */
112
+ const isTablet = () => getInfo().isTablet;
113
+ exports.isTablet = isTablet;
114
+ /** Stable, persistent unique device id. See `AppDeviceInfoConstants.deviceId`. */
115
+ const getUniqueId = () => getInfo().deviceId;
116
+ exports.getUniqueId = getUniqueId;
117
+ // ---- Environment ----
118
+ /** True on a simulator (iOS) / emulator (Android). */
119
+ const isEmulator = () => getInfo().isEmulator;
120
+ exports.isEmulator = isEmulator;
121
+ /** Active locale at launch, BCP-47, e.g. "en-US". */
122
+ const getDeviceLocale = () => getInfo().deviceLocale;
123
+ exports.getDeviceLocale = getDeviceLocale;
124
+ /** IANA time zone id, e.g. "America/New_York". */
125
+ const getTimezone = () => getInfo().timezone;
126
+ exports.getTimezone = getTimezone;
127
+ /** Android API level (e.g. 34); 0 on iOS. */
128
+ const getApiLevel = () => getInfo().apiLevel;
129
+ exports.getApiLevel = getApiLevel;
130
+ /** Where the app was installed from. See `AppDeviceInfoConstants.installerSource`. */
131
+ const getInstallerSource = () => getInfo().installerSource;
132
+ exports.getInstallerSource = getInstallerSource;
133
+ /** First-install time, epoch ms (0 if unknown). */
134
+ const getFirstInstallTime = () => getInfo().firstInstallTime;
135
+ exports.getFirstInstallTime = getFirstInstallTime;
136
+ /** Last-update time, epoch ms (0 if unknown). */
137
+ const getLastUpdateTime = () => getInfo().lastUpdateTime;
138
+ exports.getLastUpdateTime = getLastUpdateTime;
139
+ const AppDeviceInfo = {
140
+ getInfo,
141
+ getVersion: exports.getVersion,
142
+ getBuildNumber: exports.getBuildNumber,
143
+ getBundleId: exports.getBundleId,
144
+ getBaseBundleId: exports.getBaseBundleId,
145
+ getAppName: exports.getAppName,
146
+ getReadableVersion: exports.getReadableVersion,
147
+ getOsName: exports.getOsName,
148
+ getOsVersion: exports.getOsVersion,
149
+ getDeviceType: exports.getDeviceType,
150
+ getDeviceModel: exports.getDeviceModel,
151
+ getDeviceBrand: exports.getDeviceBrand,
152
+ isTablet: exports.isTablet,
153
+ getUniqueId: exports.getUniqueId,
154
+ isEmulator: exports.isEmulator,
155
+ getDeviceLocale: exports.getDeviceLocale,
156
+ getTimezone: exports.getTimezone,
157
+ getApiLevel: exports.getApiLevel,
158
+ getInstallerSource: exports.getInstallerSource,
159
+ getFirstInstallTime: exports.getFirstInstallTime,
160
+ getLastUpdateTime: exports.getLastUpdateTime,
161
+ };
162
+ exports.default = AppDeviceInfo;
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "react-native-app-device-info",
3
+ "version": "1.0.0",
4
+ "description": "Minimal, fast, synchronous app & device info for React Native — app version, build number, OS version, device name/type and a persistent unique device id. iOS + Android, Old & New Architecture.",
5
+ "main": "lib/index.js",
6
+ "module": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "react-native": "src/index.ts",
9
+ "source": "src/index.ts",
10
+ "files": [
11
+ "src",
12
+ "lib",
13
+ "android",
14
+ "ios",
15
+ "react-native.config.js",
16
+ "react-native-app-device-info.podspec",
17
+ "!android/build",
18
+ "!**/build",
19
+ "!**/__tests__",
20
+ "!**/.*"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "prepare": "npm run build",
25
+ "typecheck": "tsc --noEmit -p tsconfig.json",
26
+ "test": "node ./scripts/test.js"
27
+ },
28
+ "keywords": [
29
+ "react-native",
30
+ "ios",
31
+ "android",
32
+ "app-version",
33
+ "build-number",
34
+ "version-name",
35
+ "device-info",
36
+ "device-id",
37
+ "unique-id",
38
+ "os-version",
39
+ "device-name",
40
+ "device-type",
41
+ "bundle-id",
42
+ "expo"
43
+ ],
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/getsettalk/react-native-app-device-info.git"
47
+ },
48
+ "author": "Sujeet Kumar",
49
+ "license": "MIT",
50
+ "bugs": {
51
+ "url": "https://github.com/getsettalk/react-native-app-device-info/issues"
52
+ },
53
+ "homepage": "https://github.com/getsettalk/react-native-app-device-info#readme",
54
+ "peerDependencies": {
55
+ "react": "*",
56
+ "react-native": "*"
57
+ },
58
+ "devDependencies": {
59
+ "react": "18.2.0",
60
+ "react-native": "0.74.5",
61
+ "typescript": "^5.4.0"
62
+ }
63
+ }
@@ -0,0 +1,21 @@
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 = "react-native-app-device-info"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.author = package["author"]
12
+ s.platforms = { :ios => "12.0" }
13
+ s.source = { :git => package["repository"]["url"], :tag => "#{s.version}" }
14
+
15
+ s.source_files = "ios/**/*.{h,m,mm}"
16
+ s.requires_arc = true
17
+
18
+ # Works on both the legacy bridge and the New Architecture interop layer.
19
+ install_modules_dependencies(s) if respond_to?(:install_modules_dependencies)
20
+ s.dependency "React-Core" unless respond_to?(:install_modules_dependencies)
21
+ end
@@ -0,0 +1,16 @@
1
+ // Explicit autolinking config so the native module is picked up reliably
2
+ // on both platforms across React Native versions.
3
+ module.exports = {
4
+ dependency: {
5
+ platforms: {
6
+ android: {
7
+ sourceDir: 'android',
8
+ packageImportPath: 'import com.appdeviceinfo.AppDeviceInfoPackage;',
9
+ packageInstance: 'new AppDeviceInfoPackage()',
10
+ },
11
+ ios: {
12
+ podspecPath: __dirname + '/react-native-app-device-info.podspec',
13
+ },
14
+ },
15
+ },
16
+ };
package/src/index.ts ADDED
@@ -0,0 +1,213 @@
1
+ import { NativeModules, Platform } from 'react-native';
2
+
3
+ const LINKING_ERROR =
4
+ `The package 'react-native-app-device-info' doesn't seem to be linked. Make sure:\n\n` +
5
+ Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
6
+ '- You rebuilt the app after installing the package\n' +
7
+ '- You are not using Expo Go (use a development build instead)\n';
8
+
9
+ /**
10
+ * Shape of the constants exported synchronously by the native module.
11
+ * These are read once, at module load time — there is no async bridge call.
12
+ */
13
+ export interface AppDeviceInfoConstants {
14
+ /** App marketing version, a.k.a. version name. iOS: CFBundleShortVersionString, Android: versionName. e.g. "1.2.3" */
15
+ appVersion: string;
16
+ /** App build number / version code. iOS: CFBundleVersion, Android: versionCode. e.g. "42" */
17
+ buildNumber: string;
18
+ /** Application/bundle identifier. e.g. "com.acme.app" */
19
+ bundleId: string;
20
+ /** Human-readable app display name. */
21
+ appName: string;
22
+
23
+ /** OS name. "iOS" or "Android". */
24
+ osName: string;
25
+ /** OS version. e.g. "17.4" or "14". */
26
+ osVersion: string;
27
+ /** Platform type, handy for branching. "ios" or "android". */
28
+ deviceType: 'ios' | 'android';
29
+
30
+ /** Hardware model identifier. iOS: e.g. "iPhone15,2". Android: Build.MODEL e.g. "Pixel 8". */
31
+ deviceModel: string;
32
+ /** Device brand / manufacturer. iOS: "Apple". Android: Build.MANUFACTURER e.g. "Google". */
33
+ deviceBrand: string;
34
+ /** Whether the device is a tablet / iPad. */
35
+ isTablet: boolean;
36
+
37
+ /**
38
+ * A stable unique id for this device, persistent across app launches.
39
+ * iOS: identifierForVendor (stable while at least one app from the same
40
+ * vendor stays installed; may change if all are uninstalled).
41
+ * Android: Settings.Secure.ANDROID_ID (stable until factory reset; scoped
42
+ * to the app signing key on Android 8+).
43
+ */
44
+ deviceId: string;
45
+
46
+ /** True when running on a simulator (iOS) or emulator (Android). */
47
+ isEmulator: boolean;
48
+ /** Active locale at launch in BCP-47 form, e.g. "en-US". */
49
+ deviceLocale: string;
50
+ /** IANA time zone id, e.g. "America/New_York". */
51
+ timezone: string;
52
+ /** Android API level (Build.VERSION.SDK_INT), e.g. 34. Always 0 on iOS. */
53
+ apiLevel: number;
54
+
55
+ /**
56
+ * Where the app was installed from.
57
+ * Android: installing package, e.g. "com.android.vending" (Play Store) or
58
+ * "" when sideloaded.
59
+ * iOS: "AppStore", "TestFlight", or "Other" (dev/sideloaded builds).
60
+ */
61
+ installerSource: string;
62
+ /** First-install time as epoch milliseconds. 0 if unknown (iOS is a heuristic). */
63
+ firstInstallTime: number;
64
+ /** Last-update time as epoch milliseconds. 0 if unknown (iOS is a heuristic). */
65
+ lastUpdateTime: number;
66
+ }
67
+
68
+ /** Build-variant suffixes stripped by `getBaseBundleId()`. */
69
+ const KNOWN_BUNDLE_SUFFIXES = [
70
+ 'debug',
71
+ 'dev',
72
+ 'development',
73
+ 'staging',
74
+ 'stg',
75
+ 'qa',
76
+ 'test',
77
+ 'alpha',
78
+ 'beta',
79
+ 'release',
80
+ 'internal',
81
+ ];
82
+
83
+ type NativeShape = AppDeviceInfoConstants & {
84
+ getConstants?: () => AppDeviceInfoConstants;
85
+ };
86
+
87
+ const Native: NativeShape | null = NativeModules.AppDeviceInfo ?? null;
88
+
89
+ function readConstants(): AppDeviceInfoConstants {
90
+ if (!Native) {
91
+ throw new Error(LINKING_ERROR);
92
+ }
93
+ // New Architecture / TurboModule interop exposes a getConstants() method;
94
+ // legacy bridge exposes the values directly on the module object. Support both.
95
+ const source =
96
+ typeof Native.getConstants === 'function' ? Native.getConstants() : Native;
97
+
98
+ return {
99
+ appVersion: String(source.appVersion ?? ''),
100
+ buildNumber: String(source.buildNumber ?? ''),
101
+ bundleId: String(source.bundleId ?? ''),
102
+ appName: String(source.appName ?? ''),
103
+ osName: String(source.osName ?? ''),
104
+ osVersion: String(source.osVersion ?? ''),
105
+ deviceType: (source.deviceType === 'android' ? 'android' : 'ios'),
106
+ deviceModel: String(source.deviceModel ?? ''),
107
+ deviceBrand: String(source.deviceBrand ?? ''),
108
+ isTablet: Boolean(source.isTablet),
109
+ deviceId: String(source.deviceId ?? ''),
110
+ isEmulator: Boolean(source.isEmulator),
111
+ deviceLocale: String(source.deviceLocale ?? ''),
112
+ timezone: String(source.timezone ?? ''),
113
+ apiLevel: Number(source.apiLevel ?? 0),
114
+ installerSource: String(source.installerSource ?? ''),
115
+ firstInstallTime: Number(source.firstInstallTime ?? 0),
116
+ lastUpdateTime: Number(source.lastUpdateTime ?? 0),
117
+ };
118
+ }
119
+
120
+ let cached: AppDeviceInfoConstants | null = null;
121
+
122
+ /**
123
+ * Returns every value at once. Synchronous — no Promise, no bridge round-trip.
124
+ * The result is computed once and cached.
125
+ */
126
+ export function getInfo(): AppDeviceInfoConstants {
127
+ if (cached === null) {
128
+ cached = readConstants();
129
+ }
130
+ return cached;
131
+ }
132
+
133
+ // ---- App ----
134
+ /** App marketing version / version name, e.g. "1.2.3". */
135
+ export const getVersion = (): string => getInfo().appVersion;
136
+ /** App build number / version code, e.g. "42". */
137
+ export const getBuildNumber = (): string => getInfo().buildNumber;
138
+ /** Bundle / application id, e.g. "com.acme.app". On Android this is the
139
+ * runtime applicationId, including any build-variant suffix (".debug" etc). */
140
+ export const getBundleId = (): string => getInfo().bundleId;
141
+ /** Bundle id with a known build-variant suffix removed, e.g.
142
+ * "com.acme.app.debug" -> "com.acme.app". Only strips recognised suffixes
143
+ * (see KNOWN_BUNDLE_SUFFIXES); otherwise returns the id unchanged. */
144
+ export const getBaseBundleId = (): string => {
145
+ const id = getInfo().bundleId;
146
+ const lastDot = id.lastIndexOf('.');
147
+ if (lastDot === -1) return id;
148
+ const last = id.slice(lastDot + 1).toLowerCase();
149
+ return KNOWN_BUNDLE_SUFFIXES.includes(last) ? id.slice(0, lastDot) : id;
150
+ };
151
+ /** App display name. */
152
+ export const getAppName = (): string => getInfo().appName;
153
+ /** Convenience: "1.2.3 (42)". */
154
+ export const getReadableVersion = (): string =>
155
+ `${getInfo().appVersion} (${getInfo().buildNumber})`;
156
+
157
+ // ---- Device / OS ----
158
+ /** OS name: "iOS" or "Android". */
159
+ export const getOsName = (): string => getInfo().osName;
160
+ /** OS version, e.g. "17.4". */
161
+ export const getOsVersion = (): string => getInfo().osVersion;
162
+ /** Platform type: "ios" or "android". */
163
+ export const getDeviceType = (): 'ios' | 'android' => getInfo().deviceType;
164
+ /** Hardware model identifier, e.g. "iPhone15,2" or "Pixel 8". */
165
+ export const getDeviceModel = (): string => getInfo().deviceModel;
166
+ /** Device brand / manufacturer, e.g. "Apple" or "Google". */
167
+ export const getDeviceBrand = (): string => getInfo().deviceBrand;
168
+ /** True on iPad / Android tablets. */
169
+ export const isTablet = (): boolean => getInfo().isTablet;
170
+ /** Stable, persistent unique device id. See `AppDeviceInfoConstants.deviceId`. */
171
+ export const getUniqueId = (): string => getInfo().deviceId;
172
+
173
+ // ---- Environment ----
174
+ /** True on a simulator (iOS) / emulator (Android). */
175
+ export const isEmulator = (): boolean => getInfo().isEmulator;
176
+ /** Active locale at launch, BCP-47, e.g. "en-US". */
177
+ export const getDeviceLocale = (): string => getInfo().deviceLocale;
178
+ /** IANA time zone id, e.g. "America/New_York". */
179
+ export const getTimezone = (): string => getInfo().timezone;
180
+ /** Android API level (e.g. 34); 0 on iOS. */
181
+ export const getApiLevel = (): number => getInfo().apiLevel;
182
+ /** Where the app was installed from. See `AppDeviceInfoConstants.installerSource`. */
183
+ export const getInstallerSource = (): string => getInfo().installerSource;
184
+ /** First-install time, epoch ms (0 if unknown). */
185
+ export const getFirstInstallTime = (): number => getInfo().firstInstallTime;
186
+ /** Last-update time, epoch ms (0 if unknown). */
187
+ export const getLastUpdateTime = (): number => getInfo().lastUpdateTime;
188
+
189
+ const AppDeviceInfo = {
190
+ getInfo,
191
+ getVersion,
192
+ getBuildNumber,
193
+ getBundleId,
194
+ getBaseBundleId,
195
+ getAppName,
196
+ getReadableVersion,
197
+ getOsName,
198
+ getOsVersion,
199
+ getDeviceType,
200
+ getDeviceModel,
201
+ getDeviceBrand,
202
+ isTablet,
203
+ getUniqueId,
204
+ isEmulator,
205
+ getDeviceLocale,
206
+ getTimezone,
207
+ getApiLevel,
208
+ getInstallerSource,
209
+ getFirstInstallTime,
210
+ getLastUpdateTime,
211
+ };
212
+
213
+ export default AppDeviceInfo;