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 |
Installation
Section titled “Installation”npm install @symbiote-native/tracking-transparencyScaffolding 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).
Permission
Section titled “Permission”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> );}<script setup lang="ts">import { useTrackingPermissions } from '@symbiote-native/tracking-transparency/vue';
const [status, request] = useTrackingPermissions();</script>
<template> <view> <text>{{ status?.status ?? 'checking...' }}</text> <pressable @press="request()"> <text>Request tracking permission</text> </pressable> </view></template>import { Component, inject } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { TrackingPermissionsService } from '@symbiote-native/tracking-transparency/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` <view> <text>{{ error() ? 'check failed: ' + error()?.message : (status()?.status ?? 'checking…') }}</text> <pressable (press)="error() ? permissions.get() : permissions.request()"> <text>{{ error() ? 'Retry check' : 'Request tracking permission' }}</text> </pressable> </view> `,})export class TrackingScreen { readonly permissions = inject(TrackingPermissionsService); readonly status = this.permissions.connect(); readonly error = this.permissions.error;}<script lang="ts"> import { useTrackingPermissions } from '@symbiote-native/tracking-transparency/svelte';
const permissions = useTrackingPermissions();</script>
<view> <text>{permissions.status?.status ?? 'checking...'}</text> <pressable onPress={() => permissions.requestPermission()}> <text>Request tracking permission</text> </pressable></view>import { useTrackingPermissions } from '@symbiote-native/tracking-transparency/solid';
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.
Advertising ID
Section titled “Advertising ID”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 consentFunctions
Section titled “Functions”| 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 |
useTrackingPermissions(options?)
Section titled “useTrackingPermissions(options?)”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.
PermissionResponse
Section titled “PermissionResponse”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/requestTrackingPermissionsAsyncshort-circuit to a fixed granted response without ever calling the native module, matching upstream exactly. getAdvertisingIdreturnsnullon 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.
Common questions
Section titled “Common questions”- The app crashes when I request permission.
NSUserTrackingUsageDescriptionis missing from Info.plist. Add it before callingrequestTrackingPermissionsAsync. - 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.
How the wrapper works
Section titled “How the wrapper works”@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: useTrackingPermissionsOnly 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).