HopLinks Developer Guide
Welcome to the HopLinks Developer Guide. This manual provides comprehensive instructions for integrating HopLinks—our intelligent Dynamic and Deferred Deep Linking engine—directly into your mobile ecosystem without the need for proprietary SDKs.
The Modern FDL Alternative
Just like Firebase Dynamic Links, HopLinks routes users to the optimal destination—app, store, or web—using native OS protocols for maximum reliability and zero bloat.
How Deferred Links Work
Standard deep links fail if the app isn't installed. Deferred Deep Links "remember" the destination during the installation process, ensuring the user lands exactly where they intended after their first launch.
1. Click
User taps your smart link on any platform.
2. Install
Redirected to Store if app is missing.
3. Match
HopLinks identifies the user upon first launch.
4. Route
Seamlessly navigate to the original target.
The SDK-Less Philosophy
HopLinks provides a seamless routing experience entirely without a proprietary SDK. Instead of forcing you to bloat your app with third-party libraries, HopLinks leverages the native deep linking protocols built directly into iOS and Android.
iOS: Universal Links (AASA)
Apple devices look for a file at
/.well-known/apple-app-site-association. HopLinks automatically hosts this for
you, proving app ownership to the OS.
Android: App Links
Android devices verify ownership via
/.well-known/assetlinks.json. We host your SHA-256 fingerprints to enable
unblockable deep linking.
Intent Link
Standard URI schemes (myapp://). Simple, but only works if the app is already installed.
Dynamic Link
Device-aware smart routing. Detects platform and directs to App, Store, or Web automatically.
1. Dashboard Configuration
Register your application in the HopLinks dashboard to activate your verification endpoints.
Navigate to App Settings in your dashboard and create a new configuration.
Android: Enter your Package Name and SHA-256 Fingerprint.
iOS: Enter your Team ID and Bundle ID.
2. Native Platform Setup
Configure your native project to recognize the HopLinks domain (www.hpln.in).
Android (App Links)
Add the following intent filter to your AndroidManifest.xml:
<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="www.hpln.in" />
</intent-filter>
iOS (Universal Links)
In Xcode, go to Signing & Capabilities and add the Associated Domains capability with:
Note: Use your custom domain if you have one configured in HopLinks.
Integration: React Native (CLI)
In Expo, deep linking is mostly configured in app.json. In React Native CLI, you must modify native files for both Android and iOS. This guide walks through every step needed to replicate Expo deep linking behavior in a bare React Native CLI project.
Note
This guide covers custom URI schemes (myapp://) as well as Universal Links (https) for both Android App Links and iOS Universal Links.
Android Configuration
Modify AndroidManifest.xml to register your custom URI scheme and Universal Links.
Location: android/app/src/main/AndroidManifest.xml
Add the following intent-filter tags inside the <activity> tag (usually MainActivity):
Custom URI Scheme (myapp://)
<intent-filter android:label="Deep Link">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
Universal Links (https)
<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="hoplinks.in" />
<data android:scheme="https" android:host="www.hpln.in" />
</intent-filter>
iOS Configuration
Step A: Add URL Scheme in Info.plist
Location: ios/ReactNativeCLIDemo/Info.plist
Add the CFBundleURLTypes key to register your custom URL scheme:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Step B: Handle Links in AppDelegate.mm
Location: ios/ReactNativeCLIDemo/AppDelegate.mm
1. Import the Linking Manager
#import <React/RCTLinkingManager.h>
2. Add handling methods (before @end)
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:application openURL:url options:options];
}
- (BOOL)application:(UIApplication *)application
continueUserActivity:(nonnull NSUserActivity *)userActivity
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
React Navigation Setup (App.tsx)
Define the linking configuration and pass it to the NavigationContainer. This maps URL paths to screen names.
const linking = {
prefixes: [
"myapp://", // Custom URI scheme
"https://hoplinks.in", // Universal link
],
config: {
screens: {
Home: "home",
Explore: "product/:id", // :id is a dynamic parameter
Notifications: "notifications",
Profile: "profile",
},
},
};
// Inside your App component:
<NavigationContainer linking={linking}>
{/* ... Navigator ... */}
</NavigationContainer>
Tip
The :id in product/:id is a dynamic route parameter. It will be available in your screen component as route.params.id.
Testing Your Implementation
Android — using adb
Run the following command to trigger a deep link on a connected Android device or emulator:
adb shell am start -W \
-a android.intent.action.VIEW \
-d "myapp://product/42" \
com.reactnativeclidemo
iOS — using xcrun
Run the following command to trigger a deep link on the iOS Simulator:
xcrun simctl openurl booted "myapp://product/42"
Key Differences: Expo vs React Native CLI
| Aspect | Expo Go | React Native CLI |
|---|---|---|
| Configuration | app.json only | Native files (AndroidManifest.xml, Info.plist, AppDelegate.mm) |
| URL Scheme | exp:// prefix | Custom (e.g. myapp://) |
| Universal Links | Handled automatically | Manual autoVerify + ASLINKS file |
| Native Bridge | Built-in | RCTLinkingManager (manual) |
Implementation Checklist
- AndroidManifest.xml: Added intent-filter for myapp:// URI scheme
- AndroidManifest.xml: Added intent-filter with autoVerify=true for https Universal Links
- Info.plist: Added CFBundleURLTypes with myapp scheme
- AppDelegate.mm: Imported RCTLinkingManager
- AppDelegate.mm: Added openURL handler method
- AppDelegate.mm: Added continueUserActivity handler method
- App.tsx: Defined linking config with prefixes and screen mappings
- App.tsx: Passed linking prop to NavigationContainer
- Tested Android deep link via adb
- Tested iOS deep link via xcrun simctl
Sample Code
View the complete working React Native CLI example project on GitHub.
Integration: React Native (Expo) + Hoplinks
Complete Setup for both Android and iOS platforms.
Install Required Library
Add expo-linking to your project. Run the following command inside your project root. Use npx expo install (not plain npm) so Expo selects the version that matches your SDK.
npx expo install expo-linking
NOTE
After installation, the package is automatically added to your package.json with the correct version constraint for your Expo SDK.
Configure app.json
Register your URL scheme and verified domains. Open app.json and add the following configuration. This tells iOS and Android that your app is authorised to open specific URLs.
2.1 Set the custom URL scheme (myapp://)
2.2 Register verified HTTPS domains for Android intent filters
2.3 Register associated domains for iOS Universal Links
{
"expo": {
"scheme": "myapp",
"ios": {
"bundleIdentifier": "com.example.myapp",
"associatedDomains": [
"applinks:www.hpln.in",
"applinks:hoplinks.in"
]
},
"android": {
"package": "com.example.myapp",
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{ "scheme": "https", "host": "www.hpln.in" },
{ "scheme": "https", "host": "hoplinks.in" }
]
}
]
}
}
}
IMPORTANT
"autoVerify": true tells Android to fetch and verify the assetlinks.json file from your domain. Without it, App Links will silently fall back to the browser.
Always restart the Expo dev server after editing scheme or intentFilters — changes are not hot-reloaded.
Configure React Navigation
Map URLs to screens in App.js. Create a linking object and pass it to <NavigationContainer>. This tells React Navigation which screen to open for each incoming URL.
3.1 Import expo-linking and create the URL prefix
3.2 Define the linking config with prefixes + screen paths
3.3 Pass the config to NavigationContainer
import * as Linking from 'expo-linking';
import { NavigationContainer } from '@react-navigation/native';
// Step 3.1 — base prefix for Expo Go development URLs
const prefix = Linking.createURL('/');
// Step 3.2 — full routing configuration
const linking = {
prefixes: [
prefix, // Expo Go URLs
'myapp://', // Custom scheme
'https://www.hpln.in',
'https://hoplinks.in',
],
config: {
screens: {
Home: 'home',
Explore: 'product/:id', // :id is a dynamic param
Notifications: 'notifications',
Profile: 'profile',
},
},
};
// Step 3.3 — pass into NavigationContainer
export default function App() {
return (
<NavigationContainer linking={linking}>
{/* Your Tab.Navigator goes here */}
</NavigationContainer>
);
}
URL to screen mapping reference:
| Screen | Path Pattern | Example URL |
|---|---|---|
| Home | home | myapp://home |
| Explore | product/:id | myapp://product/id123 |
| Notifications | notifications | myapp://notifications |
| Profile | profile | myapp://profile |
Read Parameters in Screens
Extract data passed via the deep link. When a user opens myapp://product/id123, React Navigation injects { id: "id123" } as a route parameter. Use the useRoute hook to read it inside your screen component.
import { useRoute } from '@react-navigation/native';
import { View, Text } from 'react-native';
export default function ExploreScreen() {
const route = useRoute();
// Extract the dynamic :id segment from the deep link
const deepLinkId = route.params?.id;
return (
<View>
{deepLinkId && (
<Text>You opened product: {deepLinkId}</Text>
)}
</View>
);
}
NOTE
React Navigation parses and passes all URL segments and query parameters automatically. You do not need to call Linking.getInitialURL() manually for this.
Verify Domain Ownership (Automated)
Hoplinks handles server-side file hosting automatically! You do not need to worry about hosting files yourself. Simply provide your app details in the App Settings from your dashboard, and our system will automatically generate and host the required domain ownership files for Apple and Google.
WHAT HAPPENS BEHIND THE SCENES
Based on your App Settings, Hoplinks dynamically generates and serves the following files at the required paths on your verified domain:
- Android:
/.well-known/assetlinks.json - iOS:
/.well-known/apple-app-site-association
Test the Integration
Verify deep links work before going live. Testing approach depends on whether you are running Expo Go or a native build.
6.1 Testing in Expo Go — use ADB to send a test intent
# Run on a connected Android emulator or device
adb shell am start -W -a android.intent.action.VIEW \
-d "exp://127.0.0.1:8081/--/product/id123"
IMPORTANT
You cannot test https:// Universal Links inside Expo Go. The OS blocks them because Expo Go's certificate does not match your assetlinks.json.
6.2 Testing Native Builds — use the uri-scheme CLI
# Test the custom scheme
npx uri-scheme open "myapp://product/id123" --android
# Test a verified App Link
npx uri-scheme open "https://hoplinks.in/product/id123" --android
TIP
Always fully test myapp:// custom scheme routing before moving on to testing https:// App Links. This isolates navigation issues from domain verification issues.
Configure Hoplinks Dashboard
Point your short links to the right in-app screen. Each Hoplinks short link needs an In-App Deep Link value that maps to a screen in your app. Follow these steps in the dashboard:
7.1 Log into your Hoplinks Dashboard
7.2 Click "Create New Link" or edit an existing one
7.3 Set the standard Destination URL (web fallback for users without the app)
7.4 Scroll to the "Deep Link Intent" section
7.5 Enter the custom scheme URI in the "In-App Deep Link" field (see table below)
Dashboard configuration reference:
| Target Screen | In-App Deep Link value | What it opens |
|---|---|---|
| Home Screen | myapp://home | Opens the Home tab |
| Explore (product) | myapp://product/promo-123 | Opens Explore with id=promo-123 |
| Notifications | myapp://notifications | Opens Notifications screen |
| Profile Screen | myapp://profile | Opens the Profile screen |
How it works under the hood:
- App installed: Phone intercepts the intent, opens the app, React Navigation routes to the correct screen.
- App NOT installed: The custom scheme URI fails silently; Hoplinks redirects the user to the App Store or Google Play Store.
Handle Deferred Deep Linking
Route users to the right screen on first launch. Deferred Deep Linking solves this scenario: a user clicks a Hoplinks URL, is sent to the App Store because the app is not installed, downloads the app, and opens it for the first time. The app must know which screen to show.
8.1 Add getInitialURL() to your linking config in App.js
const linking = {
prefixes: [prefix, 'myapp://', 'https://www.hpln.in', 'https://hoplinks.in'],
// NEW — catches the URL that launched the app
async getInitialURL() {
// 1. Check if opened via a standard OS deep link
const url = await Linking.getInitialURL();
if (url != null) return url;
// 2. (Optional) Call Hoplinks SDK for deferred link data
// const deferredUrl = await Hoplinks.getDeferredLink();
// if (deferredUrl) return deferredUrl;
return null;
},
config: {
screens: {
Home: 'home',
Explore: 'product/:id',
Notifications: 'notifications',
Profile: 'profile',
},
},
};
8.2 Verify the flow end-to-end
Click a Hoplinks URL on a device that does not have the app installed. Install the app from the store. On first launch, confirm the app opens the correct screen.
NOTE
Without getInitialURL(), users who install via the App Store link will always land on the default Home screen instead of the content they originally clicked.
Quick Reference — Do's & Don'ts
DO
- Ensure assetlinks.json package_name exactly matches app.json
- Restart the Expo dev server after changing scheme or intentFilters
- Use human-readable URL paths (e.g. hoplinks.in/explore/design)
- Test myapp:// scheme routing with uri-scheme before testing https:// links
DON'T
- Test https:// links in Expo Go (the OS will block them)
- Add a .json extension to apple-app-site-association
- Forget to update the SHA-256 fingerprint when switching to production builds
- Serve assetlinks.json with HTML content-type or unexpected headers
Sample Code
View the complete working example project on GitHub.
Integration: Flutter
Add the app_links package to your pubspec.yaml and initialize the listener:
import 'dart:async';
import 'package:app_links/app_links.dart';
class _MyAppState extends State<MyApp> {
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
@override
void initState() {
super.initState();
initDeepLinks();
}
Future<void> initDeepLinks() async {
_appLinks = AppLinks();
// Handle deep links when app is in background/foreground
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
print('Received HopLink: $uri');
_handleLinkRoute(uri);
});
// Handle deep link when app opens from cold start
final initialUri = await _appLinks.getInitialAppLink();
if (initialUri != null) _handleLinkRoute(initialUri);
}
void _handleLinkRoute(Uri uri) {
if (uri.host == 'www.hpln.in') {
String code = uri.pathSegments.last;
print("Navigate to: $code");
}
}
}
Integration: Android (Kotlin)
In your MainActivity.kt, override onCreate and onNewIntent to
catch the URL parameters.
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
handleDeepLink(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleDeepLink(intent)
}
private fun handleDeepLink(intent: Intent?) {
val action: String? = intent?.action
val data: Uri? = intent?.data
if (Intent.ACTION_VIEW == action && data != null) {
val code = data.lastPathSegment
println("Received HopLink code: $code")
}
}
}
Integration: Android (Java)
Similar to Kotlin, override the intent handling methods in MainActivity.java.
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handleDeepLink(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleDeepLink(intent);
}
private void handleDeepLink(Intent intent) {
String action = intent.getAction();
Uri data = intent.getData();
if (Intent.ACTION_VIEW.equals(action) && data != null) {
String code = data.getLastPathSegment();
// Route to appropriate activity
}
}
}
Integration: iOS (Swift)
For iOS apps using SceneDelegate.swift (iOS 13+), intercept the
NSUserActivity:
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL {
handleDeepLink(url: incomingURL)
}
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
if let userActivity = connectionOptions.userActivities.first,
userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL {
handleDeepLink(url: incomingURL)
}
}
private func handleDeepLink(url: URL) {
let pathComponents = url.pathComponents
if let code = pathComponents.last {
print("Navigate to content: \(code)")
}
}
}
Troubleshooting
Android
Ensure android:autoVerify="true" is set.
It can take up to 20 seconds for Android to verify the JSON file after app installation.
iOS
Ensure your Apple Team ID and Bundle ID precisely
match what is configured in Xcode. iOS downloads the apple-app-site-association
file upon app installation.
Important: Tap, Don't Paste
Universal Links and App Links will not trigger if you paste the URL into a browser's address bar. They must be triggered by a "click" action from an external app (Notes, WhatsApp, Mail).
Need Expert Assistance?
Our integration engineers can help you configure Universal Links, App Links, or complex routing logic for your specific tech stack.