# Tracking transparency

> expo-tracking-transparency wrapped for every SymbioteNative adapter — the iOS App Tracking Transparency prompt and advertising ID.

`@symbiote-native/tracking-transparency` wraps
[`expo-tracking-transparency`](https://github.com/expo/expo/tree/main/packages/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](/docs/packages/location/)'s own permission surface,
so switching between the two packages needs no relearning; `getAdvertisingId`/`isAvailable` are
plain stateless free functions, same as [device](/docs/packages/device/).

| OS platform | Support |
| ----------- | ------- |
| iOS         | ✅ live |
| Android     | ✅ live |

| Framework adapter | Support |
| ----------------- | ------- |
| React             | ✅ live |
| Vue               | ✅ live |
| Angular           | ✅ live |
| Svelte            | ✅ live |
| Solid             | ✅ live |

<Aside type="note" title="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. Only iOS drives the real ATT system prompt.
</Aside>

## Installation

```sh
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`](https://github.com/OneEyed1366/symbiote-native/tree/master/packages/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).

<Aside type="danger" title="Native setup is required before first use">
  `expo-tracking-transparency`'s native code is discovered by
  `expo-modules-autolinking`, a different mechanism from the
  `react-native.config.cjs`/podspec autolinking every other SymbioteNative
  wrapper uses — and it isn't the standard Expo setup flow either, since this
  project never installs the `expo` meta-package. Follow [How to: wire up an
  Expo native module](/docs/howtos/expo-native-module-setup/) once per app
  first; it covers this package and every other `expo-modules-core` package with
  zero further native changes. Additionally, iOS needs an
  `NSUserTrackingUsageDescription` string in `Info.plist` — required for the ATT
  prompt to show at all. `symbiote-expo-link` inserts a generic default for you
  when that key is absent; write your own wording into `Info.plist` and it
  stands, since the linker never rewrites a key that already exists. The Android
  native module also pulls in
  `com.google.android.gms:play-services-ads-identifier:18.0.1` transitively,
  automatically, with no extra wiring needed.
</Aside>

## Usage

### Permission

<Tabs syncKey="framework">
  <TabItem label="React">
    ```tsx
    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>
      );
    }
    ```

  </TabItem>
  <TabItem label="Vue">
    ```vue
    <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>
    ```

  </TabItem>
  <TabItem label="Angular">
    ```ts
    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;
    }
    ```

  </TabItem>
  <TabItem label="Svelte">
    ```svelte
    <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>
    ```

  </TabItem>
  <TabItem label="Solid">
    ```tsx
    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>
      );
    }
    ```

  </TabItem>
</Tabs>

`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

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

```ts
import { getAdvertisingId } from '@symbiote-native/tracking-transparency';

const advertisingId = getAdvertisingId(); // null on the iOS Simulator, or before/without consent
```

## API

### 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?)`

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`

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)                  |

## Notes

- **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.

## Common questions

- **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](https://docs.expo.dev/versions/latest/sdk/tracking-transparency/),
[expo/expo#13059](https://github.com/expo/expo/issues/13059),
[Implement App Tracking Transparency with Expo](https://yosukep.medium.com/implement-app-tracking-transparency-with-expo-app-58cf77cb168d),
[Why the ATT prompt never shows](https://rorklab.net/en/articles/rork-dev/rork-att-tracking-permission-not-showing-fix).

## 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: 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](/docs/packages/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](/docs/howtos/expo-native-module-setup/)).
