Skip to main content

Deep Linking

Deep linking allows users to open specific StorifyMe stories directly from shared links, providing a seamless experience when content is shared across platforms.

StorifyMe provides two approaches for implementing deep links:

StorifyMe automatically generates and hosts deep linking configuration files for both platforms. This is the simplest approach and requires no backend infrastructure on your end.

Step 1: Configure App Credentials in Dashboard

  1. Log in to your StorifyMe dashboard
  2. Navigate to Settings → Mobile
  3. Configure both platforms:

iOS Configuration:

  • Team ID: Your Apple Developer Team ID
  • Bundle ID: Your app's bundle identifier (e.g., com.yourcompany.app)
  • App Store ID: Your app's Apple App Store ID

Android Configuration:

  • Package Name: Your app's package name (e.g., com.yourcompany.app)
  • SHA-256 Fingerprints: Your app signing certificate fingerprints
  1. Click Save

StorifyMe will automatically generate the configuration files at:

  • iOS: https://storifyme.xyz/.well-known/apple-app-site-association
  • Android: https://storifyme.xyz/.well-known/assetlinks.json

Step 2: Add Dependencies

Add the required packages to your pubspec.yaml:

pubspec.yaml
dependencies:
flutter:
sdk: flutter
storifyme: ^latest_version
uni_links: ^0.5.1 # For handling deep links

# Alternative: Use app_links for newer approach
# app_links: ^3.4.0

Run:

flutter pub get

Step 3: iOS Setup

Configure Associated Domains in Xcode:

  1. Open your iOS project in Xcode: ios/Runner.xcworkspace
  2. Select the Runner target
  3. Go to Signing & Capabilities
  4. Click + Capability and add Associated Domains
  5. Add the following domains:
    applinks:storifyme.xyz
    applinks:storifyme.com

Step 4: Android Setup

Get SHA-256 Fingerprints:

# Debug keystore
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

# Release keystore
keytool -list -v -keystore /path/to/your-release-key.keystore -alias your-key-alias

# Play App Signing (from Play Console)
# Go to: Your App → Setup → App integrity → App signing key certificate

Add ALL fingerprints to the StorifyMe dashboard.

Update AndroidManifest.xml:

Open android/app/src/main/AndroidManifest.xml and add intent filters:

android/app/src/main/AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme">

<!-- Your existing intent filters -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!-- StorifyMe Deep Links - storifyme.xyz -->
<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="storifyme.xyz"
android:pathPrefix="/stories/YOUR_ACCOUNT_ID/" />
</intent-filter>

<!-- StorifyMe Deep Links - storifyme.com -->
<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="storifyme.com"
android:pathPrefix="/stories/YOUR_ACCOUNT_ID/" />
</intent-filter>

<!-- Support for /shorts/ path -->
<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="storifyme.xyz"
android:pathPrefix="/shorts/YOUR_ACCOUNT_ID/" />
</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="storifyme.com"
android:pathPrefix="/shorts/YOUR_ACCOUNT_ID/" />
</intent-filter>

</activity>

Important: Replace YOUR_ACCOUNT_ID with your actual StorifyMe account ID.

The deep link URL format is:

https://storifyme.xyz/stories/{ACCOUNT_ID}/{story_handle}
https://storifyme.xyz/shorts/{ACCOUNT_ID}/{story_handle}

Create a deep link handler service:

lib/services/deep_link_handler.dart
import 'dart:async';
import 'package:uni_links/uni_links.dart';
import 'package:storifyme_flutter/storifyme_flutter.dart';

class DeepLinkHandler {
StreamSubscription? _sub;

Future<void> init() async {
// Handle initial link (app opened from link)
try {
final initialLink = await getInitialLink();
if (initialLink != null) {
_handleDeepLink(initialLink);
}
} catch (e) {
print('Failed to get initial link: $e');
}

// Handle links when app is already running
_sub = linkStream.listen((String? link) {
if (link != null) {
_handleDeepLink(link);
}
}, onError: (err) {
print('Failed to handle link: $err');
});
}

void _handleDeepLink(String url) {
// Parse the deep link URL
// Format: https://storifyme.xyz/stories/{ACCOUNT_ID}/{story_handle}
try {
final uri = Uri.parse(url);
final pathSegments = uri.pathSegments;

// For StorifyMe-hosted URLs
if (pathSegments.length >= 3 &&
(pathSegments[0] == 'stories' || pathSegments[0] == 'shorts')) {
final storyHandle = pathSegments[2];

// Open the story using StorifyMe SDK
_openStory(storyHandle);
}
} catch (e) {
print('Failed to parse deep link: $e');
}
}

void _openStory(String storyHandle) {
// Open story using StorifyMe SDK
StorifyMeFlutter.openStory(storyHandle);
}

void dispose() {
_sub?.cancel();
}
}

Add the deep link handler to your main app:

lib/main.dart
import 'package:flutter/material.dart';
import 'package:storifyme_flutter/storifyme_flutter.dart';
import 'services/deep_link_handler.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatefulWidget {

_MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
final DeepLinkHandler _deepLinkHandler = DeepLinkHandler();


void initState() {
super.initState();

// Initialize StorifyMe SDK
StorifyMeFlutter.initialize(
accountId: 'YOUR_ACCOUNT_ID',
apiKey: 'YOUR_API_KEY',
);

// Initialize deep link handler
_deepLinkHandler.init();
}


void dispose() {
_deepLinkHandler.dispose();
super.dispose();
}


Widget build(BuildContext context) {
return MaterialApp(
title: 'StorifyMe Demo',
home: HomeScreen(),
);
}
}

iOS Testing (Simulator):

# Test with storifyme.xyz
xcrun simctl openurl booted "https://storifyme.xyz/stories/YOUR_ACCOUNT_ID/test-story"

# Test with storifyme.com
xcrun simctl openurl booted "https://storifyme.com/stories/YOUR_ACCOUNT_ID/test-story"

Android Testing (ADB):

# Test with storifyme.xyz
adb shell am start \
-W -a android.intent.action.VIEW \
-d "https://storifyme.xyz/stories/YOUR_ACCOUNT_ID/test-story" \
com.yourcompany.app

# Test with storifyme.com
adb shell am start \
-W -a android.intent.action.VIEW \
-d "https://storifyme.com/stories/YOUR_ACCOUNT_ID/test-story" \
com.yourcompany.app

Replace:

  • YOUR_ACCOUNT_ID with your StorifyMe account ID
  • com.yourcompany.app with your app's package name

Physical Device Testing:

  1. Send yourself a message with a story link
  2. Tap the link on your device
  3. The app should open and display the story

iOS Issues:

  • Verify Associated Domains are correctly configured in Xcode
  • Clean and rebuild: flutter clean && flutter pub get && flutter run
  • Test on a physical device
  • Check AASA file: curl https://storifyme.xyz/.well-known/apple-app-site-association

Android Issues:

  • Verify android:autoVerify="true" is set in intent filters
  • Check path prefix includes your ACCOUNT_ID
  • Verify SHA-256 fingerprints in dashboard
  • Clear app defaults: Settings → Apps → Your App → Open by default → Clear defaults
  • Check verification: adb shell pm get-app-links com.yourcompany.app

Before you begin

Make sure you have configured your sharing domain in the StorifyMe Dashboard Settings. Set either Custom Domain OR Proxy - this domain will be used when links are copied/shared from your stories.

If you want to use your own domain for deep links, you'll need to host the configuration files on your own server.

1. Add Dependencies

Add the required packages to your pubspec.yaml:

pubspec.yaml
dependencies:
flutter:
sdk: flutter
storifyme: ^latest_version
app_links: ^3.4.0 # For handling deep links

2. Android Configuration for Custom Domain

Update AndroidManifest.xml:

Add an intent filter to your Android manifest:

android/app/src/main/AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme">

<!-- Your existing intent filters -->

<!-- Deep link 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_PROXY_DOMAIN" />
</intent-filter>
</activity>

Add assetlinks.json:

Create an assetlinks.json file at https://YOUR_PROXY_DOMAIN/.well-known/assetlinks.json:

assetlinks.json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.your.package",
"sha256_cert_fingerprints": [
"YOUR_PACKAGE_FINGERPRINT"
]
}
}
]

3. iOS Configuration for Custom Domain

Add Associated Domain:

In ios/Runner/Runner.entitlements:

ios/Runner/Runner.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:YOUR_PROXY_DOMAIN</string>
</array>
</dict>
</plist>

Add apple-app-site-association:

Create an apple-app-site-association file at https://YOUR_PROXY_DOMAIN/.well-known/apple-app-site-association:

apple-app-site-association
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.your.package"],
"paths": ["*"]
}
]
}
}

4. Implementation for Custom Domain

Method 1: Direct Story Launch (Recommended)

For custom domain deep links, use direct story opening which doesn't depend on widget loading:

main.dart
import 'package:flutter/material.dart';
import 'package:storifyme/storifyme.dart';
import 'package:app_links/app_links.dart';

class MyApp extends StatefulWidget {

_MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
final _appLinks = AppLinks();


void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initDeepLinks();
}


void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

void _initDeepLinks() {
// Handle links when app is already running
_appLinks.uriLinkStream.listen((uri) {
_handleDeepLink(uri);
});

// Handle links when app is launched from link
_appLinks.getInitialAppLink().then((uri) {
if (uri != null) {
_handleDeepLink(uri);
}
});
}

void _handleDeepLink(Uri uri) {
if (uri.scheme == 'https') {
final handle = _extractStoryHandle(uri);
if (handle != null) {
// Launch story directly using StorifyMe
_openStoryByHandle(handle);
}
}
}

String? _extractStoryHandle(Uri uri) {
final pathSegments = uri.pathSegments;

if (pathSegments.isEmpty) return null;

// For custom domain URLs with various patterns
switch (pathSegments.length) {
case 1:
// For URLs like: https://domain.com/story-handle
return pathSegments[0];

case 2:
// For URLs like: https://domain.com/story/story-handle
if (pathSegments[0] == 'story' || pathSegments[0] == 'stories') {
return pathSegments[1];
}
break;

default:
return null;
}

return null;
}

void _openStoryByHandle(String handle) {
// Note: This method depends on your StorifyMe Flutter plugin implementation
// You may need to use the method provided by your plugin
StorifyMe.openStoryByHandle(handle);
}


Widget build(BuildContext context) {
return MaterialApp(
title: 'StorifyMe App',
home: HomePage(),
);
}
}

Method 2: Widget-Based Launch

If you need to open a story within a loaded widget context:

home_page.dart
import 'package:flutter/material.dart';
import 'package:storifyme/storifyme.dart';

class HomePage extends StatefulWidget {

_HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
late StorifyMeWidget _storifyMeWidget;
String? _pendingHandle;
bool _widgetLoaded = false;


void initState() {
super.initState();
_initStorifyMeWidget();
}

void _initStorifyMeWidget() {
_storifyMeWidget = StorifyMeWidget(
widgetId: 'YOUR_WIDGET_ID',
onLoad: (stories) {
setState(() {
_widgetLoaded = true;
});

// If we have a pending handle, open it now
if (_pendingHandle != null) {
_openStoryInWidget(_pendingHandle!);
_pendingHandle = null;
}
},
onFail: (error) {
// If widget fails to load but we have a handle, try direct launch
if (_pendingHandle != null) {
StorifyMe.openStoryByHandle(_pendingHandle!);
_pendingHandle = null;
}
},
);
}

void handleDeepLink(String handle) {
if (_widgetLoaded) {
// Widget is already loaded, open immediately
_openStoryInWidget(handle);
} else {
// Store handle to open after widget loads
_pendingHandle = handle;
}
}

void _openStoryInWidget(String handle) {
_storifyMeWidget.openStoryByHandle(handle, (result) {
if (!result.success) {
// Story not found in widget, fall back to direct launch
StorifyMe.openStoryByHandle(handle);
}
});
}


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('StorifyMe Stories'),
),
body: Column(
children: [
// Your other widgets
Expanded(
child: _storifyMeWidget,
),
],
),
);
}
}

Using GetX Navigation

If you're using GetX for navigation, you can handle deep links in your route management:

app_routes.dart
import 'package:get/get.dart';
import 'package:storifyme/storifyme.dart';

class AppRoutes {
static String home = '/';
static String story = '/story/:handle';

static List<GetPage> routes = [
GetPage(
name: home,
page: () => HomePage(),
),
GetPage(
name: story,
page: () {
final handle = Get.parameters['handle'];
if (handle != null) {
// Open story directly
StorifyMe.openStoryByHandle(handle);
}
return HomePage(); // Return to home after opening story
},
),
];
}

Using go_router

If you're using go_router for navigation:

router.dart
import 'package:go_router/go_router.dart';
import 'package:storifyme/storifyme.dart';

final GoRouter router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
GoRoute(
path: '/story/:handle',
builder: (context, state) {
final handle = state.params['handle'];
if (handle != null) {
// Open story and navigate back to home
WidgetsBinding.instance.addPostFrameCallback((_) {
StorifyMe.openStoryByHandle(handle);
context.go('/');
});
}
return HomePage();
},
),
],
);

Android Testing (Using ADB):

adb shell am start \
-W -a android.intent.action.VIEW \
-d "https://YOUR_PROXY_DOMAIN/your-story-handle" \
com.your.package

iOS Testing:

  1. Use Safari in the iOS Simulator
  2. Navigate to your deep link URL
  3. Tap the link to trigger your app

Flutter Testing:

You can test deep links in development using:

// Simulate receiving a deep link during development
void _simulateDeepLink() {
final testUri = Uri.parse('https://YOUR_PROXY_DOMAIN/test-story-handle');
_handleDeepLink(testUri);
}

URL Pattern Examples

Your implementation should handle various URL patterns:

URL PatternHandle Extraction
https://domain.com/story-handlepathSegments[0]
https://domain.com/story/story-handlepathSegments[1]
https://domain.com/stories/story-handlepathSegments[1]

Best Practices

  1. Use Direct Launch for Shared Content: Direct story opening is more reliable for shared links
  2. Implement Fallback Logic: If a story isn't found in a widget, fall back to direct launch
  3. Handle App State: Consider whether your app is cold starting or already running
  4. Error Handling: Handle invalid or expired story handles gracefully
  5. User Experience: Show loading states while processing deep links
  6. Navigation: Decide how deep links should affect your app's navigation stack

Performance Considerations

  1. Lazy Loading: Only initialize deep link handling when needed
  2. Memory Management: Clean up listeners in dispose methods
  3. Background Processing: Handle deep links efficiently to avoid blocking the UI

Common Issues

  • Verify assetlinks.json is properly formatted and accessible
  • Check SHA256 fingerprint matches your app signing certificate
  • Ensure package name matches exactly
  • Verify apple-app-site-association file is accessible
  • Check Team ID and bundle identifier are correct
  • Use Apple's validation tool to test your association file

Plugin Method Not Found

  • Ensure you're using the correct method names from your StorifyMe Flutter plugin
  • Check plugin documentation for the exact API
  • Verify plugin version compatibility