Adding Deep Linking Support
Deep links direct users to specific content within your app. When users tap a deep link on a device with your app installed, the app opens directly to the intended content, such as a product page or specific experience.
Singular tracking links support both standard deep linking (for installed apps) and deferred deep linking (for new installs). For comprehensive information, see the Deep Linking FAQ and Singular Links FAQ.
Implementation Paths: This guide covers both Bare React Native and Expo implementations. The distinction exists because Expo abstracts native code management to simplify React Native development, while bare React Native provides direct access to native iOS and Android folders. Choose the appropriate implementation path for your project structure.
Requirements
Prerequisites
Complete the Singular Links Prerequisites to enable deep linking for your app.
Notes:
- This article assumes your organization is using Singular Links - Singular's tracking link technology. Older customers may be using legacy tracking links.
- Your app's deep link destinations need to be configured on the Apps page in Singular (see Configuring Your App for Attribution Tracking).
Implement Singular Links Handler
The SingularLink handler provides a callback mechanism to retrieve deep link, deferred deep link, passthrough parameters and URL parameters from Singular tracking links when the app opens.
Available Parameters:
- Deep Link (_dl): The destination URL within your app for users clicking the link
- Deferred Deep Link (_ddl): The destination URL for users who install the app after clicking the link
- Passthrough (_p): Custom data passed through the tracking link for additional context
- urlParameters: Object of query parameters from a Singular Link that opened the app. Not applicable to Deferred Deep Link scenarios.
Bare React Native Implementation
Configure deep linking for bare React Native projects with direct access to native iOS and Android code.
iOS Platform Configuration
Update iOS AppDelegate
Enable the Singular SDK to process launch-related data and handle deep
links by passing the launchOptions and
userActivity objects to the Singular SDK in your
AppDelegate file.
import Singular
// Deferred Deep link and Short Link support
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
if let singularBridge = NSClassFromString("SingularBridge") {
let selector = NSSelectorFromString("startSessionWithLaunchOptions:")
if singularBridge.responds(to: selector) {
singularBridge.perform(selector, with: launchOptions, afterDelay: 1)
}
}
factory.startReactNative(
withModuleName: "AppName", // Update with your App Name
in: window,
launchOptions: launchOptions
)
return true
}
// Universal Links (NSUserActivity)
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let url = userActivity.webpageURL {
print("Singular: Universal link received:", url.absoluteString)
if let singularBridge = NSClassFromString("SingularBridge") {
let selector = NSSelectorFromString("startSessionWithUserActivity:")
if singularBridge.responds(to: selector) {
singularBridge.perform(selector, with: userActivity, afterDelay: 1)
}
}
}
return RCTLinkingManager.application(
application,
continue: userActivity,
restorationHandler: restorationHandler
)
}
// Top of the AppDelegate.mm
#import <React/RCTLinkingManager.h>
#import <Singular-React-Native/SingularBridge.h>
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Add inside didFinishLaunchingWithOptions
[SingularBridge startSessionWithLaunchOptions:launchOptions];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
[SingularBridge startSessionWithUserActivity:userActivity];
return [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
}
These methods ensure the Singular SDK receives critical information about how and why your app was launched, which Singular uses for attribution tracking and deep link navigation.
Android Platform Configuration
Update Android MainActivity
Enable the Singular SDK to process launch-related data and handle deep
links by modifying the MainActivity file to pass the
Intent object to the Singular SDK.
// Add as part of the imports at the top of the class
import android.content.Intent
import net.singular.react_native.SingularBridgeModule
// Add to the MainActivity class
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.data != null) {
setIntent(intent)
SingularBridgeModule.onNewIntent(intent)
}
}
// Add as part of the imports at the top of the class
import android.content.Intent;
import net.singular.react_native.SingularBridgeModule;
// Add to the MainActivity class
@Override
public void onNewIntent(Intent intent) {
if(intent.getData() != null) {
setIntent(intent);
super.onNewIntent(intent);
SingularBridgeModule.onNewIntent(intent);
}
}
The Intent object contains information about how and why
your app was launched, which Singular uses for attribution tracking and
deep link navigation.
SDK Configuration
Add withSingularLink Callback
Configure the withSingularLink callback to handle incoming
deep link and deferred deep link data during SDK initialization.
// TurboModule direct API (React Native 0.76+ New Architecture)
import React, { useEffect } from 'react';
import NativeSingular from 'singular-react-native/js/NativeSingular';
import { NativeEventEmitter } from 'react-native';
function App() {
useEffect(() => {
// Create config
const config: SingularConfig = {
apikey: 'YOUR_SDK_KEY',
secret: 'YOUR_SDK_SECRET'
// Note: Deep link handlers are NOT set in config for TurboModule
};
NativeSingular.init(config);
// Set up deep link handler using event emitter
const emitter = new NativeEventEmitter(NativeSingular);
const linkListener = emitter.addListener('SingularLinkHandler', (params) => {
console.log('Singular Link resolved:', params);
// Extract deep link parameters
console.log('Singular: Deep link received:', params.deeplink);
console.log('Singular: Passthrough data:', params.passthrough);
console.log('Singular: Is deferred:', params.isDeferred);
console.log('Singular: urlParameters:', params.urlParameters);
// Handle deep link navigation
});
// Clean up event listener on unmount
return () => {
linkListener.remove();
};
}, []);
return (
// Your app components
null
);
}
import React, { useEffect } from 'react';
import { Singular, SingularConfig } from 'singular-react-native';
function App() {
useEffect(() => {
const config = new SingularConfig(
'YOUR_SDK_KEY',
'YOUR_SDK_SECRET'
)
.withSingularLink((params) => {
console.log('Singular Link resolved:', params);
// Extract deep link parameters
console.log('Singular: Deep link received:', params.deeplink);
console.log('Singular: Passthrough data:', params.passthrough);
console.log('Singular: Is deferred:', params.isDeferred);
console.log('Singular: urlParameters:', params.urlParameters);
// Handle deep link navigation
});
// Initialize SDK
Singular.init(config);
}, []);
return (
// Your app components
);
}
Note: The withSingularLink callback
is triggered only when the app opens through a Singular Link. For
more information, see the
Singular Links FAQ.
For complete method documentation, see withSingularLink reference.
Expo Implementation
Configure deep linking for Expo projects using configuration files and custom plugins to handle native code modifications automatically.
App Configuration
Update App.json for Deep Linking
Due to Expo's abstraction from native code management, add platform-specific
deep linking configurations in the app.json file.
iOS Configuration
- Add the associatedDomains capability with your Singular tracking link domain
- Add the scheme for custom URL scheme support (fallback)
Android Configuration
-
Update the host value to match your Singular Links
domain (e.g.,
example.sng.link) - Configure the scheme in the last intent filter for traditional scheme link support
{
"expo": {
"name": "MySimpleApp",
"slug": "mysimpleapp",
"scheme": "ios-app-scheme",
"ios": {
"associatedDomains": [
"applinks:example.sng.link"
]
},
"android": {
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "http",
"host": "example.sng.link",
"pathPrefix": "/A"
},
{
"scheme": "https",
"host": "example.sng.link",
"pathPrefix": "/A"
},
{
"scheme": "http",
"host": "example.sng.link",
"pathPrefix": "/B"
},
{
"scheme": "https",
"host": "example.sng.link",
"pathPrefix": "/B"
},
{
"scheme": "http",
"host": "example.sng.link",
"pathPrefix": "/E"
},
{
"scheme": "https",
"host": "example.sng.link",
"pathPrefix": "/E"
},
{
"scheme": "http",
"host": "example.sng.link",
"pathPrefix": "/F"
},
{
"scheme": "https",
"host": "example.sng.link",
"pathPrefix": "/F"
}
],
"category": [
"BROWSABLE",
"DEFAULT"
]
},
{
"action": "VIEW",
"data": [
{
"scheme": "android-app-scheme"
}
],
"category": [
"BROWSABLE",
"DEFAULT"
]
}
]
}
}
}
Custom Expo Plugins
Create Platform-Specific Plugins
These plugin files automate the native code modifications required for
Singular SDK integration in Expo's managed workflow, eliminating the
need for manual editing after each expo prebuild.
- iOS Plugin: Detects Swift or Objective-C AppDelegate and injects import statements, device identifier logging, and universal link handling code that forwards attribution data to the Singular React Native bridge
-
Android Plugin: Detects Java or Kotlin MainActivity
files and adds imports, intent logging, and
onNewIntentmethod that captures deep links and passes them to Singular's bridge module
Both plugins ensure that when users tap deep links or universal links, the native code properly captures and forwards this data to your React Native hooks for attribution tracking.
Setup Steps
- Navigate into your project's root directory
- Create a plugins folder
- Add the following JavaScript plugin files
iOS Plugin Implementation
Create plugins/singular-ios-deeplink-plugin.js to automatically
configure iOS deep linking.
// plugins/singular-ios-deeplink-plugin.js
const { withAppDelegate } = require('@expo/config-plugins');
const withUniversalSingular = (config) => {
return withAppDelegate(config, (config) => {
const { modResults } = config;
// Detect file type
const isSwift = modResults.contents.includes('import Expo') ||
modResults.contents.includes('class AppDelegate');
const isObjectiveC = modResults.contents.includes('#import') ||
modResults.contents.includes('@implementation');
console.log(`Detected AppDelegate type: ${isSwift ? 'Swift' : isObjectiveC ? 'Objective-C' : 'Unknown'}`);
if (isSwift) {
modResults.contents = applySwiftModifications(modResults.contents);
} else if (isObjectiveC) {
modResults.contents = applyObjectiveCModifications(modResults.contents);
} else {
console.warn('Unable to detect AppDelegate type - skipping Singular modifications');
}
return config;
});
};
function applySwiftModifications(contents) {
// Add Singular import
if (!contents.includes('import Singular')) {
contents = contents.replace(
/import ReactAppDependencyProvider/,
`import ReactAppDependencyProvider
import Singular`
);
}
// Add IDFV logging
const swiftLaunchPattern = /didFinishLaunchingWithOptions.*?\) -> Bool \{/s;
if (!contents.includes('UIDevice.current.identifierForVendor')) {
contents = contents.replace(
swiftLaunchPattern,
`didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
print("IDFV", UIDevice.current.identifierForVendor!.uuidString)`
);
}
// Add universal link handling
if (!contents.includes('Universal link received for Singular')) {
contents = contents.replace(
/continue userActivity: NSUserActivity,\s*restorationHandler: @escaping \(\[UIUserActivityRestoring\]\?\) -> Void\s*\) -> Bool \{/,
`continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
if let url = userActivity.webpageURL {
print("Universal link received for Singular:", url.absoluteString)
if let singularBridge = NSClassFromString("SingularBridge") {
let selector = NSSelectorFromString("startSessionWithUserActivity:")
if singularBridge.responds(to: selector) {
singularBridge.perform(selector, with: userActivity, afterDelay: 1)
}
}
}`
);
}
return contents;
}
function applyObjectiveCModifications(contents) {
// Add Singular import
if (!contents.includes('#import <Singular-React-Native/SingularBridge.h>')) {
contents = contents.replace(
/#import "AppDelegate.h"/,
`#import "AppDelegate.h"
#import <Singular-React-Native/SingularBridge.h>`
);
}
// Add IDFV logging to didFinishLaunchingWithOptions
const objcLaunchPattern = /- \(BOOL\)application:\(UIApplication \*\)application didFinishLaunchingWithOptions:\(NSDictionary \*\)launchOptions\s*{/;
if (!contents.includes('NSLog(@"IDFV %@"')) {
contents = contents.replace(
objcLaunchPattern,
`- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"IDFV %@", [UIDevice currentDevice].identifierForVendor.UUIDString);
[SingularBridge startSessionWithLaunchOptions:launchOptions];`
);
}
// Add continueUserActivity method if not present
if (!contents.includes('continueUserActivity')) {
const methodToAdd = `
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
NSLog(@"Universal link received for Singular: %@", userActivity.webpageURL.absoluteString);
[SingularBridge startSessionWithUserActivity:userActivity];
return YES;
}`;
contents = contents.replace(
/@end$/,
`${methodToAdd}
@end`
);
}
return contents;
}
module.exports = withUniversalSingular;
Android Plugin Implementation
Create plugins/singular-android-deeplink-plugin.js to automatically
configure Android deep linking.
// plugins/singular-android-deeplink-plugin.js
const { withMainActivity } = require('@expo/config-plugins');
const withSingularAndroid = (config) => {
// Add MainActivity modifications
config = withMainActivity(config, (config) => {
const { modResults } = config;
// Detect Java vs Kotlin
const isKotlin = modResults.contents.includes('class MainActivity') &&
modResults.contents.includes('override fun');
const isJava = modResults.contents.includes('public class MainActivity') &&
modResults.contents.includes('@Override');
if (isKotlin) {
modResults.contents = applyKotlinModifications(modResults.contents);
} else if (isJava) {
modResults.contents = applyJavaModifications(modResults.contents);
}
return config;
});
return config;
};
function applyKotlinModifications(contents) {
// Add imports
if (!contents.includes('android.content.Intent')) {
contents = contents.replace(
/package com\..*?\n/,
`$&
import android.content.Intent
import net.singular.react_native.SingularBridgeModule`
);
}
// Add onNewIntent method
if (!contents.includes('onNewIntent')) {
const methodToAdd = `
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (intent.data != null) {
setIntent(intent)
SingularBridgeModule.onNewIntent(intent)
}
}`;
contents = contents.replace(
/}\s*$/,
`${methodToAdd}
}`
);
}
return contents;
}
function applyJavaModifications(contents) {
// Add imports
if (!contents.includes('android.content.Intent')) {
contents = contents.replace(
/package com\..*?;/,
`$&
import android.content.Intent;
import net.singular.react_native.SingularBridgeModule;`
);
}
// Add onNewIntent method
if (!contents.includes('onNewIntent')) {
const methodToAdd = `
@Override
public void onNewIntent(Intent intent) {
if(intent.getData() != null) {
setIntent(intent);
super.onNewIntent(intent);
SingularBridgeModule.onNewIntent(intent);
}
}`;
contents = contents.replace(
/}\s*$/,
`${methodToAdd}
}`
);
}
return contents;
}
module.exports = withSingularAndroid;
Register Plugins
Update your app.json file to include the new custom plugins
alongside the Singular React Native plugin.
{
"expo": {
"plugins": [
"singular-react-native",
"./plugins/singular-ios-deeplink-plugin.js",
"./plugins/singular-android-deeplink-plugin.js"
]
}
}
SDK Configuration for Expo
Configure the withSingularLink callback in your Expo app
using the same approach as bare React Native. See the
SDK Configuration section above for
complete implementation details.
Handler Behavior
Understanding Link Resolution
The withSingularLink handler behaves differently depending
on whether the app is freshly installed or already installed.
Fresh Install (Deferred Deep Link)
On a fresh install, no Open URL exists when the app launches. Singular completes attribution to determine if the tracking link contained a deep link or deferred deep link value.
Deferred Deep Link Flow:
- User clicks a Singular tracking link configured with a deep link value
- User installs and opens the app for the first time
- Singular SDK sends the first session to Singular servers
- Attribution completes and identifies the deep link from the tracking link
-
Deep link value returns to the
withSingularLinkhandler in thedeeplinkparameter withisDeferred = true
Testing Deferred Deep Links:
- Uninstall the app from the test device (if currently installed)
- iOS: Reset your IDFA. Android: Reset your Google Advertising ID (GAID)
- Click the Singular tracking link from the device (ensure it's configured with a deep link value)
- Install and open the app
Attribution should complete successfully, and the deferred deep link
value will be passed to the withSingularLink handler.
Pro Tip: When testing deep links with a development
build using a different package name (e.g., com.example.dev
instead of com.example.prod), configure the tracking
link specifically for the development app's package name. After clicking
the test link, install the development build directly onto the device
(via Android Studio or Xcode) rather than downloading the production
app from the app store.
Already Installed (Immediate Deep Link)
When the app is already installed, clicking a Singular Link opens the app immediately using Universal Links (iOS) or Android App Links technology.
Immediate Deep Link Flow:
- User clicks a Singular tracking link
- The operating system provides an Open URL containing the entire Singular tracking link
- During SDK initialization, Singular parses the URL
-
Singular extracts the
deeplinkandpassthroughvalues -
Values return through the
withSingularLinkhandler withisDeferred = false
Advanced Features
Passthrough Parameters
Capture additional data from the tracking link click using passthrough parameters.
If a passthrough (_p) parameter is included in the tracking
link, the withSingularLink handler's passthrough
parameter contains the corresponding data. Use this for capturing campaign
metadata, user segmentation data, or any custom information you need
in the app.
Example Passthrough Value:
{
"utm_source": "Facebook",
"utm_campaign": "Web-to-App US RON",
"utm_creative": "Blue CTA for Promo",
"promo_code": "45k435hg"
}
Example Tracking Link with URL encoded passthrough value in (_p) parameter:
https://yourapp.sng.link/A1b2c/abc123?_dl=myapp://product/123&_p=%7B%22utm_source%22%3A%20%22Facebook%22%2C%22utm_campaign%22%3A%20%22Web-to-App%20US%20RON%22%2C%22utm_creative%22%3A%20%22Blue%20CTA%20for%20Promo%22%2C%22promo_code%22%3A%20%2245k435hg%22%7D
The withSingularLink handler will receive:
urlParameters = {"utm_source": "Facebook","utm_campaign": "Web-to-App US RON","utm_creative": "Blue CTA for Promo","promo_code": "45k435hg"}
Forward All Query Parameters
Capture all query parameters from the tracking link URL by appending
the _forward_params=2 parameter to your tracking link.
When _forward_params=2 is added to the tracking link, all
query parameters are included in the deeplink parameter
of the withSingularLink handler, giving you access to the
complete URL with all its parameters.
Example Tracking Link:
https://yourapp.sng.link/A1b2c/abc123?_dl=myapp://product/123&_forward_params=2&utm_source=facebook&promo=SALE2024
The withSingularLink handler will receive:
deeplink = "myapp://product/123?utm_source=facebook&promo=SALE2024"