Skip to content

Tracking transparency

@symbiote-native/tracking-transparency wraps expo-tracking-transparency — the iOS App Tracking Transparency prompt, permission get/request, and the advertising-ID getter — so every SymbioteNative adapter can reach it, not just React. Its permission surface (getTrackingPermissionsAsync/requestTrackingPermissionsAsync) shares the same useTrackingPermissions() hook/composable/service shape as location’s own permission surface, so switching between the two packages needs no relearning; getAdvertisingId/isAvailable are plain stateless free functions, same as device.

OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Solid ✅ live
Terminal window
npm install @symbiote-native/tracking-transparency

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --tracking-transparency (or add --tracking-transparency in an existing app) installs and wires this for you — see @symbiote-native/cli.

expo-tracking-transparency and expo-modules-core come along as regular dependencies, pinned to exact versions — never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

import { useTrackingPermissions } from '@symbiote-native/tracking-transparency/react';
export default function TrackingScreen() {
const [status, request] = useTrackingPermissions();
return (
<view>
<text>{status?.status ?? 'checking...'}</text>
<pressable onPress={() => request()}>
<text>Request tracking permission</text>
</pressable>
</view>
);
}

get() re-runs the check without prompting the user. On Angular the service keeps an error signal, so a failed first fetch reads apart from one still in flight; the hooks have no error slot.

The stateless functions are already framework-agnostic — import them straight from the package root, on any adapter:

import { getAdvertisingId } from '@symbiote-native/tracking-transparency';
const advertisingId = getAdvertisingId(); // null on the iOS Simulator, or before/without consent
Signature Description
getAdvertisingId(): string | null Gets the advertising ID (Android AAID / iOS IDFA). Returns null on the iOS Simulator, when tracking hasn’t been authorized via requestTrackingPermissionsAsync, or when the user declined
getTrackingPermissionsAsync(): Promise<PermissionResponse> Checks whether the user has authorized the app to access tracking-related data. Always resolves granted on Android and web
requestTrackingPermissionsAsync(): Promise<PermissionResponse> Requests the user to authorize or deny access to app-related data usable for tracking, showing the real ATT prompt on iOS. Always resolves granted on Android and web
isAvailable(): boolean Whether the tracking-transparency native module resolved at all

Angular exposes TrackingPermissionsService instead: connect() returns a Signal<PermissionResponse | null>, plus error, request() and get() on the service.

Adapter Returns
React [status, request, get] tuple, status a plain PermissionResponse | null
Vue [status, request, get] tuple, status a Ref<PermissionResponse | null>
Solid [status, request, get] tuple, status an Accessor<PermissionResponse | null>
Svelte { status, requestPermission, getPermission }, status a getter, never destructure it
Option Default Description
get true Reads the current status on mount
request false Asks the user on mount instead of only reading the status

request asks the user for the permission and updates status; get re-reads it without prompting. Both reject to their caller when the native call fails. Angular’s auto-fetch is latched to at most one run per service instance, and a failure lands in error: call get() to retry.

Re-exported verbatim from expo-modules-core, never the expo meta-package:

Field Type Description
status PermissionStatus The current permission status — 'undetermined', 'denied', or 'granted'
granted boolean Whether the permission is granted — a convenience shortcut over checking status === 'granted'
canAskAgain boolean Whether the user can be asked again for this permission, or the OS has permanently blocked it
expires PermissionExpiration When the permission expires ('never' on every platform this package targets)
  • Android and web always report granted. There is no tracking-consent concept on either platform — getTrackingPermissionsAsync/requestTrackingPermissionsAsync short-circuit to a fixed granted response without ever calling the native module, matching upstream exactly.
  • getAdvertisingId returns null on the iOS Simulator, regardless of any settings — there is no real IDFA to read there. This is expected Apple Simulator behavior, not a bug in this wrapper.
  • The app crashes when I request permission. NSUserTrackingUsageDescription is missing from Info.plist. Add it before calling requestTrackingPermissionsAsync.
  • The dialog never appears. The call ran too early. Request only while the app state is active, and guard against duplicate requests.
  • App Store rejection under Guideline 5.1.2. Generic text such as “Required for advertising” is rejected. State a concrete user benefit.
  • Android. There is no prompt; the permission reads as granted.

Sources: Expo docs: TrackingTransparency, expo/expo#13059, Implement App Tracking Transparency with Expo, Why the ATT prompt never shows.

@symbiote-native/tracking-transparency ships zero React/Vue/Angular/Svelte/Solid logic in expo-tracking-transparency itself — its functions are hand-ported, verbatim, into this package’s own core/, resolving the native module through expo-modules-core’s requireNativeModule rather than the expo meta-package this project never installs:

packages/tracking-transparency/src/
├── core/ getAdvertisingId, get/requestTrackingPermissionsAsync, isAvailable;
│ native-module.ts resolves ExpoTrackingTransparency through
│ expo-modules-core's requireNativeModule.
|-- react/ @symbiote-native/tracking-transparency/react: useTrackingPermissions
|-- vue/ @symbiote-native/tracking-transparency/vue: useTrackingPermissions
|-- angular/ @symbiote-native/tracking-transparency/angular: TrackingPermissionsService
|-- svelte/ @symbiote-native/tracking-transparency/svelte: useTrackingPermissions
`-- solid/ @symbiote-native/tracking-transparency/solid: useTrackingPermissions

Only the permission surface gets a lifecycle hook, there is per-instance state to seed and refresh; getAdvertisingId/isAvailable are stateless, re-exported verbatim by every adapter. Upstream’s useTrackingPermissions is ported to every adapter on the shared createPermissionHook factory (PermissionsServiceBase on Angular), the same shape as location’s permission hooks. The native code itself is never vendored or copied — expo-modules-autolinking resolves it straight out of node_modules (see the native setup guide).