Deeplinks

Deep Linking with ContentLinkClick

Stories authored in JOIN Studio can include “Link” actions. When a user taps such content, the Flutter plugin triggers a ContentLinkClick callback with the link you configured. Use it to route users to in-app destinations (deep links) or to external pages.

  • Event name: ContentLinkClick
  • Signature (widgets): onContentLinkClick(String link)
  • Signature (standalone): onContentLinkClick(String link)
  • Payload: link (absolute URL or app-specific deep link)
  • When fired: user taps a linkable element inside a story configured in JOIN Studio

Recommended handling

  • Validate and route: parse the link and decide if it maps to an in-app route (deep link) or should open externally.
  • Dismiss before navigating: call JOINStories.dismissPlayer() to avoid UI overlap.
  • Allowlist schemes/domains: only handle trusted schemes (e.g., myapp://) and trusted web domains.

Automatic handling of web links (HTTP/HTTPS)

  • Behavior: When a story link is a web URL (http/https), the native SDK automatically opens it in the device browser (e.g., Safari/Chrome). This happens by default; you don’t need to implement anything.
  • Callback still fires: The onContentLinkClick(String link) callback is emitted for observability (e.g., analytics), but it does not intercept or prevent the browser from opening.
  • Do not double-navigate: For web URLs, avoid also navigating in your app (e.g., pushing a WebView) to prevent duplicate/competing navigation.
  • Overriding this behavior: The Flutter plugin does not currently expose a way to stop the automatic browser open. If you want to handle links in-app:
    • Prefer a custom app scheme (e.g., myapp://...) in JOIN Studio and route it in your app’s deep-link handler.
    • Or map “friendly” web links to a custom scheme redirect service you control, then handle the custom scheme internally.

Notes

  • Custom schemes: If the link uses your app scheme (e.g., myapp://product/123), route it inside your app via onContentLinkClick and/or your app’s deep-link handler. You can optionally call await JOINStories.dismissPlayer() before navigating.
  • Analytics: Use onAnalyticsEvent or your own logging inside onContentLinkClick to attribute link clicks, even for web URLs that auto-open.

Usage with widgets (BubbleWidget / CardWidget)

BubbleWidget(
  alias: 'homepage-stories',
  onContentLinkClick: (link) async {
    // 1) Optional: close the player if it’s visible
    await JOINStories.dismissPlayer();

    // 2) Route deep link
    if (link.startsWith('myapp://product/')) {
      final productId = link.split('myapp://product/').last;
      Navigator.of(context).pushNamed('/product/$productId');
      return;
    }
    if (link.startsWith('https://shop.example.com')) {
      Navigator.of(context).pushNamed('/web', arguments: {'url': link});
      return;
    }
  },
);

The same callback is available on CardWidget:

CardWidget(
  alias: 'recommendations',
  isGrid: true,
  onContentLinkClick: (link) async {
    await JOINStories.dismissPlayer();
    Navigator.of(context).pushNamed('/deeplink', arguments: link);
  },
);

Usage with the Standalone Player

await JOINStories.startPlayer(
  'homepage-stories',
  onContentLinkClick: (link) async {
    await JOINStories.dismissPlayer();

    if (link.startsWith('myapp://')) {
      // Route custom scheme internally
      Navigator.of(context).pushNamed('/deeplink', arguments: link);
    } else if (link.startsWith('https://trusted.example')) {
      Navigator.of(context).pushNamed('/web', arguments: {'url': link});
    } else {
      // Fallback
    }
  },
  // Optional: observe analytics events as well
  onAnalyticsEvent: (eventName, payload) {
    // analytics.log('join_$eventName', payload);
  },
);

Launching a story from your app deep link (Standalone)

You can open the JOIN standalone player directly when your app receives a deep link (cold start or while running). Define a link format (for example, myapp://stories/) and route it to JOINStories.startPlayer.

  • Example link formats:

  • Minimal deep link router:

    Future<void> handleIncomingLink(Uri uri) async {
      // Expect: myapp://stories/<alias> or https://yourdomain.com/stories/<alias>
      final isStoriesPath = uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'stories';
      if (isStoriesPath && uri.pathSegments.length >= 2) {
        final alias = uri.pathSegments[1];
    
        // Optional UI customization from query params
        final origin = uri.queryParameters['origin']; // e.g., 'topRight'
        final bg = uri.queryParameters['bgColor']; // e.g., '0xFF2C3E50'
    
        await JOINStories.startPlayer(
          alias,
          standaloneOrigin: origin,
          playerBackgroundColor: bg != null ? int.tryParse(bg) : null,
          // Add other player args if you expose them via query params
          onContentLinkClick: (link) async {
            await JOINStories.dismissPlayer();
            // Route nested links from within the player if needed
            Navigator.of(globalNavigatorKey.currentContext!).pushNamed('/deeplink', arguments: link);
          },
        );
        return;
      }
    
      // Otherwise, handle other app links
    }
    

Best practices

  • Keep a single deep-link router in your app to centralize parsing/validation.

  • Normalize product/category links to stable in-app routes.

  • Log routing decisions to your analytics for debugging and attribution.