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:
Option 1: StorifyMe-Hosted Deep Links (Recommended)
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
- Log in to your StorifyMe dashboard
- Navigate to Settings → Mobile
- 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
- 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: Platform Configuration
iOS Setup
Configure Associated Domains in Xcode:
- Open your iOS project in Xcode:
ios/YourApp.xcodeproj
- Select your app target
- Go to Signing & Capabilities
- Click + Capability and add Associated Domains
- Add the following domains:
applinks:storifyme.xyz
applinks:storifyme.com
Update Native Code:
Open ios/YourApp/AppDelegate.mm
(or AppDelegate.m
) and add:
#import <React/RCTLinkingManager.h>
// Add this method to handle universal links
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
Android Setup
Get SHA-256 Fingerprints:
# Debug keystore
cd android
./gradlew signingReport
# Look for SHA-256 under "Variant: debug"
# Or using keytool
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
# Release keystore
keytool -list -v -keystore android/app/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:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<!-- 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.
Step 3: Handle Deep Links in React Native
The deep link URL format is:
https://storifyme.xyz/stories/{ACCOUNT_ID}/{story_handle}
https://storifyme.xyz/shorts/{ACCOUNT_ID}/{story_handle}
Basic Implementation:
import React, { useEffect } from 'react';
import { Linking } from 'react-native';
import StorifyMe from 'react-native-storifyme';
const App = () => {
useEffect(() => {
// Handle deep links when app is already running
const handleUrl = (event: { url: string }) => {
handleDeepLink(event.url);
};
const subscription = Linking.addEventListener('url', handleUrl);
// Handle deep links when app is launched from link
Linking.getInitialURL().then((url) => {
if (url) {
handleDeepLink(url);
}
});
return () => {
subscription?.remove();
};
}, []);
const handleDeepLink = (url: string) => {
try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol === 'https:') {
const handle = extractStoryHandle(parsedUrl);
if (handle) {
// Launch story directly
StorifyMe.openStoryByHandle(handle);
}
}
} catch (error) {
console.warn('Invalid deep link URL:', url);
}
};
const extractStoryHandle = (url: URL): string | null => {
const pathSegments = url.pathname.split('/').filter(segment => segment);
// For StorifyMe-hosted URLs: https://storifyme.xyz/stories/{ACCOUNT_ID}/{story_handle}
if (pathSegments.length >= 3 &&
(pathSegments[0] === 'stories' || pathSegments[0] === 'shorts')) {
return pathSegments[2];
}
return null;
};
return (
<YourAppContent />
);
};
export default App;
Step 4: Testing StorifyMe-Hosted Deep Links
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 IDcom.yourcompany.app
with your app's package name
Physical Device Testing:
- Send yourself a message with a story link
- Tap the link on your device
- The app should open and display the story
Troubleshooting StorifyMe-Hosted Links
iOS Issues:
- Verify Associated Domains are correctly configured in Xcode
- Rebuild and reinstall the app
- 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 - 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
Option 2: Custom Domain Deep Links (Advanced)
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.
Installation
Install the required packages:
npm install @react-native-async-storage/async-storage react-native-bootsplash
# For React Navigation (if using)
npm install @react-navigation/native @react-navigation/native-stack react-native-screens
For linking functionality, you can use React Navigation's built-in linking or implement your own solution.
Platform Configuration for Custom Domain
Android Configuration
Update AndroidManifest.xml
Add an intent filter to your Android manifest:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop">
<!-- 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
:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.your.package",
"sha256_cert_fingerprints": [
"YOUR_PACKAGE_FINGERPRINT"
]
}
}
]
iOS Configuration
Add Associated Domain:
In ios/YourApp/YourApp.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
:
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.your.package"],
"paths": ["*"]
}
]
}
}
Implementation for Custom Domain
Method 1: Direct Story Launch (Recommended)
For custom domain deep links, use direct story opening without depending on widget loading:
import React, { useEffect } from 'react';
import { Linking } from 'react-native';
import StorifyMe from 'react-native-storifyme';
const App = () => {
useEffect(() => {
// Handle deep links when app is already running
const handleUrl = (event: { url: string }) => {
handleDeepLink(event.url);
};
const subscription = Linking.addEventListener('url', handleUrl);
// Handle deep links when app is launched from link
Linking.getInitialURL().then((url) => {
if (url) {
handleDeepLink(url);
}
});
return () => {
subscription?.remove();
};
}, []);
const handleDeepLink = (url: string) => {
try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol === 'https:') {
const handle = extractStoryHandle(parsedUrl);
if (handle) {
// Launch story directly
StorifyMe.openStoryByHandle(handle);
}
}
} catch (error) {
console.warn('Invalid deep link URL:', url);
}
};
const extractStoryHandle = (url: URL): string | null => {
const pathSegments = url.pathname.split('/').filter(segment => segment);
// 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;
};
return (
<YourAppContent />
);
};
export default App;
Method 2: Widget-Based Launch
If you need to open a story within a loaded widget context:
import React, { useState, useEffect, useRef } from 'react';
import { View } from 'react-native';
import StorifyMe, { StorifyMeWidget } from 'react-native-storifyme';
interface StoriesScreenProps {
pendingHandle?: string;
}
const StoriesScreen: React.FC<StoriesScreenProps> = ({ pendingHandle }) => {
const [widgetLoaded, setWidgetLoaded] = useState(false);
const widgetRef = useRef<StorifyMeWidget>(null);
const pendingHandleRef = useRef<string | undefined>(pendingHandle);
useEffect(() => {
pendingHandleRef.current = pendingHandle;
}, [pendingHandle]);
const handleWidgetLoad = (stories: any[]) => {
setWidgetLoaded(true);
// If we have a pending handle, open it now
if (pendingHandleRef.current) {
openStoryInWidget(pendingHandleRef.current);
pendingHandleRef.current = undefined;
}
};
const handleWidgetFail = (error: string) => {
console.error('Widget failed to load:', error);
// If widget fails to load but we have a handle, try direct launch
if (pendingHandleRef.current) {
StorifyMe.openStoryByHandle(pendingHandleRef.current);
pendingHandleRef.current = undefined;
}
};
const openStoryInWidget = (handle: string) => {
widgetRef.current?.openStoryByHandle(handle, (result) => {
if (!result.success) {
// Story not found in widget, fall back to direct launch
StorifyMe.openStoryByHandle(handle);
}
});
};
const handleDeepLink = (handle: string) => {
if (widgetLoaded) {
// Widget is already loaded, open immediately
openStoryInWidget(handle);
} else {
// Store handle to open after widget loads
pendingHandleRef.current = handle;
}
};
return (
<View style={{ flex: 1 }}>
<StorifyMeWidget
ref={widgetRef}
widgetId="YOUR_WIDGET_ID"
onLoad={handleWidgetLoad}
onFail={handleWidgetFail}
style={{ flex: 1 }}
/>
</View>
);
};
export default StoriesScreen;
Using React Navigation
If you're using React Navigation, you can handle deep links through the navigation configuration:
import { LinkingOptions } from '@react-navigation/native';
import StorifyMe from 'react-native-storifyme';
const linking: LinkingOptions<RootStackParamList> = {
prefixes: ['https://YOUR_PROXY_DOMAIN'],
config: {
screens: {
Home: '',
Story: {
path: '/story/:handle',
parse: {
handle: (handle: string) => {
// Open story directly when navigating to story route
StorifyMe.openStoryByHandle(handle);
return handle;
},
},
},
// Handle direct story handle URLs
DirectStory: {
path: '/:handle',
parse: {
handle: (handle: string) => {
StorifyMe.openStoryByHandle(handle);
return handle;
},
},
},
},
},
};
export default linking;
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import linking from './navigation/LinkingConfiguration';
const Stack = createNativeStackNavigator();
const App = () => {
return (
<NavigationContainer linking={linking}>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Story" component={StoryScreen} />
</Stack.Navigator>
</NavigationContainer>
);
};
export default App;
Using Redux for State Management
If you're using Redux, you can handle deep links through actions:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface DeepLinkState {
pendingHandle?: string;
isProcessing: boolean;
}
const initialState: DeepLinkState = {
isProcessing: false,
};
const deepLinkSlice = createSlice({
name: 'deepLink',
initialState,
reducers: {
setPendingHandle: (state, action: PayloadAction<string>) => {
state.pendingHandle = action.payload;
},
clearPendingHandle: (state) => {
state.pendingHandle = undefined;
},
setProcessing: (state, action: PayloadAction<boolean>) => {
state.isProcessing = action.payload;
},
},
});
export const { setPendingHandle, clearPendingHandle, setProcessing } = deepLinkSlice.actions;
export default deepLinkSlice.reducer;
import { useEffect } from 'react';
import { Linking } from 'react-native';
import { useDispatch } from 'react-redux';
import StorifyMe from 'react-native-storifyme';
import { setPendingHandle } from '../store/deepLinkSlice';
export const useDeepLink = () => {
const dispatch = useDispatch();
useEffect(() => {
const handleUrl = (event: { url: string }) => {
processDeepLink(event.url);
};
const subscription = Linking.addEventListener('url', handleUrl);
Linking.getInitialURL().then((url) => {
if (url) {
processDeepLink(url);
}
});
return () => {
subscription?.remove();
};
}, []);
const processDeepLink = (url: string) => {
try {
const parsedUrl = new URL(url);
const handle = extractStoryHandle(parsedUrl);
if (handle) {
// Try direct launch first
StorifyMe.openStoryByHandle(handle);
// Also store in Redux for widget-based fallback
dispatch(setPendingHandle(handle));
}
} catch (error) {
console.warn('Invalid deep link URL:', url);
}
};
const extractStoryHandle = (url: URL): string | null => {
const pathSegments = url.pathname.split('/').filter(segment => segment);
switch (pathSegments.length) {
case 1:
return pathSegments[0];
case 2:
if (pathSegments[0] === 'story' || pathSegments[0] === 'stories') {
return pathSegments[1];
}
break;
default:
return null;
}
return null;
};
};
Testing Deep Links
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
- Use Safari in the iOS Simulator
- Navigate to your deep link URL
- Tap the link to trigger your app
Development Testing
You can simulate deep links during development:
// Add this to your development tools
const simulateDeepLink = (handle: string) => {
const testUrl = `https://YOUR_PROXY_DOMAIN/${handle}`;
Linking.openURL(testUrl);
};
URL Pattern Examples
Your implementation should handle various URL patterns:
URL Pattern | Handle Extraction |
---|---|
https://domain.com/story-handle | First path segment |
https://domain.com/story/story-handle | Second path segment |
https://domain.com/stories/story-handle | Second path segment |
Best Practices
- Use Direct Launch for Shared Content: Direct story opening is more reliable for shared links
- Implement Fallback Logic: If a story isn't found in a widget, fall back to direct launch
- Handle App States: Consider cold start vs. app already running scenarios
- Error Handling: Handle invalid or expired story handles gracefully
- Performance: Avoid blocking the main thread when processing deep links
- User Experience: Show loading states while processing deep links
TypeScript Types
export interface DeepLinkResult {
success: boolean;
handle?: string;
error?: string;
}
export interface StoryHandle {
handle: string;
widgetId?: string;
}
export type DeepLinkHandler = (url: string) => Promise<DeepLinkResult>;
Common Issues
Deep Link Not Working
- Verify platform-specific configuration files are properly set up
- Check that your proxy domain matches exactly in all configurations
- Test with different URL patterns
Story Not Opening
- Ensure the story handle exists and is published
- Check if direct launch method is available in your StorifyMe version
- Implement fallback to widget-based opening
Navigation Issues
- Consider how deep links affect your navigation stack
- Handle edge cases like multiple rapid deep link triggers
- Test with different app states (foreground, background, closed)