smler_deferred_link 1.2.0
smler_deferred_link: ^1.2.0 copied to clipboard
Lightweight Flutter plugin enabling Android Install Referrer & iOS clipboard deferred deep linking to land users on exact screens.
Flutter Deferred Deep Link #
A powerful yet lightweight Flutter plugin for deferred deep linking built for real production apps. It helps you extract referral information and deep link parameters on both Android and iOS without heavy attribution SDKs.
π What Is Deferred Deep Linking? #
Deferred deep linking allows your user to install your app after clicking a link, and still land on the correct screen or carry referral metadata after install.
π How It Works β Deferred Deep Linking (Android + iOS) #
If the user has not installed the app and they click a deep link, it will first open in the phoneβs default browser. From the browser, the system automatically detects the platform (Android or iOS) and redirects the user to the respective store:
Android β Google Play Store
iOS β Apple App Store
After installation and first app launch, the app will be able to read the deferred deep-link parameters and navigate to the exact intended screen inside the app.
This is the core idea of Deferred Deep Linking β opening the correct screen after the app is installed.
If you require direct deep linking (when the app is already installed), you should use packages like app_links or uni_links. This plugin focuses specifically on Deferred Deep Linking, not direct runtime linking.
You do not need Branch, Adjust, AppsFlyer, or any other paid SDK. Everything works using native platform features.
Platform Behavior #
Android
We use the Google Play Install Referrer API, which is officially supported by Google. This API lets us read details from:
https://play.google.com/store/apps/details?id=<package>&referrer=<encoded_params>
From the referrer parameter, we decode and route the user to the correct screen.
iOS
Deferred deep linking usually works out-of-the-box for many iOS users. However, for users with iCloud+ Private Relay enabled, their IP address is masked, preventing proper session matching by servers.
To avoid this problem, we use an alternative solution:
β The deep link is copied to the clipboard
β When the app is opened the first time, we read the clipboard
β If the link matches your allowed domains, we extract parameters and navigate to the correct screen
This ensures deferred linking works reliably, even under Private Relay.
Backend Support (Important) #
You must handle one small backend/website step:
When a user clicks the deep link, the web page should redirect them to:
Android
https://play.google.com/store/apps/details?id=<your.package>&referrer=<param>%3D<value>
Encode your parameters properly
The app will decode
iOS
Your webpage should ensure the deep link is placed in the clipboard:
example.com?referrer=<value>&page=<screen>
The plugin will read the clipboard to retrieve these values on first app launch.
This plugin solves both platforms:
| Platform | How It Works |
|---|---|
| Android | Uses official Google Play Install Referrer API to read the referrer param from Play Store. |
| iOS | Reads clipboard deep links (URL copied before launching app). Pattern-matches domains, subdomains, and paths, then extracts query parameters. |
| Advanced | Optional probabilistic matching for enhanced attribution when traditional methods fail (requires network call). |
π Why Use This Plugin? #
β Lightweight (no SDKs like Branch / Adjust / AppsFlyer)
β Core features work 100% offline
β Optional probabilistic matching for advanced attribution
β Automatic iOS fallback when clipboard is empty
β Zero configuration on backend for basic usage
β Works from 1st launch
β Supports unlimited custom query params
β Works with any URL structure
β Subdomains + www + scheme normalization
β Clean, safe architecture with cached responses
π§ Use Cases #
β Track marketing campaign using:
?referrer=campaign123
β Store affiliate codes
β Open after-install screens:
β Route iOS users from Safari β clipboard β app
β Internal routing: /bonus?referrer=promo50
β Attribution without Firebase Dynamic Links / Branch
β Fallback attribution when clipboard is empty (iOS)
β Cross-platform probabilistic device fingerprinting
β High-confidence install-to-click matching via API
π Architecture Overview #
Flutter App
|
|-- Platform.isAndroid ---------------------------|
| |
| Android Native (Kotlin) |
| - InstallReferrerClient |
| - Single connection + retry |
| - Cache result |
| - Return Map to Dart -----> ReferrerInfo |
| |
|-- Platform.isIOS --------------------------------|
| |
iOS Clipboard Reader (Dart) |
- Reads Clipboard.kTextPlain |
- Pattern matcher (domain/path/subdomain) |
- Parses as URI ----------------> IosClipboardDeepLinkResult
π¦ Installation #
Add:
dependencies:
smler_deferred_link: <latest-version>
The plugin automatically includes:
httpfor API calls (probabilistic matching)device_info_plusfor device fingerprinting
β Android Setup #
The plugin already includes:
implementation "com.android.installreferrer:installreferrer:2.2"
No permissions are required.
π iOS Setup #
Nothing special needed.
The plugin uses:
Clipboard.getData(Clipboard.kTextPlain)
This works on all iOS versions supported by Flutter.
π Permissions No permissions required on both platforms.
π API Reference #
π 1. Android: getInstallReferrerAndroid()
Reads Google Play Install Referrer once.
final info = await SmlerkDeferredLink.getInstallReferrerAndroid();
Returns: ReferrerInfo
info.installReferrer; // raw "utm_source=...&referrer=..."
info.asQueryParameters; // parsed params Map<String, String>
info.referrerClickTimestampSeconds;
info.installBeginTimestampSeconds;
info.installVersion;info.googlePlayInstantParam;
Example
final info = await SmlerDeferredLink.getInstallReferrerAndroid();
final params = info.asQueryParameters;
debugPrint(params['referrer']); // campaign123
debugPrint(params['uid']); // optional
Extracting shortCode and dltHeader from Path #
ReferrerInfo.extractShortCodeAndDltHeader() extracts structured path segments from the referrer URL.
Supports two formats:
https://domain.com/[dltHeader]/[shortCode]- with optional dltHeaderhttps://domain.com/[shortCode]- shortCode only
Example:
final info = await SmlerDeferredLink.getInstallReferrerAndroid();
final pathParams = info.extractShortCodeAndDltHeader();
debugPrint(pathParams['shortCode']); // e.g., "abc123"
debugPrint(pathParams['dltHeader']); // e.g., "promo" or null
Tracking Clicks #
ReferrerInfo.trackClick() automatically sends tracking data to the Smler API.
This method:
- Extracts
clickIdfrom theclickIdquery parameter - Extracts
shortCodeand optionaldltHeaderfrom the URL path - Sends tracking data to
https://smler.in/api/v2/track/{clickId} - Returns
nullifclickIddoesn't exist (no API call made)
Example:
final info = await SmlerDeferredLink.getInstallReferrerAndroid();
final response = await info.trackClick();
if (response != null) {
debugPrint('Tracking successful: $response');
} else {
debugPrint('No clickId found, tracking skipped');
}
Extracting a Single Query Parameter #
ReferrerInfo.getParam(key) lets you safely extract a single query parameter from the install referrer string β regardless of how Google Play sends it.
This method works with:
-
Full URLs (https://example.com/path?ref=123)
-
URLs without schemes (example.com/path?ref=123)
-
Subdomains (https://sub.example.com/...)
-
Raw Android referrer strings (utm_source=google&ref=mycode)
-
Any nested query formats
β Example (Android Install Referrer)
final info = await SmlerDeferredLink.getInstallReferrerAndroid();
final ref = info.getParam('ref');
print(ref); // e.g. "promo123"
β Example (Multiple params)
final campaign = info.getParam('utm_campaign');
final source = info.getParam('utm_source');
Throws
| Exception | Reason |
|---|---|
UnsupportedError |
Called on iOS/Web/Desktop |
PlatformException |
Play service unavailable, feature not supported |
StateError |
Unexpected parsing issues |
π 2. iOS: getInstallReferrerIos() Reads clipboard β checks patterns β returns matched deep link + params.
final result = await SmlerDeferredLink.getInstallReferrerIos(deepLinks: ["https://example.com/profile","example.com","sub.example.com"]);
Returns: IosClipboardDeepLinkResult?
result.fullReferralDeepLinkPath; // full string
result.queryParameters; // parsed params
result.getParam("referrer"); // campaign123
result.getParam("uid");
Extracting shortCode and dltHeader from Path (iOS) #
IosClipboardDeepLinkResult.extractShortCodeAndDltHeader() extracts structured path segments from the deep link URL.
Supports two formats:
https://domain.com/[dltHeader]/[shortCode]- with optional dltHeaderhttps://domain.com/[shortCode]- shortCode only
Example:
final result = await SmlerDeferredLink.getInstallReferrerIos(
deepLinks: ["example.com"],
);
if (result != null) {
final pathParams = result.extractShortCodeAndDltHeader();
debugPrint(pathParams['shortCode']); // e.g., "abc123"
debugPrint(pathParams['dltHeader']); // e.g., "promo" or null
}
Tracking Clicks (iOS) #
IosClipboardDeepLinkResult.trackClick() automatically sends tracking data to the Smler API.
This method:
- Extracts
clickIdfrom theclickIdquery parameter - Extracts
shortCodeand optionaldltHeaderfrom the URL path - Sends tracking data to
https://smler.in/api/v2/track/{clickId} - Returns
nullifclickIddoesn't exist (no API call made)
Example:
final result = await SmlerDeferredLink.getInstallReferrerIos(
deepLinks: ["example.com"],
);
if (result != null) {
final response = await result.trackClick();
if (response != null) {
debugPrint('Tracking successful: $response');
} else {
debugPrint('No clickId found, tracking skipped');
}
}
Matching Rules
Accepts:
http://, https://, or no scheme
Subdomains (m.example.com, sub.example.com)
www. variants
Path must match pattern prefix (optional)
Example
final res = await SmlerDeferredLink.getInstallReferrerIos(deepLinks: ["example.com", "example.com/profile"]);
if (res != null) {
final referrer = res.getParam('referrer');
debugPrint("iOS Referrer: $referrer");
}
π Probabilistic Matching (Advanced Attribution) #
Probabilistic matching enables accurate install attribution by analyzing device and network fingerprints when traditional methods fail or return insufficient data. This is particularly useful for iOS users when clipboard matching fails or for validating Android install referrer data.
When to Use Probabilistic Matching #
β
iOS Fallback: When getInstallReferrerIos() returns null (clipboard empty or no match)
β Cross-Platform Validation: Confirm install attribution across both platforms
β Enhanced Attribution: Get additional click metadata and campaign information
How It Works #
- Automatic Device Detection: Plugin automatically extracts device model, OS version, and platform
- API Matching: Sends fingerprint to Smler API for probabilistic matching
- Confidence Score: Returns a match score (0.0 to 1.0) indicating confidence level
- Smart Threshold: Only fetch tracking data when score > 0.65 for high-confidence matches
π API: getProbabilisticMatch() #
Performs probabilistic matching to link install events to clicks.
final result = await HelperReferrer.getProbabilisticMatch(
domain: 'example.com',
clickId: 'optional-click-id', // optional
);
Parameters:
domain(required): Your domain name (e.g., "example.com")clickId(optional): Click identifier for enhanced matching
Returns: Map<String, dynamic> with:
matched(bool): Whether a match was foundscore(double): Confidence score (0.0 - 1.0)matchedAttributes(List): Attributes that matchedclickDetails(Map): Details of the matched clickshortUrl(Map): Complete short URL object with metadatafingerprint(Map): Device fingerprint datadomain(String): Extracted domain from shortUrlpathParams(Map): ContainsshortCode,dltHeader, anddomain
Example:
import 'package:smler_deferred_link/src/helpers.dart';
final result = await HelperReferrer.getProbabilisticMatch(
domain: 'example.com',
);
if (result['matched'] == true) {
final score = result['score'] as double;
print('Match confidence: $score');
if (score > 0.65) {
// High confidence - proceed with attribution
final pathParams = result['pathParams'];
print('Short code: ${pathParams['shortCode']}');
print('Domain: ${pathParams['domain']}');
}
}
π API: fetchTrackingData() #
Fetches detailed tracking data from the Smler API when you have a clickId.
final trackingData = await HelperReferrer.fetchTrackingData(
clickId,
pathParams,
domain,
);
Parameters:
clickId(String): The click ID from query parameterspathParams(Map<String, String?>): Map containingshortCodeand optionaldltHeaderdomain(String?): The domain name from the referrer URL
Returns: Map<String, dynamic> with API response data or error information
Example:
final clickDetails = result['clickDetails'] as Map<String, dynamic>?;
final clickId = clickDetails?['clickId'] as String?;
final pathParams = result['pathParams'] as Map<String, dynamic>?;
if (clickId != null && pathParams != null) {
final trackingData = await HelperReferrer.fetchTrackingData(
clickId,
Map<String, String?>.from(pathParams),
result['domain'] as String?,
);
print('Tracking data: $trackingData');
}
π iOS Fallback Pattern (Recommended) #
Use probabilistic matching as a fallback when clipboard matching fails on iOS:
Future<void> _loadInstallReferrerIos() async {
try {
final result = await SmlerDeferredLink.getInstallReferrerIos(
deepLinks: ['example.com', 'example.com/profile'],
);
if (result == null) {
// Clipboard empty or no match - fall back to probabilistic
debugPrint('π Falling back to probabilistic matching...');
await _tryProbabilisticMatch('example.com');
return;
}
// Process clipboard result
final params = result.queryParameters;
// ... handle navigation
} catch (e) {
debugPrint('Error: $e');
}
}
Future<void> _tryProbabilisticMatch(String domain) async {
final result = await HelperReferrer.getProbabilisticMatch(
domain: domain,
);
if (result['matched'] == true) {
final score = result['score'] as double;
if (score > 0.65) {
// High confidence - fetch tracking data
final clickDetails = result['clickDetails'] as Map?;
final clickId = clickDetails?['clickId'] as String?;
final pathParams = result['pathParams'] as Map?;
if (clickId != null && pathParams != null) {
final trackingData = await HelperReferrer.fetchTrackingData(
clickId,
Map<String, String?>.from(pathParams),
result['domain'] as String?,
);
// Process tracking data and navigate
debugPrint('Attribution confirmed: $trackingData');
}
}
}
}
π― Android Enhanced Attribution #
Combine install referrer with probabilistic matching for complete attribution:
Future<void> _loadInstallReferrerAndroid() async {
final info = await SmlerDeferredLink.getInstallReferrerAndroid();
final params = info.asQueryParameters;
// Get basic referrer data
debugPrint('Referrer: ${info.installReferrer}');
// Enhance with probabilistic matching
final result = await HelperReferrer.getProbabilisticMatch(
domain: 'example.com',
);
if (result['matched'] == true && result['score'] > 0.65) {
// Cross-validate attribution
debugPrint('Probabilistic match confirms attribution');
// ... proceed with tracking
}
}
β‘ No Permissions Required #
Device and OS information is automatically extracted using the device_info_plus package without requiring any special permissions. The plugin accesses:
β Device manufacturer and model (e.g., "Samsung Galaxy S21")
β OS version (e.g., "Android 13", "iOS 16.4")
β System name and basic hardware info
β No unique identifiers (IMEI, serial numbers, advertising IDs)
β No runtime permission dialogs
β No manifest/plist configuration needed
π§ͺ Full Usage Example (Android + iOS) #
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String? status;
Map<String, String> params = {};
Map<String, dynamic>? trackingResponse;
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
try {
if (Platform.isAndroid) {
final info = await SmlerDeferredLink.getInstallReferrerAndroid();
params = info.asQueryParameters;
status = "Android Referrer Loaded";
// Track click automatically
trackingResponse = await info.trackClick();
} else if (Platform.isIOS) {
final res = await SmlerDeferredLink.getInstallReferrerIos(
deepLinks: ["example.com", "example.com/profile"],
);
if (res != null) {
params = res.queryParameters;
status = "iOS Clipboard Deep Link Loaded";
// Track click automatically
trackingResponse = await res.trackClick();
} else {
status = "No deep link found";
}
}
} catch (e) {
status = "Error: $e";
}
setState(() {});
}
@override
Widget build(BuildContext ctx) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("Smler Deferred Link")),
body: Padding(
padding: const EdgeInsets.all(16),
child: ListView(
children: [
Text(status ?? "Loading..."),
const SizedBox(height: 20),
const Text("Params:", style: TextStyle(fontSize: 18)),
...params.entries.map((e) => Text("${e.key}: ${e.value}")),
if (trackingResponse != null) ...[
const SizedBox(height: 20),
const Text("Tracking Response:", style: TextStyle(fontSize: 18)),
Text(trackingResponse.toString()),
],
],
),
),
),
);
}
}
π§ Best Practices #
β Call API only once on first screen
The plugin caches results automatically.
β Store result locally
Install referrer is static and wonβt change.
β For iOS
Use clipboard reading only on first launch, optional:
await Clipboard.setData(const ClipboardData(text: ""));
π Troubleshooting #
β Android returns empty referrer
Play Store did not include any referrer parameter. Consider using probabilistic matching as a fallback.
β iOS returns null
Clipboard may be empty or the link does not match any allowed pattern. Use probabilistic matching as a fallback strategy.
β iOS parsing fails
Ensure your passed URL patterns include base domains.
β Cannot parse URL
Clipboard might contain text that is not a URL.
β Probabilistic match score is too low (< 0.65)
This indicates low confidence in the match. The device fingerprint may not match any recent clicks, or multiple similar clicks exist. Only proceed with attribution if you accept lower confidence.
β Probabilistic matching returns an error
Check your network connection and ensure the domain parameter is correct. The API endpoint must be reachable.
Clipboard might contain text that is not a URL.
β FAQ #
Does this plugin track users?
Core features (Install Referrer API & clipboard reading) are 100% offline with no network calls. Optional probabilistic matching and tracking APIs make network calls to the Smler API only when explicitly invoked.
Can I use this without making network calls?
Yes. Simply don't call getProbabilisticMatch() or fetchTrackingData(). The basic Install Referrer and clipboard features work completely offline.
Can I clear Android referrer?
No. Google Play controls it. You can ignore it after reading.
Is clipboard reading safe / allowed?
Yes, Flutter allows access to clipboard text.
Can it handle /path/subpath?
Yes. Pattern paths must match prefix.
For more information see https://developer.android.com/google/play/installreferrer