Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/windows-app-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ jobs:
run-windows-tests:
name: Build & run tests
runs-on: windows-2019
env:
# Node 17+ uses OpenSSL 3.0; Metro/RN bundler needs legacy provider for hashing
NODE_OPTIONS: --openssl-legacy-provider

steps:
- uses: actions/checkout@v5
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [15.0.2](https://github.com/react-native-device-info/react-native-device-info/compare/v15.0.1...v15.0.2) (2026-02-21)

### Bug Fixes

* **android:** add optional gradle dependency for appSetId to work ([#1750](https://github.com/react-native-device-info/react-native-device-info/issues/1750)) ([8401770](https://github.com/react-native-device-info/react-native-device-info/commit/840177075429a2447e1b4d3101504eb3e82dcefc))

## [15.0.1](https://github.com/react-native-device-info/react-native-device-info/compare/v15.0.0...v15.0.1) (2025-11-13)

### Bug Fixes
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ This module defaults to AndroidX you should configure your library versions simi
googlePlayServicesIidVersion = "17.0.0" // default: "17.0.0" - AndroidX
//Option 3 (legacy GooglePlay dependency before AndroidX):
googlePlayServicesIidVersion = "16.0.1"

// getAppSetId() - optional: set to include play-services-appset (e.g. "16.1.0")
// playServicesAppSetVersion = "16.1.0"

//include as needed:
compileSdkVersion = "28" // default: 28 (28 is required for AndroidX)
Expand Down Expand Up @@ -245,7 +246,7 @@ DeviceInfo.getAndroidId().then((androidId) => {

### getAppSetId()

Gets the AppSetId for Android devices. AppSetId is part of Android's Privacy Sandbox and provides a privacy-preserving identifier for advertising and analytics purposes. This API is only available on Android 14 (API level 34) and above.
Gets the App Set ID for Android devices via Google Play services. App Set ID provides a privacy-preserving identifier for correlating usage across apps from the same developer (e.g. analytics, fraud prevention). **Optional**: set `playServicesAppSetVersion` in your app's `android/build.gradle` ext to include the dependency; otherwise returns `{ id: 'unknown', scope: -1 }`.

The returned object contains:
- `id`: The AppSetId string value (returns "unknown" if not available)
Expand Down Expand Up @@ -273,7 +274,7 @@ if (appSetIdInfo.id === 'unknown') {
}
```

**Note**: AppSetId requires Android 14 (API level 34) or higher. On older Android versions or when the service is unavailable, the function will return `{ id: 'unknown', scope: -1 }`.
**Note**: To use `getAppSetId()` on Android you must add the optional dependency by setting `playServicesAppSetVersion` in your app's `android/build.gradle` ext block (e.g. `playServicesAppSetVersion = "16.1.0"`). If the dependency is not included or the service is unavailable, the function returns `{ id: 'unknown', scope: -1 }`.

---

Expand Down
5 changes: 5 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ dependencies {
def firebaseBomVersion = safeExtGet("firebaseBomVersion", null)
def firebaseIidVersion = safeExtGet('firebaseIidVersion', null)
def googlePlayServicesIidVersion = safeExtGet('googlePlayServicesIidVersion', null)
def playServicesAppSetVersion = safeExtGet('playServicesAppSetVersion', null)

if (firebaseBomVersion) {
implementation platform("com.google.firebase:firebase-bom:${firebaseBomVersion}")
Expand All @@ -69,6 +70,10 @@ dependencies {
} else if(googlePlayServicesIidVersion){
implementation "com.google.android.gms:play-services-iid:$googlePlayServicesIidVersion"
}
// Needed for getAppSetId() to work - optional: set playServicesAppSetVersion in app's ext to include
if (playServicesAppSetVersion) {
implementation "com.google.android.gms:play-services-appset:$playServicesAppSetVersion"
}

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testImplementation "org.mockito:mockito-core:3.6.28"
Expand Down
87 changes: 53 additions & 34 deletions android/src/main/java/com/learnium/RNDeviceInfo/RNDeviceModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import android.Manifest;
import android.annotation.SuppressLint;
import android.adservices.appsetid.AppSetId;
import android.adservices.appsetid.AppSetIdManager;
import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.Context;
Expand All @@ -21,7 +19,6 @@
import android.net.wifi.WifiInfo;
import android.os.Build;
import android.os.Environment;
import android.os.OutcomeReceiver;
import android.os.PowerManager;
import android.os.StatFs;
import android.os.BatteryManager;
Expand Down Expand Up @@ -66,6 +63,9 @@
import java.math.BigInteger;
import java.util.Locale;
import java.util.Map;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import javax.annotation.Nonnull;

Expand Down Expand Up @@ -1128,43 +1128,62 @@ private boolean hasKeyboard(String name) {

@ReactMethod
public void getAppSetId(Promise promise) {
System.err.println("RNDI: getAppSetId starting");
if (Build.VERSION.SDK_INT >= 34) { // Android 14 (API level 34)
try {
AppSetIdManager appSetIdManager = AppSetIdManager.get(getReactApplicationContext());
appSetIdManager.getAppSetId(
getReactApplicationContext().getMainExecutor(),
new OutcomeReceiver<AppSetId, Exception>() {
public void onResult(AppSetId appSetId) {
System.err.println("RNDI: AppSetId success.");
try {
// Optionally load App Set classes via reflection (only when play-services-appset is included)
Class<?> appSetClass = Class.forName("com.google.android.gms.appset.AppSet");
ClassLoader loader = appSetClass.getClassLoader();
Method getClientMethod = appSetClass.getMethod("getClient", Context.class);
Object client = getClientMethod.invoke(null, getReactApplicationContext());
Method getAppSetIdInfoMethod = client.getClass().getMethod("getAppSetIdInfo");
Object task = getAppSetIdInfoMethod.invoke(client);

Class<?> onSuccessListenerClass =
Class.forName("com.google.android.gms.tasks.OnSuccessListener", true, loader);
InvocationHandler successHandler =
(proxy, method, args) -> {
if ("onSuccess".equals(method.getName()) && args != null && args.length == 1) {
Object appSetIdInfo = args[0];
String id = (String) appSetIdInfo.getClass().getMethod("getId").invoke(appSetIdInfo);
Object scopeObj = appSetIdInfo.getClass().getMethod("getScope").invoke(appSetIdInfo);
int scope = scopeObj instanceof Number ? ((Number) scopeObj).intValue() : -1;
WritableMap result = Arguments.createMap();
result.putString("id", appSetId.getId());
result.putInt("scope", appSetId.getScope());
result.putString("id", id != null ? id : "unknown");
result.putInt("scope", scope);
promise.resolve(result);
};
public void onError(Exception exception) {
System.err.println("RNDI: AppSetId was a failure: " + exception);
exception.printStackTrace(System.err);
// Return default values instead of rejecting the promise
}
return null;
};
Object successListener =
Proxy.newProxyInstance(loader, new Class<?>[] {onSuccessListenerClass}, successHandler);

Class<?> onFailureListenerClass =
Class.forName("com.google.android.gms.tasks.OnFailureListener", true, loader);
InvocationHandler failureHandler =
(proxy, method, args) -> {
if ("onFailure".equals(method.getName()) && args != null && args.length == 1) {
Exception e = (Exception) args[0];
System.err.println("RNDI: AppSetId was a failure: " + e);
e.printStackTrace(System.err);
WritableMap result = Arguments.createMap();
result.putString("id", "unknown");
result.putInt("scope", -1);
promise.resolve(result);
};
}
);
} catch (Exception e) {
System.err.println("RNDI Exception: " + e);
e.printStackTrace(System.err);
// Return default values instead of rejecting the promise
WritableMap result = Arguments.createMap();
result.putString("id", "unknown");
result.putInt("scope", -1);
promise.resolve(result);
}
} else {
// Return default values for unsupported Android versions
System.err.println("RNDI: simply didn't try)");
}
return null;
};
Object failureListener =
Proxy.newProxyInstance(loader, new Class<?>[] {onFailureListenerClass}, failureHandler);

task.getClass()
.getMethod("addOnSuccessListener", onSuccessListenerClass)
.invoke(task, successListener);
task.getClass()
.getMethod("addOnFailureListener", onFailureListenerClass)
.invoke(task, failureListener);
} catch (Throwable t) {
// ClassNotFoundException when play-services-appset not included, or other errors
System.err.println("RNDI Exception: " + t);
t.printStackTrace(System.err);
WritableMap result = Arguments.createMap();
result.putString("id", "unknown");
result.putInt("scope", -1);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-device-info",
"version": "15.0.1",
"version": "15.0.2",
"description": "Get device information using react-native",
"react-native": "src/index.ts",
"types": "lib/typescript/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type AvailableCapacityType = 'total' | 'important' | 'opportunistic';

/**
* Google Play Services App Set ID payload describing identifier and scope.
* When the API is unavailable, id is "unknown" and scope is -1.
*/
export interface AppSetIdInfo {
id: string;
Expand Down
Loading