> ## Documentation Index
> Fetch the complete documentation index at: https://docs.linkrunner.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter SDK

> Complete guide for integrating Linkrunner in Flutter apps

<div
  className="video-container"
  style={{
    borderRadius: "12px",
    overflow: "hidden",
    width: "100%",
    maxWidth: "100%",
}}
>
  <iframe
    style={{
        width: "100%",
        height: "min(410px, 50vh)",
        display: "block",
    }}
    src="https://www.youtube.com/embed/tX05ioHJ6bc"
    title="Flutter Installation Guide"
    frameBorder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowFullScreen
  />
</div>

## Installation

### Requirements

* Flutter 3.19.0 or higher
* Dart 3.3.0 or higher
* iOS 15.0+ / Android 5.0 (API level 21) and above

### Step 1: Add the Package

Run the following command to add the latest version of the Linkrunner package to your project:

```bash theme={null}
flutter pub add linkrunner
```

This command will automatically:

* Add the latest version of `linkrunner` to your `pubspec.yaml`
* Download and install the package and its dependencies

### Step 2: Platform Specific Setup

#### Android Configuration

1. Ensure your project's `minSdkVersion` is at least 21 in your `android/app/build.gradle` file:

```gradle theme={null}
android {
    defaultConfig {
        minSdkVersion 21
        // other config...
    }
}
```

2. Add the following permissions to your `AndroidManifest.xml` file:

```xml theme={null}
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```

> **Note:** The `AD_ID` permission (`<uses-permission android:name="com.google.android.gms.permission.AD_ID" />`) is already included in the SDK and is required for collecting device identifiers (GAID). If your app participates in [Designed for Families](https://support.google.com/googleplay/android-developer/topic/9877766?hl=en\&ref_topic=9858052), you should revoke AAID and disable AAID collection. See the [Disabling AAID Collection](#disabling-aaid-collection) section for more details.

#### Revoking the AD\_ID Permission

According to [Google's Policy](https://support.google.com/googleplay/android-developer/answer/11043825?hl=en), apps that target children must not transmit the Advertising ID.

To revoke the AD\_ID permission, use Flutter SDK version 3.5.0 and above. Children apps targeting Android 13 (API 33) and above must prevent the permission from getting merged into their app by adding a revoke declaration to their Manifest. Use the [`setDisableAaidCollection()`](#disabling-aaid-collection) and [`isAaidCollectionDisabled()`](#disabling-aaid-collection) functions to disable AAID collection programmatically:

**android/app/src/main/AndroidManifest.xml**

```xml theme={null}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Remove AD_ID permission that comes from the SDK -->
    <uses-permission
        android:name="com.google.android.gms.permission.AD_ID"
        tools:node="remove" />

    <!-- Your other permissions -->
</manifest>
```

<Note>
  Make sure to add `xmlns:tools="http://schemas.android.com/tools"` to your manifest tag to use the `tools:node="remove"` attribute. If you disable AAID collection, you should also remove the AD\_ID permission from your manifest to fully comply with Google Play's Family Policy requirements.
</Note>

For more information, see [Google Play Services documentation](https://developers.google.com/android/reference/com/google/android/gms/ads/identifier/AdvertisingIdClient.Info#public-string-getid).

#### Backup Configuration

For Android apps, the SDK provides backup rules to exclude Shared Preferences data from backup. This prevents the retention of the Linkrunner install ID during reinstallation, ensuring accurate detection of new installs and re-installs.

For detailed backup configuration instructions, please refer to the [Android SDK Backup Configuration](/sdk/android/installation#backup-configuration).

#### Encrypted SharedPreferences

<Note>
  **SDK Version Requirement:** Starting from `linkrunner` **v3.9.1**, the SDK automatically encrypts the credentials it stores in Android SharedPreferences (such as the install ID and other persisted SDK state). No additional configuration is required — upgrade to v3.9.1 or above to get this behavior by default.
</Note>

On Android, values written by the SDK are encrypted at rest, with a hardware-protected key generated on the device and stored in the [Android Keystore](https://developer.android.com/training/articles/keystore). The key never leaves the device and is not bundled with the SDK.

If you are upgrading from an earlier version, the SDK will transparently migrate any existing plaintext entries to the encrypted store on the next read after the upgrade — no code changes are needed on your side.

#### iOS Configuration

1. Update your iOS deployment target to iOS 15.0 or higher in your `ios/Podfile`:

```ruby theme={null}
platform :ios, '15.0'
```

2. Add the following to your `Info.plist` file for App Tracking Transparency:

```xml theme={null}
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads and improve your app experience.</string>
```

## Initialization (Required)

You'll need your [project token](https://dashboard.linkrunner.io/dashboard?m=documentation) to get started!

Note: The initialization method doesn't return any value. To get attribution data and deeplink information, use the `getAttributionData` method.

```dart theme={null}
import 'package:linkrunner/linkrunner.dart';

Future<void> initLinkrunner() async {
  try {
    // Initialize with your project token
    await LinkRunner().init(
      'YOUR_PROJECT_TOKEN',
      'YOUR_SECRET_KEY', // Optional: Required for SDK signing
      'YOUR_KEY_ID', // Optional: Required for SDK signing
      false, // Optional: Set to true to disable IDFA collection for iOS devices (defaults to false)
      true // Optional: Enable debug mode for development (defaults to false)
    );
    print('LinkRunner initialized');
  } catch (e) {
    print('Error initializing LinkRunner: $e');
  }
}

// Call this in your app's initialization
@override
void initState() {
  WidgetsFlutterBinding.ensureInitialized(); // Make sure this is added!
  super.initState();
  initLinkrunner();
}
```

## SDK Signing Parameters (Optional)

For enhanced security, the LinkRunner SDK requires the following signing parameters during initialization:

* **`secretKey`**: A unique secret key used for request signing and authentication
* **`keyId`**: A unique identifier for the key pair used in the signing process

You can find your project token, secret key, and key ID [here](https://dashboard.linkrunner.io/settings?s=sdk-signing).

### Platform-Specific SDK Signing

For applications requiring different signing keys per platform:

```dart theme={null}
import 'dart:io' show Platform;
import 'package:linkrunner/linkrunner.dart';

Future<void> initLinkrunnerWithSigning() async {
  try {
    // Initialize with your project token and SDK signing parameters
    await LinkRunner().init(
      'YOUR_PROJECT_TOKEN',
      Platform.isIOS ? 'YOUR_IOS_SECRET_KEY' : 'YOUR_ANDROID_SECRET_KEY', // Platform-specific secret key
      Platform.isIOS ? 'YOUR_IOS_KEY_ID' : 'YOUR_ANDROID_KEY_ID', // Platform-specific key ID
      true, // Optional: Enable debug mode for development (defaults to false)
    );
    print('LinkRunner initialized with SDK signing');
  } catch (e) {
    print('Error initializing LinkRunner: $e');
  }
}
```

## User Identification (Required)

Call the `signup` method as soon as the user is identified — whether through signup or login. This is the moment Linkrunner ties the install (and any future events) to a user identifier.

It is strongly recommended to use the integrated platform's identify function to set a persistent user\_id once it becomes available (typically after signup or login).

* [Mixpanel - ID Management & User Identification](https://docs.mixpanel.com/docs/tracking-methods/id-management/identifying-users-simplified)
* [PostHog - How User Identification Works](https://posthog.com/docs/product-analytics/identify#how-identify-works)
* [Amplitude - Identify Users Documentation](https://amplitude.com/docs/get-started/identify-users)

If the platform's identifier function is not called, you must provide a user identifier for Mixpanel, PostHog, and Amplitude integration.

* mixpanelDistinctId for Mixpanel
* amplitudeDeviceId for Amplitude
* posthogDistinctId for PostHog

```dart theme={null}
Future<void> onSignup() async {
  try {
    await LinkRunner().signup(
      userData: LRUserData(
        id: '123', // Required: User ID
        name: 'John Doe', // Optional
        phone: '9876543210', // Optional
        email: 'user@example.com', // Optional
        // These properties are used to track reinstalls
        userCreatedAt: '2024-01-01T00:00:00Z', // Optional
        isFirstTimeUser: true, // Optional
        mixpanelDistinctId: 'mixpanelDistinctId', // Optional - Mixpanel Distinct ID
        amplitudeDeviceId: 'amplitudeDeviceId', // Optional - Amplitude User ID
        posthogDistinctId: 'posthogDistinctId', // Optional - PostHog Distinct ID
      ),
      data: {}, // Optional: Any additional data
    );
    print('Signup successful');
  } catch (e) {
    print('Error during signup: $e');
  }
}
```

## Handle Deeplink

To enable [remarketing and reattribution](/features/remarketing), you need to capture deep links and pass them to the Linkrunner SDK. This allows Linkrunner to detect returning users who open the app via a deep link.

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:app_links/app_links.dart';
import 'package:linkrunner/linkrunner.dart';

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _appLinks = AppLinks();

  @override
  void initState() {
    super.initState();
    _initLinkRunner();
  }

  Future<void> _initLinkRunner() async {
    // Init SDK first
    await LinkRunner().init('your_project_token');

    // Cold start — app was launched by a deeplink
    final initialLink = await _appLinks.getInitialLink();
    if (initialLink != null) {
      LinkRunner().handleDeeplink(initialLink.toString());
    }

    // Warm start — app was in background, deeplink brought it to foreground
    _appLinks.uriLinkStream.listen((Uri uri) {
      LinkRunner().handleDeeplink(uri.toString());
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // your app content
    );
  }
}
```

## Getting Attribution Data

To get attribution data and deeplink information for the current installation, use the `getAttributionData` function:

```dart theme={null}
Future<void> getAttributionInfo() async {
  try {
    final attributionData = await LinkRunner().getAttributionData();
    print('Attribution data: $attributionData');
  } catch (e) {
    print('Error getting attribution data: $e');
  }
}
```

The `getAttributionData` function returns an `AttributionData` object with the following structure:

```dart theme={null}
class AttributionData {
  final String? deeplink;            // Optional: The deep link URL that led to app installation
  final CampaignData campaignData;   // Required: Campaign information
}

class CampaignData {
  final String id;                   // Required: Campaign ID
  final String name;                 // Required: Campaign name
  final String? adNetwork;           // Optional: "META" | "GOOGLE" | null
  final String? groupName;           // Optional: Campaign group name
  final String? assetGroupName;      // Optional: Asset group name
  final String? assetName;           // Optional: Asset name
  final String type;                 // Required: Campaign type ("ORGANIC" | "INORGANIC")
  final String installedAt;          // Required: Installation timestamp
  final String storeClickAt;         // Required: Store click timestamp
}
```

Example response:

```dart theme={null}
{
  "deeplink": "https://app.yourdomain.com/product/123",
  "campaign_data": {
    "id": "camp_123",
    "name": "Summer Sale 2024",
    "ad_network": "META",
    "group_name": "iOS Campaign",
    "asset_group_name": "Product Catalog",
    "asset_name": "Banner Ad 1",
    "type": "INORGANIC",
    "installed_at": "2024-03-20T10:30:00Z",
    "store_click_at": "2024-03-20T10:29:45Z"
  }
}
```

## Setting User Data

Call `setUserData` each time the app opens and the user is logged in:

<Note>
  **`setUserData` is optional and is not a replacement for `signup`.** Always call `signup` first as soon as the user is identified (signup or login). Use `setUserData` afterwards only when additional user details become available later — for example, when the user adds a phone number, email, or completes their profile after identification.
</Note>

```dart theme={null}
Future<void> setUserData() async {
  try {
    await LinkRunner().setUserData(
      userData: LRUserData(
        id: '123', // Required: User ID
        name: 'John Doe', // Optional
        phone: '9876543210', // Optional
        email: 'user@example.com', // Optional
        mixpanelDistinctId: 'mixpanelDistinctId', // Optional - Mixpanel Distinct ID
        amplitudeDeviceId: 'amplitudeDeviceId', // Optional - Amplitude User ID
        posthogDistinctId: 'posthogDistinctId', // Optional - PostHog Distinct ID
      ),
    );
    print('User data set successfully');
  } catch (e) {
    print('Error setting user data: $e');
  }
}
```

## Setting CleverTap ID

Use the `setAdditionalData` method to set CleverTap ID:

```dart theme={null}
Future<void> setIntegrationData() async {
  try {
    await LinkRunner().setAdditionalData(
      integrationData: {
        'clevertap_id': 'YOUR_CLEVERTAP_USER_ID', // CleverTap user identifier
      },
    );
    print('CleverTap ID set successfully');
  } catch (e) {
    print('Error setting CleverTap ID: $e');
  }
}
```

### Parameters for `LinkRunner.setAdditionalData`

* `clevertap_id`: String (optional) - CleverTap user identifier

This method allows you to connect user identities across different analytics and marketing platforms.

## Revenue Tracking

<Note>
  Revenue data is only stored and displayed for attributed users. Make sure you have implemented the [`.signup`](#user-registration) function before capturing payments. To attribute a test user, follow the [Integration Testing](/testing/integration-testing) guide. You can verify your events are being captured on the [Events Settings](https://dashboard.linkrunner.io/dashboard/settings/events) page.
</Note>

### Capturing Payments

Track payment information:

```dart theme={null}
Future<void> capturePayment() async {
  try {
    await LinkRunner().capturePayment(
      capturePayment: LRCapturePayment(
        amount: 99.99, // Required: Payment amount
        userId: 'user123', // Required: User identifier
        paymentId: 'payment456', // Optional: Unique payment identifier
        type: PaymentType.FIRST_PAYMENT, // Optional: Payment type
        // type: PaymentType.SECOND_PAYMENT, // Optional: Payment type
        status: PaymentStatus.PAYMENT_COMPLETED, // Optional: Payment status
        eventData: { // Optional: Ecommerce/custom event data
          'content_ids': ['product_123'],
          'content_type': 'product',
          'currency': 'USD',
          'value': 99.99,
          'num_items': 1,
          'order_id': 'order_12345',
          'contents': [
            {
              'id': 'product_123',
              'quantity': 1,
              'item_price': 99.99
            }
          ]
        },
      ),
    );
    print('Payment captured successfully');
  } catch (e) {
    print('Error capturing payment: $e');
  }
}
```

#### Parameters for `LRCapturePayment`

* `amount`: double (required) - The payment amount
* `userId`: String (required) - Identifier for the user making the payment
* `paymentId`: String (optional) - Unique identifier for the payment
* `type`: PaymentType (optional) - Type of payment. Available options:
  * `PaymentType.FIRST_PAYMENT` - First payment made by the user
  * `PaymentType.SECOND_PAYMENT` - Second payment made by the user
  * `PaymentType.WALLET_TOPUP` - Adding funds to a wallet
  * `PaymentType.FUNDS_WITHDRAWAL` - Withdrawing funds
  * `PaymentType.SUBSCRIPTION_CREATED` - New subscription created
  * `PaymentType.SUBSCRIPTION_RENEWED` - Subscription renewal
  * `PaymentType.ONE_TIME` - One-time payment
  * `PaymentType.RECURRING` - Recurring payment
  * `PaymentType.DEFAULT_PAYMENT` - Default type (used if not specified)
* `status`: PaymentStatus (optional) - Status of the payment. Available options:
  * `PaymentStatus.PAYMENT_INITIATED` - Payment has been initiated
  * `PaymentStatus.PAYMENT_COMPLETED` - Payment completed successfully (default if not specified)
  * `PaymentStatus.PAYMENT_FAILED` - Payment attempt failed
  * `PaymentStatus.PAYMENT_CANCELLED` - Payment was cancelled
* `eventData`: `Map<String, dynamic>` (optional) - Key-value pairs for additional event data, including Meta ecommerce properties.

### Removing Payments

Remove payment records (for refunds or cancellations):

```dart theme={null}
Future<void> removePayment() async {
  try {
    await LinkRunner().removePayment(
      removePayment: LRRemovePayment(
        userId: 'user123', // Either userId or paymentId must be provided
        paymentId: 'payment456', // Optional: Unique payment identifier
      ),
    );
    print('Payment removed successfully');
  } catch (e) {
    print('Error removing payment: $e');
  }
}
```

#### Parameters for `LRRemovePayment`

* `userId`: String (required) - Identifier for the user whose payment is being removed
* `paymentId`: String (optional) - Unique identifier for the payment to be removed

Note: Either `paymentId` or `userId` must be provided when calling `removePayment`. If only `userId` is provided, all payments for that user will be removed.

## Ecommerce Events

> **Minimum SDK Version:** Ecommerce Event Manager requires `linkrunner-flutter` **v3.7.0** or above. Please ensure your SDK is updated before using this feature.

If you are tracking Ecommerce events to sync with Meta Catalog Sales, you must format your `eventData` to include Meta's required fields. **You also need to map your custom event to the standard commerce event in the Linkrunner Dashboard.**

For detailed explanations of the required fields like `content_ids`, `contents`, and `value`, refer to our [Meta Commerce Manager documentation](/ecommerce-manager/meta-commerce-manager#understanding-event_data).

### Add To Cart Example

Use the `trackEvent` method to send an `AddToCart` event:

```dart theme={null}
Future<void> trackAddToCart() async {
  try {
    await LinkRunner().trackEvent(
      eventName: 'add_to_cart', // Map this custom event to "AddToCart" in the Linkrunner Dashboard
      eventData: {
        'content_ids': ['product_123'],
        'contents': [
          {
            'id': 'product_123', // Matches content_ids
            'quantity': 1,
            'item_price': 49.99
          }
        ],
        'content_type': 'product',
        'currency': 'USD',
        'value': 49.99,
        'num_items': 1
      },
    );
    print('Add To Cart event tracked successfully');
  } catch (e) {
    print('Error tracking Add To Cart event: $e');
  }
}
```

### View Content Example

Use the `trackEvent` method to send a `ViewContent` event:

```dart theme={null}
Future<void> trackViewContent() async {
  try {
    await LinkRunner().trackEvent(
      eventName: 'view_item', // Map this custom event to "ViewContent" in the Linkrunner Dashboard
      eventData: {
        'content_ids': ['product_123'],
        'contents': [
          {
            'id': 'product_123', // Matches content_ids
            'quantity': 1,
            'item_price': 49.99
          }
        ],
        'content_type': 'product',
        'currency': 'USD',
        'value': 49.99,
        'num_items': 1
      },
    );
    print('View Content event tracked successfully');
  } catch (e) {
    print('Error tracking View Content event: $e');
  }
}
```

### Payment / Purchase Example

Use the `capturePayment` method to send a `Purchase` event containing the ecommerce payload:

```dart theme={null}
Future<void> capturePurchase() async {
  try {
    await LinkRunner().capturePayment(
      capturePayment: LRCapturePayment(
        amount: 49.99,
        userId: 'user123',
        paymentId: 'payment_456',
        type: PaymentType.FIRST_PAYMENT, // Map this payment type to "Purchase" in the Linkrunner Dashboard
        status: PaymentStatus.PAYMENT_COMPLETED,
        eventData: {
          'content_ids': ['product_123'],
          'contents': [
            {
              'id': 'product_123', // Matches content_ids
              'quantity': 1,
              'item_price': 49.99
            }
          ],
          'content_type': 'product',
          'currency': 'USD',
          'value': 49.99,
          'num_items': 1,
          'order_id': 'order_abc123' // Required for Purchase events
        },
      ),
    );
    print('Purchase captured successfully');
  } catch (e) {
    print('Error capturing purchase: $e');
  }
}
```

> **Note:** For more information on testing and verifying your ecommerce events, please see our [Testing Ecommerce Events](/ecommerce-manager/meta-commerce-manager#testing-ecommerce-events) guide.

## Tracking Custom Events

<Note>
  Events are only stored and displayed for attributed users. Make sure you have implemented the [`.signup`](#user-registration) function before tracking events. To attribute a test user, follow the [Integration Testing](/testing/integration-testing) guide. You can verify your events are being captured on the [Events Settings](https://dashboard.linkrunner.io/dashboard/settings/events) page. For capturing revenue, it is recommended to use the [`.capturePayment`](#revenue-tracking) method instead of `.trackEvent`.
</Note>

Track custom events in your app:

```dart theme={null}
Future<void> trackEvent() async {
  try {
    await LinkRunner().trackEvent(
      eventName: 'purchase_initiated', // Event name
      eventData: { // Optional: Event data
        'product_id': '12345',
        'category': 'electronics',
        'amount': 99.99, // Include amount as a number for revenue sharing with ad networks like Google and Meta
      },
    );
    print('Event tracked successfully');
  } catch (e) {
    print('Error tracking event: $e');
  }
}
```

### Revenue Sharing with Ad Networks

To enable revenue sharing with ad networks like Google Ads and Meta, include an `amount` parameter as a number in your custom event data. This allows the ad networks to optimize campaigns based on the revenue value of conversions:

```dart theme={null}
Future<void> trackPurchaseEvent() async {
  try {
    await LinkRunner().trackEvent(
      eventName: 'purchase_completed',
      eventData: {
        'product_id': '12345',
        'category': 'electronics',
        'amount': 149.99, // Revenue amount as a number
      },
    );
    print('Purchase event with revenue tracked successfully');
  } catch (e) {
    print('Error tracking purchase event: $e');
  }
}
```

<Note>
  For revenue sharing with ad networks to work properly, ensure the `amount` parameter is passed as a number (double
  or int), not as a string.
</Note>

## Enhanced Privacy Controls

The SDK offers options to enhance user privacy:

```dart theme={null}
// Enable PII (Personally Identifiable Information) hashing
LinkRunner().enablePIIHashing(true);
```

When PII hashing is enabled, sensitive user data like name, email, and phone number are hashed using SHA-256 before being sent to Linkrunner servers.

## Disabling AAID Collection

<Note>
  **SDK Version Requirement:** The AAID collection disable functionality requires Flutter SDK version 3.5.0 or higher.
</Note>

The SDK provides options to disable AAID (Google Advertising ID) collection. This is useful for apps targeting children or families to comply with Google Play's Family Policy.

### Disable AAID Collection

To disable AAID collection, call `setDisableAaidCollection` before SDK initialization:

```dart theme={null}
// Disable AAID collection
LinkRunner().setDisableAaidCollection(true);

// Check if AAID collection is disabled
bool isDisabled = LinkRunner().isAaidCollectionDisabled();
```

When AAID collection is disabled, the SDK will not collect or send the Google Advertising ID (GAID) to Linkrunner servers.

### Removing AD\_ID Permission

If you want to completely remove the `AD_ID` permission from your app's manifest (for example, for apps targeting children), you can override the SDK's permission declaration in your `android/app/src/main/AndroidManifest.xml`. For detailed instructions on revoking the AD\_ID permission, including Google's policy requirements for apps targeting children and Android 13+ (API 33+), see the [Revoking the AD\_ID Permission](#revoking-the-ad-id-permission) section above.

## Uninstall Tracking

### Before you begin

Here's what you need to know before getting started:

**Requirements:**

* Flutter SDK 3.2.2 and later
* [Firebase in your Flutter project (Android)](https://firebase.google.com/docs/flutter/setup)
* [Registering your app with APNs (iOS)](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns)

### Android

Connect Firebase Cloud Messaging (FCM) with Linkrunner

<AccordionGroup>
  <Accordion title="FCM HTTP v1 API">
    To configure FCM HTTP V1 for uninstalls:

    **Enable the FCM API:**

    1. Go to the [FCM console](https://console.firebase.google.com).
    2. Select a project.
    3. Go to **Project Overview** > **Project settings**.
    4. Copy the Project ID. This will be required in a later step.
           <img src="https://mintcdn.com/linkrunner-01ef8e08/LD9nw_sG4NEbwpxy/images/firebase-project-id.png?fit=max&auto=format&n=LD9nw_sG4NEbwpxy&q=85&s=08820fbedd002295f9927df27056a220" alt="Project ID" width="2908" height="1690" data-path="images/firebase-project-id.png" />
    5. Go to the **Cloud Messaging** tab.
    6. Make sure that Firebase Cloud Messaging API (V1) is set to Enabled.

    **Create a custom role for Linkrunner Uninstall:**

    1. Go to the **Service accounts** tab.
    2. Click **Manage service account permissions**.
    3. A new browser tab opens in Google Cloud Platform.
    4. In the side menu, select **Roles**.
    5. Click **+ Create role**.
    6. Enter the following details:
       * **Title**: Linkrunner Uninstalls
       * **ID**: lr\_uninstalls
       * **Role launch stage**: General availability
    7. Click **+ Add permissions**.
    8. In **Enter property name or value** field, enter `cloudmessaging.messages.create` and select it from the search results.
           <img src="https://mintcdn.com/linkrunner-01ef8e08/LD9nw_sG4NEbwpxy/images/google-cloud-permission.png?fit=max&auto=format&n=LD9nw_sG4NEbwpxy&q=85&s=cf6f47ab3bfd035ebe522918cee23083" alt="Google Cloud Permission" width="3014" height="1704" data-path="images/google-cloud-permission.png" />
    9. Check the **cloudmessaging.messages.create** option and click **Add**.
    10. Click **Create**.

    **Assign Linkrunner the FCM uninstall role:**

    1. In the side menu, select **IAM**.
    2. Open the **View by Principals** tab.
    3. Click **Grant Access**.
    4. In **Add Principals** -> **New principals** field, enter `lr-uninstalls-tracking@lr-uninstalls-tracking.iam.gserviceaccount.com`
    5. In **Assign Roles** -> **Select a role** field, enter `Linkrunner Uninstalls` and select it from the search results.
    6. Click **Save**.

    The Linkrunner service account has been assigned the role of Linkrunner Uninstalls.
  </Accordion>

  <Accordion title="Linkrunner Dashboard">
    1. In Linkrunner, go to **Settings** > **Uninstall Tracking**.
    2. Under the **Android** tab, enter the Firebase Project ID that you copied initially and click **Save**.

           <img src="https://mintcdn.com/linkrunner-01ef8e08/dA0-VzMmaaAmaYW8/images/uninstall-tracking-dashboard-android.png?fit=max&auto=format&n=dA0-VzMmaaAmaYW8&q=85&s=c5bd3a7810d89555e9751f4f93e18747" alt="Uninstall Tracking" width="2990" height="1676" data-path="images/uninstall-tracking-dashboard-android.png" />
  </Accordion>

  <Accordion title="Integrate with Linkrunner SDK">
    Follow these instructions to integrate FCM with the Linkrunner SDK:

    1. **Set up Firebase Cloud Messaging:**

    Set up Firebase Cloud Messaging in your flutter app. See the [Firebase Cloud Messaging documentation](https://firebase.google.com/docs/cloud-messaging/flutter/client) for detailed instructions.

    2. Configure your app to provide the device's push token to the Linkrunner SDK.

    ```dart theme={null}
    import 'package:firebase_messaging/firebase_messaging.dart';
    import 'package:linkrunner/linkrunner.dart';

    class MyFirebaseMessagingService {

        static Future<void> initialize() async {
            // Fetch FCM token and set in Linkrunner SDK
            String? token = await FirebaseMessaging.instance.getToken();
            if (token != null) {
                await LinkRunner().setPushToken(token);
            }
        }

        static void setupTokenRefresh() {
            // Receive new FCM token and set in Linkrunner SDK
            FirebaseMessaging.instance.onTokenRefresh
                .listen((fcmToken) async {
                    await LinkRunner().setPushToken(fcmToken);
                })
                .onError((err) {
                    // Error getting token.
                });
        }

        static void setupMessageListener() {
            FirebaseMessaging.onMessage.listen((RemoteMessage message) {
                if (message.data.containsKey("lr-uninstall-tracking")) {
                    return;
                } else {
                    // Handle other data payloads here
                }
            });
        }
    }
    ```

    Custom implementations of FCM's `onMessageReceived` method can unintentionally make uninstall push notifications visible to users, disrupting the intended silent experience. To avoid this, ensure your logic checks if the message contains `lr-uninstall-tracking` and handles it accordingly, as shown in the code example above.
  </Accordion>
</AccordionGroup>

### iOS

Connect APNs with Linkrunner

<AccordionGroup>
  <Accordion title="Apple Developer Portal">
    Get the required credentials from the Apple Developer Portal:

    **APNs Authentication Key (p8) and Key ID:**

    * Go to the [Apple Developer Portal](https://developer.apple.com/account).
    * Select **Identifiers** under **Certificates, IDs & Profiles**.
    * Click on the app you want to track uninstalls for. Then, under **Capabilities**, search for **Push Notifications** and enable it.
    * Under **Certificates, IDs & Profiles**, select **Keys** and click on plus (+) icon to create a key. Enable APNs when creating the key and download the key file (p8).
    * The Key ID can be found in the **Keys** tab.

    **Bundle ID and Team ID:**

    * Under **Identifiers**, click on your app and you will see the Bundle ID and Team ID (App ID Prefix).
  </Accordion>

  <Accordion title="Linkrunner Dashboard">
    1. In Linkrunner, go to **Settings** > **Uninstall Tracking**.
    2. Under the **iOS** tab, upload the APNs Authentication Key (p8) file and enter the Key ID, Bundle ID and Team ID (App ID Prefix) that you copied from the Apple Developer Portal.

           <img src="https://mintcdn.com/linkrunner-01ef8e08/dA0-VzMmaaAmaYW8/images/uninstall-tracking-dashboard-ios.png?fit=max&auto=format&n=dA0-VzMmaaAmaYW8&q=85&s=e44e679c08127f94bc524f89f5e889cb" alt="Uninstall Tracking" width="3000" height="1676" data-path="images/uninstall-tracking-dashboard-ios.png" />
  </Accordion>

  <Accordion title="Integrate with Linkrunner SDK">
    Follow these instructions to integrate FCM with the Linkrunner SDK:

    1. **Set up Firebase Cloud Messaging:**

    Set up Firebase Cloud Messaging in your flutter app if you haven't already. See the [Firebase Cloud Messaging documentation](https://firebase.google.com/docs/cloud-messaging/flutter/client) for detailed instructions.

    2. Configure your app to provide the device's APNs token to the Linkrunner SDK.

    ```dart theme={null}
    import 'package:firebase_messaging/firebase_messaging.dart';
    import 'package:linkrunner/linkrunner.dart';

    class MyFirebaseMessagingService {

        static Future<void> initialize() async {
            // Fetch APNs token and set in Linkrunner SDK
            String? token = await FirebaseMessaging.instance.getAPNSToken();
            if (token != null) {
                await LinkRunner().setPushToken(token);
            }
        }
    }
    ```
  </Accordion>
</AccordionGroup>

## Function Placement Guide

| Function                                | Where to Place                         | When to Call                                   |
| --------------------------------------- | -------------------------------------- | ---------------------------------------------- |
| `LinkRunner().init`                     | App initialization                     | Once when app starts                           |
| `LinkRunner().getAttributionData`       | Attribution data handling flow         | Whenever the attribution data is needed        |
| `LinkRunner().setAdditionalData`        | Integration code                       | When third-party integration IDs are available |
| `LinkRunner().signup`                   | Identification flow (signup or login)  | Once when the user is identified               |
| `LinkRunner().setUserData`              | Authentication logic                   | Every time app opens with logged-in user       |
| `LinkRunner().trackEvent`               | Throughout app                         | When specific user actions occur               |
| `LinkRunner().capturePayment`           | Payment processing                     | When user makes a payment                      |
| `LinkRunner().removePayment`            | Refund flow                            | When payment needs to be removed               |
| `LinkRunner().handleDeeplink`           | Deep link entry points                 | When app is opened via a deep link             |
| `LinkRunner().setDisableAaidCollection` | App initialization or privacy settings | When you need to disable AAID collection       |
| `LinkRunner().isAaidCollectionDisabled` | Privacy settings or compliance checks  | When you need to check AAID collection status  |

## Complete Example

Here's a simplified example showing how to integrate Linkrunner in a Flutter app:

You can find your project token [here](https://dashboard.linkrunner.io/dashboard?s=members\&m=documentation).

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:linkrunner/linkrunner.dart';

final linkrunner = LinkRunner();

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Linkrunner Demo',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  bool _initialized = false;

  @override
  void initState() {
    super.initState();
    _initializeLinkrunner();
  }

  Future<void> _initializeLinkrunner() async {
    try {
      await LinkRunner().init('YOUR_PROJECT_TOKEN');
      setState(() {
        _initialized = true;
      });
    } catch (e) {
      print('Error initializing LinkRunner: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Linkrunner Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('LinkRunner ${_initialized ? 'Initialized' : 'Initializing...'}'),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async {
                await LinkRunner().trackEvent(eventName: 'button_clicked');
              },
              child: Text('Track Custom Event'),
            ),
          ],
        ),
      ),
    );
  }
}
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Test Your Integration" icon="flask" href="/testing/integration-testing">
    Validate your setup end-to-end
  </Card>

  <Card title="Set Up Deep Linking" icon="link" href="/features/deep-linking">
    Configure deep links for your app
  </Card>
</CardGroup>

## Support

If you encounter issues during integration, contact us at [support@linkrunner.io](mailto:support@linkrunner.io).
