Skip to content

Deferred Deep Linking failing specifically via Meta Ads (Facebook/Instagram) #192

@saarcasmic16

Description

@saarcasmic16

GitHub Issue: Deferred Deep Link Not Triggered via Meta App Promotion Campaign

Summary

We are facing an issue where Deferred Deep Linking works perfectly when clicking the Adjust link via a mobile browser, but fails to pass the deep link data when the same link is used in a Meta App Promotion campaign.

Adjust Link Used:
https://alrighttv.go.link/m/69843434610b6e71fe97323a/1?adj_t=1xfmwvhw


Scenario 1: Manual Browser Click (Working)

  1. Click the link in Chrome/Safari on a device without the app.
  2. Redirected to Play Store.
  3. App installed and opened.
  4. User completes onboarding and payment.

Result: The app correctly receives the deep link data and automatically opens the specific show.


Scenario 2: Meta Ad Campaign (Failing)

  1. The same link is entered into the "Deferred Deep Link" input box in the Meta Ads Manager.
  2. User clicks the ad on Facebook/Instagram.
  3. Redirected to Play Store.
  4. App installed and opened.
  5. User completes onboarding and payment.

Result: The app does not trigger the deep link; the user lands on the default home screen instead of the specific show.


Technical Questions for Support

  1. Is there a specific configuration required in the Adjust dashboard to ensure Meta passes the deferred deep link parameters correctly to the SDK for a Flutter app?
  2. Are there known limitations with Meta's "Deferred Deep Link" field when using go.link short links vs. raw Adjust trackers?
  3. Can you verify if the attribution for these Meta installs is correctly firing the deferredDeeplinkCallback callback in the Adjust SDK?

Code Snippets

1. Adjust SDK Initialization & Deferred Deep Link Callback (adjust_service.dart)

// ================= DEFERRED DEEPLINK =================
config.deferredDeeplinkCallback = (String? deeplink) async {
  if (deeplink == null) {
    return;
  }

  try {
    await HandleDeepLinkNavigationService()
        .handleDeepLinkNavigationForAdjust(deeplink, campId: null);
    MoEngageService.instance.trackDeepLinkReceivedEvent(
      deepLinkUrl: deeplink,
      source: 'deferred_adjust',
      success: true,
    );
  } catch (e) {
    MoEngageService.instance.trackDeepLinkReceivedEvent(
      deepLinkUrl: deeplink,
      source: 'deferred_adjust',
      success: false,
      errorMessage: e.toString(),
    );
  }
};

2. Deep Link Navigation Handler (deep_link.dart)

Future<void> handleDeepLinkNavigationForAdjust(
  String deepLinkUrl, {
  String? campId,
}) async {

  if (_shouldSkipAsDuplicate()) {
    log('🔁 Deep link already handled, skipping');
    return;
  }

  try {
    log('🔗 Parsing deep link: $deepLinkUrl');

    // Resolve short Adjust links (e.g. https://alrighttv.go.link/xxxxx) to their
    // full destination URL so that path segments can be parsed correctly.
    final String? resolvedUrl = await Adjust.processAndResolveDeeplink(
      AdjustDeeplink(deepLinkUrl),
    );
    log('🔗 Resolved URL: $resolvedUrl');

    final normalizedUrl = normalizeDeepLink(resolvedUrl ?? deepLinkUrl);
    final uri = Uri.parse(normalizedUrl);

    // IMPORTANT: only pathSegments (handles // automatically)
    final segments = uri.pathSegments.where((e) => e.isNotEmpty).toList();

    log("uri: $uri");
    log("segments: $segments");

    // Find s or m dynamically
    final int smIndex = segments.indexWhere((e) => e == 's' || e == 'm');

    late final String type;
    late final String movieId;
    late final int episodeIndex;
    int? seasonNumber;
    String seasonId = '';
    String configId = '';
    String trialConfigId = '';

    if (smIndex == 0) {
      // Normal case: s/m exists
      type = segments[smIndex];
      movieId = segments[smIndex + 1];
      var nextSegmentIndex = smIndex + 2;
      if (segments.length > nextSegmentIndex &&
          int.tryParse(segments[nextSegmentIndex]) == null) {
        seasonId = segments[nextSegmentIndex];
        nextSegmentIndex++;
      }
      episodeIndex = segments.length > nextSegmentIndex
          ? int.tryParse(segments[nextSegmentIndex]) ?? 0
          : 0;
      // ... (season, configId, trialConfigId parsing)
    } else {
      // Adjust / redirect stripped s/m prefix
      type = 'm'; // default
      movieId = segments[0];
      var nextSegmentIndex = 1;
      if (segments.length > nextSegmentIndex &&
          int.tryParse(segments[nextSegmentIndex]) == null) {
        seasonId = segments[nextSegmentIndex];
        nextSegmentIndex++;
      }
      episodeIndex = segments.length > nextSegmentIndex
          ? int.tryParse(segments[nextSegmentIndex]) ?? 0
          : 0;
      // ... (season, configId, trialConfigId parsing)
    }

    final campaignId = uri.queryParameters['adj_t'];
    await addCampaignIdToServer(campaignId ?? 'sharing');

    // Store deep link data
    box.qWrite(StorageVariable.deepLinkMovieId, movieId);
    box.qWrite(StorageVariable.deepLinkEpisodeIndex, episodeIndex);
    box.qWrite(StorageVariable.isFromDeepLink, true);

    final String? token = box.read(StorageVariable.token);
    final bool isPremiumPlan = box.read(StorageVariable.isPremiumPlan) ?? false;
    final bool freeTrial = box.read(StorageVariable.freeTrial) == true;

    if (token == null || token.isEmpty) {
      log('🔐 User not logged in. Deep link stored.');
      return;
    }

    if (Get.context != null) {
      if (isPremiumPlan || freeTrial) {
        launchScreen(
          Get.context!,
          VideoPreviewScreen(
            movieId: movieId,
            episodeIndex: episodeIndex,
            seasonNumber: seasonNumber,
            seasonId: seasonId.isNotEmpty ? seasonId : null,
          ),
          isNewTask: true,
          pageRouteAnimation: PageRouteAnimation.Slide,
        );
      } else {
        launchScreen(
          Get.context!,
          SubscriptionModelView(fullView: true),
          isNewTask: true,
          arguments: {
            'configId': configId,
            'trialConfigId': trialConfigId,
          },
          pageRouteAnimation: PageRouteAnimation.SlideBottomTop,
        );
      }
    }
  } catch (e) {
    log('❌ Error handling deep link: $e');
    MoEngageService.instance.trackDeepLinkReceivedEvent(
      deepLinkUrl: deepLinkUrl,
      source: 'adjust',
      success: false,
      errorMessage: e.toString(),
    );
  }
}

3. Android Build Configuration (android/app/build.gradle)

plugins {
    id "com.android.application"
    id 'com.google.gms.google-services'
    id 'com.google.firebase.crashlytics'
    id "kotlin-android"
    id "dev.flutter.flutter-gradle-plugin"
}

android {
    namespace = "com.app.alright"
    compileSdk = 36
    ndkVersion = flutter.ndkVersion

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
        coreLibraryDesugaringEnabled true
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_17.toString()
    }

    defaultConfig {
        applicationId = "com.app.alright"
        minSdkVersion 24
        targetSdk = 35
        versionCode = flutter.versionCode
        versionName = flutter.versionName
    }

    signingConfigs {
       release {
           keyAlias keystoreProperties['keyAlias']
           keyPassword keystoreProperties['keyPassword']
           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
           storePassword keystoreProperties['storePassword']
       }
   }

   buildTypes {
       release {
           minifyEnabled false
           shrinkResources false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
           signingConfig signingConfigs.release
       }
   }

   buildFeatures {
     buildConfig true
   }
}

dependencies {
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'

    implementation 'com.android.billingclient:billing:6.1.0'
    implementation 'androidx.browser:browser:1.2.0'

    // Adjust SDK
    implementation 'com.adjust.sdk:adjust-android:4.38.0'

    // Google Play services for Adjust
    implementation 'com.google.android.gms:play-services-ads-identifier:18.1.0'
    implementation 'com.android.installreferrer:installreferrer:2.2'

    implementation("androidx.core:core:1.6.0")
    implementation("androidx.appcompat:appcompat:1.3.1")
    implementation("androidx.lifecycle:lifecycle-process:2.7.0")

    // MoEngage SDK
    implementation("com.moengage:moe-android-sdk:14.02.00")
    implementation("com.moengage:rich-notification:6.1.0")
    implementation("com.moengage:inapp:9.1.1")

    implementation("com.github.bumptech.glide:glide:4.16.0")
    implementation("com.razorpay:checkout:1.6.41")
    implementation("com.razorpay:standard-core:1.7.7")
    implementation("com.razorpay:core:1.0.6")
    implementation 'com.mux:stats.muxcore:8.8.0'
}

flutter {
    source = "../.."
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions