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.

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.

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

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

Method 1: Direct Story Launch (Recommended)

For shared story 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);

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)