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