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 +21 -0
- package/README.md +401 -0
- package/android/build.gradle +48 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/appdeviceinfo/AppDeviceInfoModule.kt +120 -0
- package/android/src/main/java/com/appdeviceinfo/AppDeviceInfoPackage.kt +16 -0
- package/ios/AppDeviceInfo.h +4 -0
- package/ios/AppDeviceInfo.m +108 -0
- package/lib/index.d.ts +125 -0
- package/lib/index.js +162 -0
- package/package.json +63 -0
- package/react-native-app-device-info.podspec +21 -0
- package/react-native.config.js +16 -0
- package/src/index.ts +213 -0
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,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;
|