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 callawait JOINStories.dismissPlayer()
before navigating. - Analytics: Use
onAnalyticsEvent
or your own logging insideonContentLinkClick
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:
- myapp://stories/homepage
- https://yourdomain.com/stories/homepage
-
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.
Updated 7 days ago