Dynamic Links

Use Cases

  • When you want users to install app instead of them using website.
  • When you want some kind of redirection from website to app.

Scenerios

  • App is installed but we don't want any redirection from website.
  • App is installed and we want redirections from website.
  • App is not installed and we want to force users to download it.
  • App is not installed and we let users to see content on website.

General Concept

  • Create a dynamic link on firebase.
  • Let's say for /test-path you want to redirect users to app if they are using phone.
  • So for path /test-path in website we will check if user has opened the page in phone or other devices.
    • If it's not phone, we just show content of website.
    • If it's phone, we trigger redirect to dynamic link we got from firebase.
  • When user hits dynamic link, We can configure what we want it to do.
    • if Installed open app and pass the link and data.
    • if App not installed
      • Open app/ play store page for your app.
      • Or redirect to somewhere.
  • We write logic to handle dynamic link and data in app.

How to use?

Dynamic Links
  • VaahExtendFlutter makes it so much easier to integrate dynamic links we will show you the process now.

Setup Firebase For Your Project (if you haven't done that yet)

  • Install Firebase CLI in your system. And Log in to your account.
  • Install the FlutterFire CLI
  • run flutterfire configure in your project -> then select create new project/ configure old project
  • Check the steps in the video: Configure Firebase App
  • Pass parameters to connect firebase app with vaahextendflutter and handle dynamic links. This will intialize the firebase app everytime app is started. Check the steps in the video: Integrating Firebase With VaahFlutter
  • Go to Dynamic Links section
  • Press Get Started
  • Enter the domain
  • And done
  • Check the steps in the video: Enable Dynamic Links

Things you need to know

  • Short dynamic links does not support custom parameters/ passing custom data.
  • Long dynamic links does support it but there are some rules you follow to make it work perfectly.
  • In long dynamic links you need to encode deep link which you're passing. (On the website part, Stay with us and this point will make sense.)
Team ID
For iOS; Team ID in firebase (iOS App) is mandetory and it can't be fake - and using that you will have to sign your app, where for development purpose fake app id will work.
  • Short links
    • https://yourapp.page.link/custom ✅ works
    • https://yourapp.page.link/custom?payload={} ❌ parameters won't be passed to app
  • Long links
    • https://yourapp.page.link/?link=https://website.com/custom?payload={}&apn=com.example.app&ibi=com.example.app&isi=123456789 ✅ works, where deep link should be encoded (for better reading we did not encoded it in documentation.)
    • where https://yourapp.page.link/ is main dynamic link. And you pass deep link after ?link= which is passed in app and should be handled by app e.g. https://website.com/custom?payload={} (should be encoded tho). You pass android package name with &apn=com.example.app and iOS bundle identifier &ibi=com.example.app. You pass App Store ID using &isi=123456789.
    • If app isn't installed and you want user to redirect to some link you pass parameters link; for android fallback link as &afl=https://vaahflutter.ml for iOS fallback link as &ifl=https://vaahflutter.ml
  • In your website
    • Do not redirect links to the same link from where it's coming; when user is not using mobile devices.
    • Example of bad implementation: for website.com/custom you've applied redirect logic for that path (without checking if user is using mobile or not). Which will redirect to dynamic link.
    • So when user opens website.com/custom -> it redirects to dynamic link -> as user is not on mobile device they are redirected to website.com/custom again. -> again it redirects to dynamic link -> ...
    • So when this link is opened in browser in device other than Android and iOS, the link resolves it to the same link from where it came (website.com/custom). You can see, how it becomes infinite loop.

We suggest you create one single link to handle everthing. But you pass different parameters with it. Stay with us and it will make sense.

  • In the Dynamic Links section, click on New Dynamic Link. And configure it like shown in video.
  • Check the steps in the video: Create Dynamic Link

Add Intent Filters In Android Menifest

<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:host="name.page.link" android:scheme="https"/>
</intent-filter>

Add Associated Domain In iOS Project

path: ios/Runner/Runner.entitlements

<dict>
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:YOUR_FIREBASE_APP_DYNAMIC_LINK_PREFIX.page.link</string>
    </array>
</dict>
</plist>

Encoding URL (Website Part)

  • Dynamic link is whole link, it contains deep link and other parameters.
  • Deep link is a link which is passed to the app from website.
Encoding
  • In dynamic link, the deep link you are passing has to be encoded.
  • If it's encoded incorrectly then it won't be passed to app. It will be broken because we have special characters in our link.
  • So we can't pass deep link without encoding it.
  • In Url parameters are encoded differently and whole url is encoded differently.
  • We have https://website.com/custom?payload={} as deep link
  • So we can see parameter part here is : ?payload={}
  • And link part is : https://website.com/custom
  • We can't encode both parts same way.
    • e.g. if we talk about javascript: for parameter encoding we use encodeURIComponent and after that we add that encoded part to the link part and then encode whole thing with encodeURI.
    • e.g. if we talk about PHP: for parameter encoding we use rawurlencode method and after that we add that encoded part to the link part and then encode whole thing with urlencode.
  • So as example

For Javascript

let link = 'https://website.com/custom';
let parameters = encodeURIComponent('?payload={}');
let encodedURL = encodeURI(`link${parameters}`);

// Use encodedURL for redirection

For PHP

$link = 'https://website.com/custom?payload=';
$parameters = rawurlencode('{}');
$encoded =  urlencode($link.$parameters);

// Use encoded output for redirection
Encoding url and parameters

Never encode in the manner illustrated here

For Javascript

encodeURIComponent('https://website.com/custom?payload={}');
encodeURI('https://website.com/custom?payload={}');
encodeURIComponent('https://website.com/custom');
encodeURI('?payload={}');

For PHP

rawurlencode('https://website.com/custom?payload={}');
urlencode('https://website.com/custom?payload={}');
rawurlencode('https://website.com/custom');
urlencode('?payload={}');

Handling Redirection (Website Part)

  • In app, vaah flutter can handle query parameter payload with below properties
payload = {
  "path": "/",
  "data": {},
  "auth": null
};
  • In Website we check if device is mobile or not.
  • If Mobile
    • we create one query parameter payload like shown above, add it to the end point. e.g. https://website.com/custom?payload={"path":null,"data":null,"auth":null}
    • And then encode that link. e.g. hhttps://website.com/custom%3Fpayload%3D%7B%22path%22%3Anull%2C%22data%22%3Anull %2C%22auth%22%3Anull%7D
    • We put this encoded part in link parameter of dynamic link we created and redirect user to that link. e.g. https://yourapp.page.link/?link=hhttps://website.com/custom%3Fpayload%3D%7B%22path%22%3Anull%2C%22data%22%3 Anull%2C%22auth%22%3Anull%7D&apn=com.example.app&ibi=com.example.app&isi=123456789
  • If not Mobile: we show website content.

Handling '/custom' path in PHP.

<?php
$useragent=$_SERVER['HTTP_USER_AGENT'];
if(!preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i',$useragent)||preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i',substr($useragent,0,4))){
    echo "Hello World";
}

else{
    $payload = [
        "path" => "/",
        "auth" => null,
        "data" => [
            "id" => "test"
        ]
    ];
    $payload = json_encode($payload);
    
    $deeplink = urlencode("https://website.com/custom/?payload=".rawurlencode($payload));
    $dynamiclink = "https://yourapp.page.link/?apn=com.example.app&ibi=com.example.app&isi=123456789&link=".$deeplink;
    
    header("Location: ".$dynamiclink);
}
?>

Handling '/custom' path in Javascript.

router.get('/custom', function (req, res, next) {
  if (devicetype.isDevicePhone(req.headers['user-agent'])) {
    const endPoint = 'https://website.com/custom';
    const dynamicLink = 'https://yourapp.page.link/?apn=com.example.app&ibi=com.example.app&isi=123456789&link=';

    const payload = {
      "path": "/ui-page",
      "data": {},
      "auth": null
    };
    const parameters = `?payload=${JSON.stringify(payload)}`;
    const encodedParameters = encodeURIComponent(parameters);
    const deeplink = encodeURI(endPoint + encodedParameters);
    const url = `${dynamicLink}${deeplink}`;

    res.redirect(url);
  }
  else {
    res.render('error', { page: 'Error', menuId: 'link', status: 404, message: 'App Not Installed!' });
  }
});

Function to check device type

const isDevicePhone = (agent) => {
    if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(agent) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(agent.substr(0, 4))) {
        return true;
    }
    return false;
}

exports.isDevicePhone = isDevicePhone;
  • Initialize firebase with the app. if setup is not done check this, and pass FirebaseConfig parameter to base controller, if you're using not whole code from project then initialize and configure firebase app by your self.
  • Initialize dynamic links from VaahExtendFlutter Service to catch initial links (when app is close), and subscribe to the links which are coming (when app is open). If you're using whole code from project you don't have to do anything expect initializing base controller.
class BaseController extends GetxController {
  Future<void> init({
    FirebaseOptions? firebaseOptions,
    required Widget app,
  }) async {
    if (firebaseOptions != null) {
      await Firebase.initializeApp(
        options: firebaseOptions,
      );
      DynamicLinks.init();
    }
...
  • VaahExtendFlutter Service -> Dynamic Links will handle links with payload data. example of payload
const payload = {
  "path": null,
  "data": null,
  "auth": null
};
  • Auth is used to pass data related to auth. e.g. x-csrf token, barer token, etc. Thus it depends on project and developer can configure how to handle auth parameter in app by doing some changes in dynamic_links.dart file. for now auth is passed to the route along with data so dev can handle it there for specific routes.
  • If path is not null then we redirect user to that route with the data. Routes will have to handle data by themselves.
  • dart code to handle the dynamic link

We need to decode the full link in handle link function, as we are passing encoded link from website.

static Future<void> _handleLink(PendingDynamicLinkData linkData) async {
  try {
    final Uri decodedLink = Uri.parse(Uri.decodeFull(linkData.link.toString()));
    final dynamic payload = _decodePayload(decodedLink);
    _dynamicLinksStreamController.add(
      DeepLink(
        encoded: linkData.link.toString(),
        decoded: "${linkData.link.host}${linkData.link.path}?payload=$payload",
      ),
    );
    Log.success({
      "encoded": linkData.link.toString(),
      "decoded": "${linkData.link.host}${linkData.link.path}?payload=$payload",
    });
    if (payload != null && payload['path'] != null) {
      Get.offAllNamed(
        payload['path'],
        arguments: <String, dynamic>{
          'data': payload['data'],
          'auth': payload['auth'],
        },
      );
    }
  } catch (error, stackTrace) {
    Log.exception(
      error,
      stackTrace: stackTrace,
      hint: "Error handling dynamic link! ${linkData.asMap()}",
    );
  }
}

static dynamic _decodePayload(Uri link) {
  try {
    return jsonDecode(link.queryParameters['payload'].toString());
  } catch (error, stackTrace) {
    Log.exception(
      error,
      stackTrace: stackTrace,
      hint: "Error decoding payload! $link",
    );
    return null;
  }
}

Source Code

import 'dart:async';
import 'dart:convert';

import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';
import 'package:get/get.dart';

import './logging_library/logging_library.dart';

abstract class DynamicLinks {
  static void init() async {
    // handle initial dynamic link
    final getInitialLink = await _firebaseDynamicLinks.getInitialLink();
    if (getInitialLink != null) {
      _handleLink(getInitialLink);
    }

    // listen/ subscribe to the links which comes later and handle them
    _firebaseDynamicLinks.onLink
        .listen(
          _handleLink,
        )
        .onError(
          (error, stackTrace) => Log.exception(error, stackTrace: stackTrace),
        );
  }

  static final FirebaseDynamicLinks _firebaseDynamicLinks = FirebaseDynamicLinks.instance;
  static final StreamController<DeepLink> _dynamicLinksStreamController =
      StreamController<DeepLink>.broadcast();
  static final Stream<DeepLink> dynamicLinksStream = _dynamicLinksStreamController.stream;

  static Future<ShortDynamicLink?> createLink({
    required String? path,
    required dynamic data,
    required dynamic auth,
  }) async {
    try {
      final String parameters = jsonEncode({"path": path, "data": data, "auth": auth});
      return await _firebaseDynamicLinks.buildShortLink(
        DynamicLinkParameters(
          link: Uri.parse("https://your.domain?payload=$parameters"),
          uriPrefix: "https://YOUR_FIREBASE_APP_DYNAMIC_LINK_PREFIX.page.link",
          androidParameters: const AndroidParameters(packageName: "your.package.name"),
          iosParameters: const IOSParameters(bundleId: "your.bundle.identifier"),
        ),
        shortLinkType: ShortDynamicLinkType.unguessable,
      );
    } catch (error, stackTrace) {
      Log.exception(error, stackTrace: stackTrace, hint: "Error creating dynamic link!");
      return null;
    }
  }

  static Future<void> _handleLink(PendingDynamicLinkData linkData) async {
    try {
      final Uri decodedLink = Uri.parse(Uri.decodeFull(linkData.link.toString()));
      final dynamic payload = _decodePayload(decodedLink);
      _dynamicLinksStreamController.add(
        DeepLink(
          encoded: linkData.link.toString(),
          decoded: "${linkData.link.host}${linkData.link.path}?payload=$payload",
        ),
      );
      Log.success({
        "encoded": linkData.link.toString(),
        "decoded": "${linkData.link.host}${linkData.link.path}?payload=$payload",
      });
      if (payload != null && payload['path'] != null) {
        Get.offAllNamed(
          payload['path'],
          arguments: <String, dynamic>{
            'data': payload['data'],
            'auth': payload['auth'],
          },
        );
      }
    } catch (error, stackTrace) {
      Log.exception(
        error,
        stackTrace: stackTrace,
        hint: "Error handling dynamic link! ${linkData.asMap()}",
      );
    }
  }

  static dynamic _decodePayload(Uri link) {
    try {
      return jsonDecode(link.queryParameters['payload'].toString());
    } catch (error, stackTrace) {
      Log.exception(
        error,
        stackTrace: stackTrace,
        hint: "Error decoding payload! $link",
      );
      return null;
    }
  }
}

class DeepLink {
  final String encoded;
  final String decoded;

  const DeepLink({
    required this.encoded,
    required this.decoded,
  });
}