
Security News
Ruby's Bundler 4.0.18 Extends Cooldown to bundle lock and bundle cache
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.
@ulinkly/react-native
Advanced tools
Official ULink deep linking SDK for React Native and Expo. Bridges the native iOS (ULinkSDK 1.1.1) and Android (ly.ulink:ulink-sdk 1.1.0) SDKs via the Expo Modules API, delivering full feature parity with the Flutter SDK: dynamic links, deferred deep linking, install attribution, reinstall detection, session tracking, and MAU dedup via persistent device ID.
ULink ships native SDKs for every major mobile platform:
ULinkSDK (CocoaPods + SPM)ly.ulink:ulink-sdk (Maven Central)flutter_ulink_sdk (pub.dev)@ulinkly/react-native (this package)expo prebuild.Using Claude Code, Cursor, Codex, or another AI coding agent? Install the ULink onboarding skill in one command:
npx skills add https://ulink.ly
Then ask your assistant to "setup ulink" — it'll detect your React Native project, configure your ULink dashboard, edit your native files, and verify the integration. Works with 50+ AI agents via the open agent-skills CLI. Learn more →
npx expo install @ulinkly/react-native
Add the config plugin to app.json (see Config Plugin below), then rebuild:
# Expo dev client
npx expo run:ios
npx expo run:android
# Or prebuild + native build
npx expo prebuild
Expo Go is not supported. This package uses native modules that are not available in the Expo Go sandbox. You must use a development build or
expo prebuild.
npm install @ulinkly/react-native
npx install-expo-modules@latest # one-time: wires Expo Modules into your existing RN project
cd ios && pod install
Bare RN users must also complete the manual native setup below (no config plugin).
Expo / managed workflow only. Bare RN users: see Manual Native Setup.
Add the plugin to your app.json:
{
"expo": {
"plugins": [
["@ulinkly/react-native", {
"scheme": "myapp",
"domains": ["myapp.shared.ly"]
}]
]
}
}
| Prop | Type | Required | Description |
|---|---|---|---|
scheme | string | Yes | Your app's custom URL scheme (without ://). Must match what you registered in the ULink dashboard. |
domains | string[] | No | One or more Associated Domains / App Link hosts (e.g. ["myapp.shared.ly"]). |
What the plugin configures during expo prebuild:
CFBundleURLTypes entry for scheme in Info.plist; adds applinks:<domain> entries to the Associated Domains entitlement.<intent-filter> to your main activity; adds an android:autoVerify="true" HTTPS host <intent-filter> for each domain.Run npx expo prebuild after updating the plugin config.
Skip this section if you are using the config plugin.
Before configuring native files, register your app in the ULink dashboard:
shared.ly. ULink automatically serves the .well-known/ files needed for Universal Links / App Links verification.1. URL Scheme — ios/<YourApp>/Info.plist
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string> <!-- your scheme, without :// -->
</array>
</dict>
</array>
2. Associated Domains — Xcode Signing & Capabilities
In Xcode, go to your target → Signing & Capabilities → + Capability → Associated Domains. Add:
applinks:myapp.shared.ly
Or directly in ios/<YourApp>/<YourApp>.entitlements:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myapp.shared.ly</string>
</array>
3. AppDelegate wiring
The SDK module ships an Expo Module AppDelegate subscriber that automatically intercepts Universal Link continuations (application(_:continue:restorationHandler:)) and custom-scheme opens (application(_:open:options:)) when running with install-expo-modules. No manual AppDelegate edits are required for bare RN projects that have run npx install-expo-modules@latest.
If for any reason you need to wire manually, forward both callbacks through RCTLinkingManager (standard RN practice) — the native module listens on the same URL delivery path.
android/app/src/main/AndroidManifest.xml — add inside your main <activity>:
<!-- Custom scheme deep links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Replace with your registered scheme (without ://) -->
<data android:scheme="myapp" />
</intent-filter>
<!-- HTTPS App Links (auto-verified) -->
<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" />
<!-- Replace with your subdomain on shared.ly -->
<data android:scheme="https" android:host="myapp.shared.ly" />
</intent-filter>
Note: ULink automatically serves the .well-known/assetlinks.json file for your registered domain — no manual hosting required.
import { useEffect } from 'react';
import ULink from '@ulinkly/react-native';
async function initULink() {
// Always await initialize() before calling any other method.
// On Android, calls made before init resolves are rejected with an error.
await ULink.initialize({
apiKey: 'YOUR_API_KEY',
debug: __DEV__,
});
}
// Call once at app startup — before mounting any screen that needs deep links.
initULink().catch(console.error);
// In a component or navigation root:
function App() {
useEffect(() => {
// Subscribe to incoming deep links (cold-start and foreground).
const dynSub = ULink.onDynamicLink((data) => {
console.log('Dynamic link received:', data.parameters);
// Navigate based on data.parameters, e.g. router.push(data.parameters?.screen)
});
const uniSub = ULink.onUnifiedLink((data) => {
console.log('Unified link received:', data);
// Unified links are platform redirects — open externally if needed.
});
return () => {
dynSub.remove();
uniSub.remove();
// Do NOT call ULink.dispose() here — see Caveats below.
};
}, []);
return <YourNavigator />;
}
// Create a shareable link
async function shareLink() {
const response = await ULink.createLink({
domain: 'shadd.shared.ly', // required: your registered ULink subdomain
type: 'dynamic',
slug: 'my-promo',
// externalId deduplicates repeat calls — if a link with this ID already exists
// on the domain, the existing link is returned instead of creating a new one.
externalId: 'promo-launch-2024',
iosFallbackUrl: 'https://apps.apple.com/app/id123456789',
androidFallbackUrl: 'https://play.google.com/store/apps/details?id=com.myapp',
fallbackUrl: 'https://myapp.com/promo',
parameters: { screen: 'promo', campaign: 'launch' },
socialMediaTags: {
ogTitle: 'Check out MyApp!',
ogDescription: 'Download and get 20% off your first order.',
ogImage: 'https://myapp.com/og-promo.jpg',
},
});
if (response.success) {
console.log('Share URL:', response.url);
}
}
// Manually resolve a link URL
async function openLink(url: string) {
const response = await ULink.resolveLink(url);
if (response.success && response.data) {
console.log('Link data:', response.data.parameters);
}
}
ULink.initialize(config: ULinkConfig): Promise<void>Initializes the native SDK. Must be called and awaited before any other method.
initialize() resolves are rejected with a ULinkError.await init first for predictable behavior.initialize() is idempotent: subsequent calls are no-ops.await ULink.initialize({
apiKey: 'YOUR_API_KEY',
debug: true,
});
ULink.createLink(parameters: ULinkParameters): Promise<ULinkResponse>Creates a shareable deep link on the ULink platform.
ULink.resolveLink(url: string): Promise<ULinkResponse>Resolves a ULink URL to its stored data (parameters, fallback URLs, social tags).
ULink.processULink(url: string): Promise<ULinkResolvedData | null>Processes a raw ULink URL and returns resolved data, or null if the URL is not a ULink.
ULink.checkDeferredLink(): Promise<void>Explicitly triggers a deferred deep link check. Normally called once after initialize() when autoCheckDeferredLink is not set. Results are delivered via the onDynamicLink or onUnifiedLink event.
ULink.getInitialDeepLink(): Promise<ULinkResolvedData | null>Returns the deep link that launched the app in the current session, or null if the app was opened normally. Note: cold-start links are also delivered via onDynamicLink/onUnifiedLink events (the preferred pattern).
ULink.getInitialUri(): Promise<string | null>Returns the raw URI string that opened the app, if any.
ULink.setInitialUri(uri: string): Promise<void>Override the initial URI — primarily a testing aid.
ULink.getLastLinkData(): Promise<ULinkResolvedData | null>Returns the most recently resolved link data, optionally persisted across launches (see persistLastLinkData in ULinkConfig).
ULink.getInstallationId(): Promise<string | null>Returns this device's ULink installation ID (a UUID generated on first launch and persisted).
ULink.getInstallationInfo(): Promise<ULinkInstallationInfo | null>Returns full installation metadata including persistentDeviceId, isReinstall, and previousInstallationId.
ULink.isReinstall(): Promise<boolean>Returns true if the current install was detected as a reinstall.
ULink.getCurrentSessionId(): Promise<string | null>Returns the active session ID, or null if no session is active.
ULink.hasActiveSession(): Promise<boolean>Returns true if a session is currently active.
ULink.getSessionState(): Promise<SessionState>Returns the current session lifecycle state.
ULink.endSession(): Promise<void>Explicitly ends the current session.
ULink.dispose(): Promise<void>Tears down the native SDK singleton. Advanced/teardown only — see Caveats.
Event listeners return an EventSubscription with a .remove() method. Always call .remove() in your cleanup to prevent memory leaks.
ULink.onDynamicLink(callback: (data: ULinkResolvedData) => void): EventSubscriptionFires when a dynamic link is received — cold-start, warm (foreground), or deferred install. This is the primary channel for deep link navigation.
ULink.onUnifiedLink(callback: (data: ULinkResolvedData) => void): EventSubscriptionFires when a unified (simple redirect) link is received. Unified links are platform-based redirects — the SDK does not auto-navigate; inspect data and open externally if appropriate.
ULink.onReinstallDetected(callback: (info: ULinkInstallationInfo) => void): EventSubscriptionFires once when a reinstall is detected, carrying the ULinkInstallationInfo payload (includes previousInstallationId).
ULink.onLog(callback: (entry: ULinkLogEntry) => void): EventSubscriptionDebug-only. Forwards native SDK log entries to JS. Use to inspect SDK internals during development; remove or gate behind __DEV__ in production.
ULinkConfiginterface ULinkConfig {
apiKey: string; // required
baseUrl?: string; // default: https://api.ulink.ly
debug?: boolean;
persistLastLinkData?: boolean;
lastLinkTimeToLiveSeconds?: number;
clearLastLinkOnRead?: boolean;
redactAllParametersInLastLink?: boolean;
redactedParameterKeysInLastLink?: string[];
autoCheckDeferredLink?: boolean;
// enableDeepLinkIntegration is intentionally NOT exposed:
// the module forces it false on Android and owns link delivery on both platforms.
}
ULinkParametersinterface ULinkParameters {
domain: string; // Required. Your registered ULink subdomain (e.g. "myapp.shared.ly").
type?: 'dynamic' | 'unified';
slug?: string;
name?: string; // Optional display label shown in the ULink dashboard.
externalId?: string; // Optional idempotency key — dedupes repeat createLink calls on the same domain.
iosUrl?: string;
androidUrl?: string;
iosFallbackUrl?: string;
androidFallbackUrl?: string;
fallbackUrl?: string;
parameters?: Record<string, unknown>; // arbitrary JSON-serializable map
socialMediaTags?: SocialMediaTags;
metadata?: Record<string, unknown>; // arbitrary JSON-serializable map
}
ULinkResponseinterface ULinkResponse {
success: boolean;
url?: string;
data?: ULinkResolvedData;
error?: string;
}
ULinkResolvedDatainterface ULinkResolvedData {
type: string;
slug?: string;
fallbackUrl?: string;
iosFallbackUrl?: string;
androidFallbackUrl?: string;
parameters?: Record<string, unknown>;
metadata?: Record<string, unknown>;
socialMediaTags?: SocialMediaTags;
isDeferred?: boolean;
matchType?: string;
rawData?: Record<string, unknown>;
}
ULinkInstallationInfointerface ULinkInstallationInfo {
installationId: string;
isReinstall: boolean;
previousInstallationId?: string;
reinstallDetectedAt?: string;
persistentDeviceId?: string; // iOS: Keychain UUID; Android: OS ANDROID_ID
}
SessionStateenum SessionState {
IDLE = 'idle',
INITIALIZING = 'initializing',
ACTIVE = 'active',
ENDING = 'ending',
FAILED = 'failed',
}
SocialMediaTagsinterface SocialMediaTags {
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
}
ULinkLogEntryinterface ULinkLogEntry {
level: string;
message: string;
timestamp?: number; // epoch milliseconds
tag?: string;
}
await ULink.initialize(...) firstinitialize() is async. Call it at app startup (before mounting any screen that handles deep links) and always await it. On Android, any method called before initialize() resolves will reject with a ULinkError. On iOS, calls are queued, but order is undefined — await init for reliable behavior on both platforms.
iOS deferred linking uses fingerprint matching (IP, user-agent, timestamp). It does not use IDFA, SKAdNetwork, pasteboard, or ATT — no tracking permission prompts or extra entitlements are required. Because matching is fingerprint-based, a 100% match rate cannot be guaranteed on iOS (network/VPN conditions can reduce accuracy). Android deferred linking uses the Play Install Referrer (deterministic) with a fingerprint fallback.
persistentDeviceId is platform-specificgetInstallationInfo() returns a persistentDeviceId field that is used for MAU dedup:
ANDROID_ID, which is scoped to the app's signing key and user profile. It may be null on some older or heavily customized devices.This is not a ULink-generated ID on Android; it is the system-level device identifier.
dispose() on component unmountULink.dispose() tears down the native SDK singleton (stops session tracking, unsubscribes Combine/SharedFlow streams). Calling it on React component unmount or fast-refresh will break the SDK for the lifetime of the process. Use it only for intentional full teardown (e.g., user logout in an app that needs to reinitialize with a different API key).
@ulinkly/react-native is a native module. It cannot run in the Expo Go sandbox. Use a development build (npx expo run:ios / npx expo run:android) or expo prebuild with your native toolchain.
In v0.1.0, traffic from @ulinkly/react-native is reported in ULink analytics as sdk-ios / sdk-android (the same identifiers the native SDKs use). React Native-specific tagging is planned for a future release.
In-app deep links with custom parameters, fallback URLs, and smart app store redirects. Use for navigating users to specific in-app screens. Delivered via onDynamicLink.
Simple platform-based redirects (e.g. iOS App Store URL, Android Play Store URL, web fallback). The SDK does not auto-redirect — inspect the data in onUnifiedLink and open externally if appropriate.
When a link has allowQueryPassthrough enabled (configured via the ULink dashboard or REST API), query parameters appended to the link URL at click time (e.g. ?orderId=123) are merged into data.parameters before delivery. Passthrough values always arrive as strings and override stored params with the same key. No SDK changes needed.
ULink.onDynamicLink((data) => {
const orderId = data.parameters?.orderId; // e.g. "123" (string)
});
MIT — see LICENSE.
Repository: https://github.com/FlywheelStudio/ulink-react-native
FAQs
Official ULink deep linking SDK for React Native and Expo.
The npm package @ulinkly/react-native receives a total of 47 weekly downloads. As such, @ulinkly/react-native popularity was classified as not popular.
We found that @ulinkly/react-native demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.

Company News
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.