flux_dynamic_link 1.3.0 copy "flux_dynamic_link: ^1.3.0" to clipboard
flux_dynamic_link: ^1.3.0 copied to clipboard

Flutter plugin for Flux Dynamic Link

Flux Dynamic Link (Flutter) #

Flutter plugin to:

  • Receive Flux links (deep links + universal links) and emit the resolved target URL.
  • Create shortened links via your Flux backend.
  • Support deferred deep linking:
    • Android: Play Install Referrer
    • iOS: NativeLink (clipboard token) + fingerprint fallback

This package is designed to work with the Flux Dynamic Link backend (the Worker/API in this repo). You must provide:

  • publicKey: API key used as Authorization: Bearer ...
  • linkDomain: your link service domain, e.g. https://{your_link_domain} or a custom domain

Requirements #

  • Flutter >= 3.3.0
  • Dart >= 3.10.0

1) Create your Flux project #

Create your project in FluxBuilder (download: https://fluxbuilder.com/download).

Keep these values:

  • Project Id
  • Project Key (used as publicKey)
  • Link domain (used as linkDomain)

2) Install #

Add to your app pubspec.yaml:

dependencies:
  flux_dynamic_link: ^1.3.0
  app_links: ^7.0.0

Then run:

flutter pub get

3) Platform setup #

Android #

Add deep link + app link intent-filters to your app AndroidManifest.xml on your main Activity.

Replace:

  • {your_scheme} with your custom URL scheme
  • {your_link_domain} with your custom domain host if you use one
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
        android:scheme="{your_scheme}"
        android:host="{your_link_domain}" />
</intent-filter>

<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_link_domain}" />
</intent-filter>

Also ensure your domain hosts a valid /.well-known/assetlinks.json for Android App Links verification.

iOS #

  1. Add a custom URL scheme in ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>{your_scheme}</string>
    </array>
  </dict>
</array>
  1. Enable Universal Links (recommended):
  • Open Xcode → Runner target → Signing & Capabilities → add Associated Domains
  • Add: applinks:{your_link_domain} (or your custom domain)

Your domain must host a valid Apple App Site Association file (AASA).

4) Initialize #

Important: if you want deferred deep link callbacks, call FluxDynamicLink.setOnInstallReferrerDetected before initialize().

import 'package:flutter/widgets.dart';
import 'package:flux_dynamic_link/flux_dynamic_link.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  FluxDynamicLink.setOnInstallReferrerDetected((result) {
    // Called once per install (guarded by local storage)
    if (result.matched == true) {
      // Handle deferred deep link here
      // result.deepLink / result.webFallbackUrl / result.metadata
    }
  });

  await FluxDynamicLink.initialize(
    publicKey: '{publicKey}',
    linkDomain: 'https://{your_link_domain}',
  );

  runApp(const MyApp());
}

Use app_links package to handle incoming links, then pass them to FluxDynamicLink for processing.

import 'package:app_links/app_links.dart';
import 'package:flux_dynamic_link/flux_dynamic_link.dart';

class _MyAppState extends State<MyApp> {
  final AppLinks _appLinks = AppLinks();
  bool _isInitialLinkHandled = false;

  @override
  void initState() {
    super.initState();
    initAppLinks();
  }

  Future<void> initAppLinks() async {
    try {
      // Handle initial link when app is launched from a link
      final initialUri = await _appLinks.getInitialLink();
      if (initialUri != null && !_isInitialLinkHandled) {
        print('[AppLinks] Initial link: $initialUri');
        await FluxDynamicLink.instance.handleDynamicLink(initialUri.toString());
        _isInitialLinkHandled = true;
      }

      // Listen for incoming links when app is already running
      _appLinks.uriLinkStream.listen(
        (uri) {
          print('[AppLinks] Incoming link: $uri');
          FluxDynamicLink.instance.handleDynamicLink(uri.toString());
        },
        onError: (error) {
          print('[AppLinks] Link stream error: $error');
        },
      );

      // Listen to the dynamicLinkStream and handle navigation
      FluxDynamicLink.instance.dynamicLinkStream.listen((url) {
        final uri = Uri.tryParse(url);
        if (uri == null) return;
        // Navigate based on uri.path / uri.queryParameters
        print('Resolved link: $url');
      });
    } catch (error) {
      print('[AppLinks] Initialization error: $error');
    }
  }
}

The dynamicLinkStream emits the resolved target URL (usually actual_url from the link, otherwise a fallback resolved from the backend).

6) Deferred deep linking #

Android (Play Install Referrer) #

On first app open after installation, the plugin reads Play Install Referrer and attempts to match a deferred deep link. You receive the result via setOnInstallReferrerDetected.

On iOS, App Store installs do not preserve query params into the app. Flux supports a deterministic NativeLink-style flow:

  1. Your iOS landing page generates a single-use token server-side.
  2. On user gesture (CTA tap), the page copies https://<domain>/_nl?t=TOKEN into clipboard and redirects to the App Store.
  3. On first app open, the plugin:
    • reads clipboard once (to reduce repeated paste permission prompts)
    • obtains a stable install_id from Keychain
    • calls POST /api/nativelink/redeem to redeem TOKEN
  4. If NativeLink is missing/expired/overwritten/denied, the plugin falls back to fingerprint matching.

Backend requirement: expose POST /api/nativelink/redeem on your linkDomain.

Use createShortenedLink to create a link, then getFullUrl to get the shareable URL.

final linkDetails = await FluxDynamicLink.instance.createShortenedLink(
  CreateFluxDynamicLinkForm(
    slug: 'product-123',
    iosDeeplink: 'fluxstore://product/123',
    androidDeeplink: 'fluxstore://product/123',
    androidPackage: 'com.inspireui.fluxstore',
    appstoreUrl: 'https://apps.apple.com/app/id1234567890',
    playstoreUrl: 'https://play.google.com/store/apps/details?id=com.inspireui.fluxstore',
    webFallbackUrl: 'https://fluxstore.app/product/123',
    metadata: LinkMetadata(
      title: 'FluxStore Product 123',
      description: 'Check out this product',
      image: 'https://fluxstore.app/images/product-123.jpg',
    ),
    expiresAt: DateTime.now().add(const Duration(days: 30)),
  ),
);

final url = FluxDynamicLink.instance.getFullUrl(linkDetails);
// => {linkDomain}/l/{slug}

8) Custom domains (optional) #

If you use a custom domain (e.g. https://links.customer.com), update all of these:

  • App initialization: linkDomain: 'https://links.customer.com'
  • Android: intent-filter host + assetlinks.json must be served by that domain
  • iOS: Associated Domains must include applinks:links.customer.com and AASA must be served by that domain

Typical DNS is:

links.customer.com CNAME deeplink.fluxbuilder.com

Testing #

  • Android Install Referrer is only reliable when installed from Play Store (or internal testing tracks). A local flutter run install usually will not provide real referrer data.

Troubleshooting #

  • Not receiving links:
    • Ensure the clicked link host matches linkDomain (or deeplink.fluxbuilder.com).
    • Verify Android intent-filters + assetlinks.json.
    • Verify iOS Associated Domains + AASA.
  • iOS deferred not matched:
    • NativeLink must be copied on a user gesture.
    • Clipboard can be overwritten before first app open; plugin will then fallback to fingerprint.