Skip to main content

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:
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:
android {
    defaultConfig {
        minSdkVersion 21
        // other config...
    }
}
  1. Add the following permissions to your AndroidManifest.xml file:
<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, you should revoke AAID and disable AAID collection. See the Disabling AAID Collection section for more details.

Revoking the AD_ID Permission

According to Google’s Policy, 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() and isAaidCollectionDisabled() functions to disable AAID collection programmatically: android/app/src/main/AndroidManifest.xml
<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>
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.
For more information, see Google Play Services documentation.

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.

iOS Configuration

  1. Update your iOS deployment target to iOS 15.0 or higher in your ios/Podfile:
platform :ios, '15.0'
  1. Add the following to your Info.plist file for App Tracking Transparency:
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads and improve your app experience.</string>

Initialization

You’ll need your project token to get started!

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. Note: The initialization method doesn’t return any value. To get attribution data and deeplink information, use the getAttributionData method.
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();
}

Platform-Specific SDK Signing

For applications requiring different signing keys per platform:
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 Registration

Call the signup method once after the user has completed your app’s onboarding process: 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). 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
Future<void> onSignup() async {
  try {
    await LinkRunner().signup(
      userData: LRUserData(
        id: '123', // Required: User ID
        name: 'John Doe', // Optional
        phone: '9876543210', // Optional
        email: '[email protected]', // 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');
  }
}

Getting Attribution Data

To get attribution data and deeplink information for the current installation, use the getAttributionData function:
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:
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:
{
  "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:
Future<void> setUserData() async {
  try {
    await LinkRunner().setUserData(
      userData: LRUserData(
        id: '123', // Required: User ID
        name: 'John Doe', // Optional
        phone: '9876543210', // Optional
        email: '[email protected]', // 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:
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.

Tracking Custom Events

Track custom events in your app:
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:
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');
  }
}
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.

Revenue Tracking

Capturing Payments

Track payment information:
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
      ),
    );
    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

Removing Payments

Remove payment records (for refunds or cancellations):
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.

Enhanced Privacy Controls

The SDK offers options to enhance user privacy:
// 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

SDK Version Requirement: The AAID collection disable functionality requires Flutter SDK version 3.5.0 or higher.
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:
// 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 section above.

Uninstall Tracking

Before you begin

Here’s what you need to know before getting started: Requirements:

Android

Connect Firebase Cloud Messaging (FCM) with Linkrunner
To configure FCM HTTP V1 for uninstalls:Enable the FCM API:
  1. Go to the FCM console.
  2. Select a project.
  3. Go to Project Overview > Project settings.
  4. Copy the Project ID. This will be required in a later step. Project ID
  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. Google Cloud Permission
  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.
  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. Uninstall Tracking
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 for detailed instructions.
  1. Configure your app to provide the device’s push token to the Linkrunner SDK.
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.

iOS

Connect APNs with Linkrunner
Get the required credentials from the Apple Developer Portal:APNs Authentication Key (p8) and Key ID:
  • Go to the Apple Developer Portal.
  • 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).
  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. Uninstall Tracking
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 for detailed instructions.
  1. Configure your app to provide the device’s APNs token to the Linkrunner SDK.
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);
        }
    }
}

Function Placement Guide

FunctionWhere to PlaceWhen to Call
LinkRunner().initApp initializationOnce when app starts
LinkRunner().getAttributionDataAttribution data handling flowWhenever the attribution data is needed
LinkRunner().setAdditionalDataIntegration codeWhen third-party integration IDs are available
LinkRunner().signupOnboarding flowOnce after user completes onboarding
LinkRunner().setUserDataAuthentication logicEvery time app opens with logged-in user
LinkRunner().trackEventThroughout appWhen specific user actions occur
LinkRunner().capturePaymentPayment processingWhen user makes a payment
LinkRunner().removePaymentRefund flowWhen payment needs to be removed
LinkRunner().setDisableAaidCollectionApp initialization or privacy settingsWhen you need to disable AAID collection
LinkRunner().isAaidCollectionDisabledPrivacy settings or compliance checksWhen 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.
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

Support

If you encounter issues during integration, contact us at [email protected].