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)