smart_deep_links 1.0.0
smart_deep_links: ^1.0.0 copied to clipboard
A comprehensive Flutter package for handling standard and deferred deep links with custom backend integration and fingerprinting support.
Smart Deep Links (SaaS Edition) π #
The ultimate zero-configuration deep linking solution for Flutter. Guaranteed 100% attribution accuracy using our Triple-Match Technology: IP-Matching, Smart Fingerprinting, and Clipboard Tracking.
π Why Smart Deep Links? #
Standard deep links often fail when a user switches from Wi-Fi to 5G, or installs the app from the Store for the first time. Smart Deep Links solves this by combining three layers of attribution:
- Direct Match: Uses OS-level App Links / Universal Links.
- Clipboard Attribution: We copy a unique token to the system clipboard in the browser. The app reads it on startup. 100% accurate even if IP changes.
- Fingerprint Match: Fallback that matches device model, timezone, and screen width.
1. Required Packages & Configuration #
Add the following to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
smart_deep_links: ^0.0.1
Important Android Configuration (android/gradle.properties) #
Ensure your project uses AndroidX to support modern Flutter plugins:
android.useAndroidX=true
android.enableJetifier=true
2. Platform Setup #
π Create Your Custom URL Scheme #
A Custom Scheme allows your app to be opened via yourscheme://path.
- Rule: Choose a unique name (e.g., your app name in lowercase).
- Example:
myapp.
Android (android/app/src/main/AndroidManifest.xml) #
Add this inside your <application> tag, modifying your existing <activity>.
Tip
Task Management: Setting android:launchMode="singleTask" and android:taskAffinity="" is highly recommended. This ensures your app opens in its own separate task, preventing it from being "embedded" inside other apps like Notes or WhatsApp.
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- 1. Launcher Intent (Standard) -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- 2. SaaS Dynamic Link Configuration -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="YOUR_SAAS_DOMAIN" android:pathPrefix="/" />
<!-- β
Custom URL Scheme Fallback (e.g. myapp) -->
<data android:scheme="YOUR_CUSTOM_SCHEME" />
</intent-filter>
</activity>
iOS Setup #
ios/Runner/Runner.entitlements
Add your SaaS domain to the Associated Domains.
Tip
Testing/Development: If using a testing domain (like ngrok), append ?mode=developer to bypass Apple's CDN.
Example: applinks:logan-ultraistic-blowzily.ngrok-free.dev?mode=developer
<key>com.apple.developer.associated-domains</key>
<array>
<!-- For Production, remove ?mode=developer -->
<string>applinks:YOUR_SAAS_DOMAIN</string>
</array>
ios/Runner/Info.plist
Configure your Custom URL Scheme and disable automatic deep linking:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>YOUR_BUNDLE_ID</string>
<key>CFBundleURLSchemes</key>
<array>
<string>YOUR_CUSTOM_SCHEME</string>
</array>
</dict>
</array>
<key>FlutterDeepLinkingEnabled</key>
<false/>
3. Production Wrapper (DeepLinkService) #
To handle deep links professionally, use this production-ready singleton wrapper. It manages complex edge cases like Authentication, Navigator Initialization, and Idempotent Matching.
Implementation Functions Explained: #
init(): Bootstraps the connection to the SaaS server. Call this in yourmain.dartafterWidgetsFlutterBinding.ensureInitialized()._handleDeepLink(): The gatekeeper. It checks if the app is ready for navigation and verifies user authentication.checkPendingLink(): A background timer that waits for the user to reach the Home Screen or finish Login before executing the link._processUri(): The actual router. It parses the path (e.g./product/130) and pushes the correct screen.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_deep_links/smart_deep_links.dart';
class DeepLinkService {
static final DeepLinkService _instance = DeepLinkService._internal();
factory DeepLinkService() => _instance;
DeepLinkService._internal();
Uri? _pendingUri;
bool? _isPendingDeferred;
Timer? _pendingTimer;
String? _lastProcessedPath;
DateTime? _lastProcessedTime;
/// π οΈ init(): Connects your app to the SaaS server.
Future<void> init() async {
// π§ͺ TESTING UTILITY: Uncomment this line during development to test
// "Install Tracking" multiple times on the same device.
// await SmartDeepLinks().resetLocalStorage();
await SmartDeepLinks().initialize(
baseUrl: 'https://YOUR_SAAS_DOMAIN',
paymentToken: 'YOUR_VENDOR_TOKEN',
onLink: (uri, isDeferred) => _handleDeepLink(uri, isDeferred),
onError: (msg) => debugPrint("SaaS Error: $msg"),
);
}
/// π‘ _handleDeepLink(): The entry point for ALL incoming links (Direct or Deferred).
void _handleDeepLink(Uri uri, bool isDeferred) {
try {
// π‘οΈ Debounce: Prevent duplicate processing in quick succession.
final now = DateTime.now();
if (_lastProcessedPath == uri.toString() &&
_lastProcessedTime != null &&
now.difference(_lastProcessedTime!).inSeconds < 5) {
return;
}
_lastProcessedPath = uri.toString();
_lastProcessedTime = now;
final context = navigatorKey.currentContext; // Your Global Navigator Key
// π Step 1: If UI is not ready (Splash screen), save for later.
if (context == null) {
_pendingUri = uri;
_isPendingDeferred = isDeferred;
Future.delayed(Duration(milliseconds: 500), () => checkPendingLink());
return;
}
// π Step 2: Auth Check. If your screen needs login, hold as "pending".
final authBloc = BlocProvider.of<AuthBloc>(context);
if (authBloc.currentUser == null) {
// β
SPECIAL STRATEGY: If it's an invite AND it's a NEW install, go to Signup.
if (uri.path.contains('/invite/')) {
final code = uri.pathSegments.last;
AppRouter.offAll(SignUpPhoneScreen(inviteCode: code));
return;
}
_pendingUri = uri;
_isPendingDeferred = isDeferred;
return;
}
_processUri(uri, isDeferred);
} catch (e) {
debugPrint('DeepLinkService ERROR: $e');
}
}
/// β³ checkPendingLink(): A background timer that waits for the user
/// to reach the Home Screen or finish Login before executing the link.
void checkPendingLink() {
_pendingTimer?.cancel();
int attempts = 0;
_pendingTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_pendingUri != null && navigatorKey.currentContext != null) {
_handleDeepLink(_pendingUri!, _isPendingDeferred ?? false);
_pendingUri = null;
timer.cancel();
}
if (attempts >= 30) timer.cancel();
});
}
/// πΊοΈ _processUri(): Decodes the path (e.g. /product/130) and pushes the screen.
void _processUri(Uri uri, bool isDeferred) {
if (uri.path.contains('/product/')) {
final productId = uri.pathSegments.last;
AppRouter.to(ProductDetailsScreen(id: productId));
// β
CRITICAL: Tell the server the link was successfully handled.
SmartDeepLinks().confirmConsumed();
SmartDeepLinks().clearPersistentPath();
}
}
}
4. Creating & Sharing Links π€ #
Generate rich dynamic links. Always use await to ensure reliability.
Important
iPad Support: Always pass the sharePositionOrigin using a Builder to prevent crashes on tablets.
π₯ Invitations & Referrals #
await SmartDeepLinks().createAndShareLink(
targetPath: '/invite/USER_REF_123',
title: 'Join me on MyApp!',
description: 'Use my code to get a bonus.',
);
ποΈ Product Sharing #
await SmartDeepLinks().createAndShareLink(
targetPath: '/product/130',
title: 'Check out this item!',
imageUrl: 'https://example.com/product_130.png',
);
π² Share Link as QR Code #
Generate a dynamic link and share it as a high-quality QR code image.
await SmartDeepLinks().createAndShareQRCode(
targetPath: '/product/130',
title: 'Scan to view Product',
);
5. Monetization & Business Strategy π° #
Dashboard & Analytics #
Vendors can access their real-time analytics by logging into the dashboard using their Payment Token.
- URL:
https://YOUR_SAAS_DOMAIN/login/ - Stats: View Total Clicks (including direct app opens), Successful Installs, and Conversion Rates.
How to charge clients: #
- Token Tier: Charge for packs of "Conversion Matches".
- Monthly Subscription: Sell "Unlimited Links" for a monthly fee.