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: Platform Configuration​

iOS Setup​

Configure Associated Domains in Xcode:

  1. Open your iOS project in Xcode: ios/YourApp.xcodeproj

  2. Select your app target

  3. Go to Signing & Capabilities

  4. Click + Capability and add Associated Domains

  5. 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:

android/app/src/main/AndroidManifest.xml
<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.

The deep link URL format is:

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

Basic Implementation:

App.tsx
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;

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
  • 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

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.

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:

android/app/src/main/AndroidManifest.xml
<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:

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:

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:

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:

App.tsx
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:

StoriesScreen.tsx
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:

navigation/LinkingConfiguration.ts
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;
App.tsx
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:

store/deepLinkSlice.ts
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;
hooks/useDeepLink.ts
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;
};
};

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

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 PatternHandle Extraction
https://domain.com/story-handleFirst path segment
https://domain.com/story/story-handleSecond path segment
https://domain.com/stories/story-handleSecond path segment

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 States: Consider cold start vs. app already running scenarios
  4. Error Handling: Handle invalid or expired story handles gracefully
  5. Performance: Avoid blocking the main thread when processing deep links
  6. User Experience: Show loading states while processing deep links

TypeScript Types​

types/deepLink.ts
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​

  • 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
  • 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)