smart_deep_links 1.0.0 copy "smart_deep_links: ^1.0.0" to clipboard
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.


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:

  1. Direct Match: Uses OS-level App Links / Universal Links.
  2. 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.
  3. 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 your main.dart after WidgetsFlutterBinding.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();
    }
  }
}

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',
);

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.
2
likes
140
points
9
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A comprehensive Flutter package for handling standard and deferred deep links with custom backend integration and fingerprinting support.

Homepage

License

MIT (license)

Dependencies

app_links, device_info_plus, dio, flutter, package_info_plus, path, path_provider, share_plus, shared_preferences

More

Packages that depend on smart_deep_links

Packages that implement smart_deep_links