Skip to content

Conversation

ianmaccallum
Copy link
Collaborator

@ianmaccallum ianmaccallum commented Sep 23, 2025

  • Adds UIKit code samples
  • Adds RN guide and RN code samples
  • Marks iOS / RN sdks as beta
  • Fixes DUB_DOMAIN description

Summary by CodeRabbit

  • Documentation
    • Added a React Native installation guide, snippets, and example links; added React Native cards to installation snippets.
    • Expanded deep-link and quickstart docs to include React Native, iOS UIKit, and SwiftUI with init, first-launch handling, URL listeners, and trackOpen/trackLead/trackSale samples.
    • Marked Client-side Mobile SDKs (iOS, React Native) as Beta and updated navigation.

Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

Walkthrough

Adds React Native support to mobile deep-link and installation docs alongside iOS, introduces new RN installation/initialization snippets, expands iOS UIKit/SwiftUI examples, updates navigation (docs.json) with Beta tags and RN guide, and augments quickstarts with first-launch handling, deep-link processing, and trackOpen flows.

Changes

Cohort / File(s) Summary
Deep-links concepts
concepts/deep-links/attribution.mdx, concepts/deep-links/quickstart.mdx
Extend from iOS-only to iOS + React Native; add multi-language code samples for initialization, first-launch checks, deep-link handling, and trackOpen; include UIKit and SwiftUI variants.
Docs navigation/config
docs.json
Mark Client-side SDK (Mobile) as Beta; add React Native installation page entry.
Mobile installation guides
sdks/client-side-mobile/installation-guides/react-native.mdx, sdks/client-side-mobile/installation-guides/swift.mdx, sdks/client-side-mobile/introduction.mdx
Add new RN guide (Beta), update Swift guide (tag, snippet path, examples), refresh introduction with RN entry, Beta labels, and new example cards (Swift UIKit, RN).
RN install/init snippets
snippets/steps/install-react-native-sdk.mdx, snippets/steps/initialize-react-native-sdk.mdx, snippets/dub-client-react-native-install.mdx
Provide RN install commands; document SDK initialization via provider/manual; add RN examples for trackOpen, trackLead, trackSale with TypeScript.
iOS install/init and examples
snippets/dub-client-ios-install.mdx, snippets/steps/initialize-ios-sdk.mdx
Add UIKit initialization via AppDelegate; expand iOS snippets for trackOpen (first-launch), trackLead, trackSale with Swift examples.
Mobile guides index snippet
snippets/dub-client-mobile-installation-guides.mdx
Add React Native card to installation guides list.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant App
  participant SDK as Dub SDK
  participant OS as OS/Linking
  participant Dest as Destination

  rect rgba(230,245,255,0.5)
  Note over App,SDK: App start
  User->>App: Launch app
  App->>SDK: init(publishableKey, domain)
  end

  alt First launch with initial deep link
    OS-->>App: getInitialURL()
    App->>SDK: trackOpen(deepLink)
    SDK-->>App: { destinationUrl? }
    App->>Dest: Navigate if destinationUrl present
  else Subsequent opens via URL event
    OS-->>App: url event (deepLink)
    App->>SDK: trackOpen(deepLink)
    SDK-->>App: { destinationUrl? }
    App->>Dest: Navigate if destinationUrl present
  end

  opt Errors
    SDK-->>App: error
    App->>App: Handle/log error
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Dub iOS SDK docs #232 — Touches the same mobile SDK and deep-link docs areas, including navigation and snippets, likely part of the same documentation expansion.
  • iOS: Deferred deep linking #226 — Updates deep-links quickstart and trackOpen examples across iOS/RN, overlapping with the flows expanded here.

Suggested reviewers

  • devkiran
  • steven-tey

Poem

A hop, a skip, I bound through links,
RN and iOS in tidy syncs.
First launch whispers, “track and go,”
Deep paths bloom where metrics flow.
With Beta badges shining bright,
I thump my paws—docs set to flight! 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Adds UIKit / RN docs” directly reflects the main changes in the pull request by indicating that new documentation for both UIKit and React Native is being added, which aligns with the core objectives of this update.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-ios-docs-and-add-uikit-and-rn

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ianmaccallum ianmaccallum force-pushed the fix-ios-docs-and-add-uikit-and-rn branch from 5a9a068 to 20a4123 Compare September 23, 2025 14:04
@ianmaccallum ianmaccallum marked this pull request as ready for review September 23, 2025 14:04
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (7)
snippets/steps/initialize-react-native-sdk.mdx (1)

28-28: Add missing import for useEffect hook.

The manual initialization example uses useEffect but doesn't import it from React.

Apply this fix:

+import React, { useEffect } from "react";
 import dub from "@dub/react-native";
snippets/dub-client-react-native-install.mdx (2)

125-138: Lead tracking snippet uses await at top level

Wrap in an async function to be copy‑pasteable.

Apply this diff:

-try {
-  await dub.trackLead({
-    eventName: "User Sign Up",
-    customerExternalId: user.id,
-    customerName: user.name,
-    customerEmail: user.email,
-  });
-} catch (error) {
-  // Handle sale tracking error
-}
+async function trackLead(user: { id: string; name: string; email: string }) {
+  try {
+    await dub.trackLead({
+      eventName: "User Sign Up",
+      customerExternalId: user.id,
+      customerName: user.name,
+      customerEmail: user.email,
+    });
+  } catch (error) {
+    // Handle lead tracking error
+  }
+}

150-163: Sale tracking snippet uses await at top level

Wrap in an async function for correctness.

Apply this diff:

-try {
-  await dub.trackSale({
-    customerExternalId: user.id,
-    amount: product.price.amount,
-    currency: "usd",
-    eventName: "Purchase",
-  });
-} catch (error) {
-  // Handle sale tracking error
-}
+async function trackSale(
+  user: { id: string },
+  product: { price: { amount: number } }
+) {
+  try {
+    await dub.trackSale({
+      customerExternalId: user.id,
+      amount: product.price.amount,
+      currency: "usd",
+      eventName: "Purchase",
+    });
+  } catch (error) {
+    // Handle sale tracking error
+  }
+}
snippets/dub-client-ios-install.mdx (2)

15-17: macOS version is incorrect

“macOS 10.13 (Ventura)+” mixes version and codename. Ventura is macOS 13.

Apply this diff:

-- macOS 10.13 (Ventura)+
+- macOS 13 (Ventura)+

168-176: Consider SceneDelegate URL handling for scene-based apps

If the app uses scenes (iOS 13+), implement scene(:openURLContexts:) as well; application(:open:options:) may not fire.

concepts/deep-links/attribution.mdx (2)

31-35: Outdated availability note conflicts with RN examples below

Update to reflect RN support (Beta) shown in this page.

Apply this diff:

-<Note>
-  This feature is currently only available for iOS (Swift). React Native and
-  Android support are coming soon. If you'd like early access, please [contact
-  us](https://dub.co/contact/support).
-</Note>
+<Note>
+  Available for iOS (Swift) and React Native (Beta). Android support is coming soon. If you'd like early access, please [contact us](https://dub.co/contact/support).
+</Note>

269-297: RN conversion helpers should be async

Functions call await but aren’t declared async.

Apply this diff:

-function trackLead(user: User) {
+async function trackLead(user: User) {
@@
-}
+}
@@
-function trackSale(user: User, product: Product) {
+async function trackSale(user: User, product: Product) {
@@
-}
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ebed3c and 20a4123.

📒 Files selected for processing (12)
  • concepts/deep-links/attribution.mdx (6 hunks)
  • concepts/deep-links/quickstart.mdx (2 hunks)
  • docs.json (1 hunks)
  • sdks/client-side-mobile/installation-guides/react-native.mdx (1 hunks)
  • sdks/client-side-mobile/installation-guides/swift.mdx (2 hunks)
  • sdks/client-side-mobile/introduction.mdx (2 hunks)
  • snippets/dub-client-ios-install.mdx (5 hunks)
  • snippets/dub-client-mobile-installation-guides.mdx (1 hunks)
  • snippets/dub-client-react-native-install.mdx (1 hunks)
  • snippets/steps/initialize-ios-sdk.mdx (1 hunks)
  • snippets/steps/initialize-react-native-sdk.mdx (1 hunks)
  • snippets/steps/install-react-native-sdk.mdx (1 hunks)
🔇 Additional comments (17)
sdks/client-side-mobile/installation-guides/swift.mdx (4)

4-4: LGTM! Proper Beta tagging applied.

The addition of the "Beta" tag aligns with the PR's objective to mark iOS SDK as beta status.


7-7: LGTM! Updated import path for iOS-specific content.

The change from dub-client-mobile-install.mdx to dub-client-ios-install.mdx correctly specializes the import for iOS-specific installation instructions.


15-17: LGTM! Improved clarity in domain selection instructions.

The updated wording provides clearer guidance by directing users to select their domain from the Custom Domains settings page.


40-47: LGTM! Added UIKit example card.

The addition of the Swift (UIKit) card provides developers with reference implementation for UIKit-based applications, complementing the existing SwiftUI example.

snippets/steps/install-react-native-sdk.mdx (1)

1-14: LGTM! Well-structured installation step.

The installation step provides clear, comprehensive instructions for installing the Dub React Native SDK using npm, yarn, and pnpm package managers.

docs.json (2)

179-179: LGTM! Appropriate Beta tagging for mobile SDK group.

The Beta tag correctly indicates the maturity level of the Client-side SDK (Mobile) functionality.


185-186: LGTM! Added React Native installation guide to navigation.

The addition of the React Native installation guide entry properly expands the mobile SDK documentation coverage.

snippets/steps/initialize-ios-sdk.mdx (1)

29-50: LGTM! Added comprehensive UIKit initialization example.

The UIKit initialization example provides a complete implementation showing how to initialize the Dub SDK in an AppDelegate-based application. The code follows iOS conventions with proper AppDelegate structure and initialization in application:didFinishLaunchingWithOptions:.

snippets/steps/initialize-react-native-sdk.mdx (1)

1-39: LGTM! Comprehensive React Native initialization options.

The initialization step provides two clear options for developers:

  • Provider-based approach using DubProvider (recommended for most use cases)
  • Manual initialization approach using dub.init() in useEffect

Both examples are well-documented and follow React Native best practices.

snippets/dub-client-mobile-installation-guides.mdx (1)

9-15: LGTM! Added React Native installation guide card.

The React Native card follows the same structure as the existing Swift card and provides appropriate navigation to the React Native installation guide.

sdks/client-side-mobile/introduction.mdx (2)

13-14: LGTM! Updated SDK listings with Beta indicators.

The changes appropriately:

  • Mark both iOS and React Native SDKs as beta
  • Update the messaging to indicate Android is coming soon (instead of React Native)

40-55: LGTM! Added comprehensive example cards.

The new example cards provide developers with GitHub repositories for both UIKit and React Native implementations, expanding from the original SwiftUI-only example.

sdks/client-side-mobile/installation-guides/react-native.mdx (5)

1-5: LGTM! Proper front matter with Beta tag.

The front matter correctly establishes the React Native guide with appropriate title, description, and Beta tag.


7-9: LGTM! Well-structured imports.

The imports correctly reference the React Native-specific installation snippet and shared verification components.


15-17: LGTM! Clear prerequisite instructions.

The prerequisites section provides clear guidance for obtaining the publishable key and domain from the appropriate settings pages.


31-40: LGTM! Appropriate example card with GitHub link.

The example card provides developers with a reference React Native implementation hosted on GitHub.


25-25: React Native installation snippet exists and is valid.
Found /snippets/dub-client-react-native-install.mdx; it contains install, init, and tracking (trackOpen/trackLead/trackSale) examples.

import GeneratePublishableKeyStep from "/snippets/steps/generate-publishable-key.mdx";
import AllowlistDomainsStep from "/snippets/steps/allowlist-domains.mdx";
import InstallationGuides from "/snippets/dub-client-mobile-installation-guides.mdx";
import InstallationGuides from "/snippets/dub-client-ios-installation-guides.mdx";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Verify snippet import path exists

Ensure /snippets/dub-client-ios-installation-guides.mdx is present (name suggests iOS‑only; you may have renamed it).

Run:


🏁 Script executed:

#!/bin/bash
fd -a 'dub-client-*-installation-guides.mdx' | sed -n '1,200p'

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== attribution.mdx (first 200 lines) ==="
if [ -f concepts/deep-links/attribution.mdx ]; then
  sed -n '1,200p' concepts/deep-links/attribution.mdx
else
  echo "MISSING: concepts/deep-links/attribution.mdx"
fi

echo
echo "=== snippets directory listing ==="
if [ -d snippets ]; then
  ls -la snippets | sed -n '1,200p'
else
  echo "MISSING: snippets/ directory"
fi

echo
echo "=== Searching repo for matching snippet filenames and references ==="
rg -n --hidden --no-ignore-vcs -S 'dub-client-.*installation-guides.*\.mdx' || true
rg -n --hidden --no-ignore-vcs -S 'installation-guides' || true
rg -n --hidden --no-ignore-vcs -S 'import InstallationGuides' || true

Length of output: 11766


Broken snippet import — update import path

concepts/deep-links/attribution.mdx imports "/snippets/dub-client-ios-installation-guides.mdx" (line 10) but that file does not exist; replace with "/snippets/dub-client-mobile-installation-guides.mdx" (present) or with "/snippets/dub-client-ios-install.mdx" if you intended an iOS‑only snippet.
Location: concepts/deep-links/attribution.mdx:10

🤖 Prompt for AI Agents
In concepts/deep-links/attribution.mdx around line 10, the import path
"/snippets/dub-client-ios-installation-guides.mdx" points to a non-existent
file; replace it with the correct snippet path. Update the import to either
"/snippets/dub-client-mobile-installation-guides.mdx" if you want the mobile
installation guides, or "/snippets/dub-client-ios-install.mdx" if you intended
an iOS-only snippet; make the change on line 10 so the file imports an existing
snippet.

Comment on lines +91 to +143
```typescript React Native expandable
import { useState, useEffect, useRef } from "react";
import { Linking } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import dub from "@dub/react-native";

export default function App() {
useEffect(() => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});

// Check if this is first launch
const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");

if (isFirstLaunch === null) {
await handleFirstLaunch();
await AsyncStorage.setItem("is_first_launch", "false");
} else {
// Handle initial deep link url (Android only)
const url = await Linking.getInitialURL();

if (url) {
await handleDeepLink(url);
}
}

const linkingListener = Linking.addEventListener("url", (event) => {
handleDeepLink(event.url);
});

return () => {
linkingListener.remove();
};
}, []);

const handleFirstLaunch = async (
deepLinkUrl?: string | null | undefined
): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);

const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};

// Return your app...
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

RN example: async useEffect and undefined handleDeepLink

Same issues as other RN snippets: await in non‑async useEffect and missing handleDeepLink.

Apply this diff to the effect:

   useEffect(() => {
-    dub.init({
-      publishableKey: "<DUB_PUBLISHABLE_KEY>",
-      domain: "<DUB_DOMAIN>",
-    });
-    // Check if this is first launch
-    const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
-    if (isFirstLaunch === null) {
-      await handleFirstLaunch();
-      await AsyncStorage.setItem("is_first_launch", "false");
-    } else {
-      // Handle initial deep link url (Android only)
-      const url = await Linking.getInitialURL();
-      if (url) {
-        await handleDeepLink(url);
-      }
-    }
-    const linkingListener = Linking.addEventListener("url", (event) => {
-      handleDeepLink(event.url);
-    });
-    return () => {
-      linkingListener.remove();
-    };
+    const init = async () => {
+      dub.init({
+        publishableKey: "<DUB_PUBLISHABLE_KEY>",
+        domain: "<DUB_DOMAIN>",
+      });
+      const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
+      if (isFirstLaunch === null) {
+        await handleFirstLaunch();
+        await AsyncStorage.setItem("is_first_launch", "false");
+      } else {
+        const url = await Linking.getInitialURL();
+        if (url) await handleDeepLink(url);
+      }
+    };
+    init();
+    const linkingListener = Linking.addEventListener("url", (event) => {
+      handleDeepLink(event.url);
+    });
+    return () => linkingListener.remove();
   }, []);

And add handleDeepLink:

   const handleFirstLaunch = async (
@@
   };
+
+  const handleDeepLink = async (deepLinkUrl: string): Promise<void> => {
+    try {
+      const response = await dub.trackOpen(deepLinkUrl);
+      const destinationURL = response.link?.url;
+      // Navigate to the destination URL
+    } catch (error) {
+      // Handle error
+    }
+  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```typescript React Native expandable
import { useState, useEffect, useRef } from "react";
import { Linking } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import dub from "@dub/react-native";
export default function App() {
useEffect(() => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});
// Check if this is first launch
const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
if (isFirstLaunch === null) {
await handleFirstLaunch();
await AsyncStorage.setItem("is_first_launch", "false");
} else {
// Handle initial deep link url (Android only)
const url = await Linking.getInitialURL();
if (url) {
await handleDeepLink(url);
}
}
const linkingListener = Linking.addEventListener("url", (event) => {
handleDeepLink(event.url);
});
return () => {
linkingListener.remove();
};
}, []);
const handleFirstLaunch = async (
deepLinkUrl?: string | null | undefined
): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);
const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};
// Return your app...
}
```
import { useState, useEffect, useRef } from "react";
import { Linking } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import dub from "@dub/react-native";
export default function App() {
useEffect(() => {
const init = async () => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});
const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
if (isFirstLaunch === null) {
await handleFirstLaunch();
await AsyncStorage.setItem("is_first_launch", "false");
} else {
const url = await Linking.getInitialURL();
if (url) await handleDeepLink(url);
}
};
init();
const linkingListener = Linking.addEventListener("url", (event) => {
handleDeepLink(event.url);
});
return () => linkingListener.remove();
}, []);
const handleFirstLaunch = async (
deepLinkUrl?: string | null | undefined
): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);
const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};
const handleDeepLink = async (deepLinkUrl: string): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);
const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};
// Return your app...
}
🤖 Prompt for AI Agents
In concepts/deep-links/attribution.mdx around lines 91–143, the useEffect
currently uses top-level await and references a missing handleDeepLink; fix by
making the effect synchronous and declaring an inner async function (e.g., async
function init()) that calls dub.init, reads
AsyncStorage.getItem("is_first_launch"), conditionally calls handleFirstLaunch
or Linking.getInitialURL()/handleDeepLink, and sets AsyncStorage when
appropriate, then invoke init() from useEffect; also add a handleDeepLink async
function (similar to handleFirstLaunch) that accepts a url string | null |
undefined, calls dub.trackOpen(url), extracts response.link?.url for navigation,
and handles errors; ensure the Linking event subscription is created and cleaned
up correctly (store subscription and call .remove() in the cleanup) and
await/await usage is confined to the inner async function and helper functions.

Comment on lines +359 to +387
```swift iOS (UIKit) expandable
// ViewController.swift
import UIKit
import Dub

class ViewController: UIViewController {
// View controller lifecycle

private func trackLead(customerExternalId: String, name: String, email: String) {
Task {
do {
let response = try await dub.trackLead(customerExternalId: customerExternalId, name: name, email: email)
} catch let error as DubError {
print(error.localizedDescription)
}
}
}

private func trackSale(customerExternalId: String, amount: Int, currency: String = "usd", eventName: String? = "Purchase", customerName: String? = nil, customerEmail: String? = nil, customerAvatar: String? = nil) {
Task {
do {
let response = try await dub.trackSale(customerExternalId: customerExternalId, amount: amount, currency: currency, eventName: eventName, customerName: customerName, customerEmail: customerEmail, customerAvatar: customerAvatar)
} catch let error as DubError {
print(error.localizedDescription)
}
}
}
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

UIKit conversion examples reference undefined dub instance

Use Dub.shared in UIKit instead of dub.

Apply this diff:

-                let response = try await dub.trackLead(customerExternalId: customerExternalId, name: name, email: email)
+                let response = try await Dub.shared.trackLead(customerExternalId: customerExternalId, name: name, email: email)
@@
-                let response = try await dub.trackSale(customerExternalId: customerExternalId, amount: amount, currency: currency, eventName: eventName, customerName: customerName, customerEmail: customerEmail, customerAvatar: customerAvatar)
+                let response = try await Dub.shared.trackSale(customerExternalId: customerExternalId, amount: amount, currency: currency, eventName: eventName, customerName: customerName, customerEmail: customerEmail, customerAvatar: customerAvatar)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```swift iOS (UIKit) expandable
// ViewController.swift
import UIKit
import Dub
class ViewController: UIViewController {
// View controller lifecycle
private func trackLead(customerExternalId: String, name: String, email: String) {
Task {
do {
let response = try await dub.trackLead(customerExternalId: customerExternalId, name: name, email: email)
} catch let error as DubError {
print(error.localizedDescription)
}
}
}
private func trackSale(customerExternalId: String, amount: Int, currency: String = "usd", eventName: String? = "Purchase", customerName: String? = nil, customerEmail: String? = nil, customerAvatar: String? = nil) {
Task {
do {
let response = try await dub.trackSale(customerExternalId: customerExternalId, amount: amount, currency: currency, eventName: eventName, customerName: customerName, customerEmail: customerEmail, customerAvatar: customerAvatar)
} catch let error as DubError {
print(error.localizedDescription)
}
}
}
}
```
// ViewController.swift
import UIKit
import Dub
class ViewController: UIViewController {
// View controller lifecycle
private func trackLead(customerExternalId: String, name: String, email: String) {
Task {
do {
let response = try await Dub.shared.trackLead(customerExternalId: customerExternalId, name: name, email: email)
} catch let error as DubError {
print(error.localizedDescription)
}
}
}
private func trackSale(customerExternalId: String, amount: Int, currency: String = "usd", eventName: String? = "Purchase", customerName: String? = nil, customerEmail: String? = nil, customerAvatar: String? = nil) {
Task {
do {
let response = try await Dub.shared.trackSale(customerExternalId: customerExternalId, amount: amount, currency: currency, eventName: eventName, customerName: customerName, customerEmail: customerEmail, customerAvatar: customerAvatar)
} catch let error as DubError {
print(error.localizedDescription)
}
}
}
}
🤖 Prompt for AI Agents
In concepts/deep-links/attribution.mdx around lines 359 to 387, the UIKit
examples call an undefined variable dub; update those calls to use the shared
singleton (Dub.shared) instead (e.g., Dub.shared.trackLead(...) and
Dub.shared.trackSale(...)) so the examples compile; keep imports as-is and only
replace occurrences of dub with Dub.shared inside the Task blocks.

Comment on lines +198 to +250
```typescript React Native expandable
import { useState, useEffect, useRef } from "react";
import { Linking } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import dub from "@dub/react-native";

export default function App() {
useEffect(() => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});

// Check if this is first launch
const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");

if (isFirstLaunch === null) {
await handleFirstLaunch();
await AsyncStorage.setItem("is_first_launch", "false");
} else {
// Handle initial deep link url (Android only)
const url = await Linking.getInitialURL();

if (url) {
await handleDeepLink(url);
}
}

const linkingListener = Linking.addEventListener("url", (event) => {
handleDeepLink(event.url);
});

return () => {
linkingListener.remove();
};
}, []);

const handleFirstLaunch = async (
deepLinkUrl?: string | null | undefined
): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);

const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};

// Return your app...
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

RN example: async useEffect and undefined handleDeepLink

Same fixes as above: wrap awaits in async init and add handleDeepLink.

Apply this diff to the effect:

   useEffect(() => {
-    dub.init({
-      publishableKey: "<DUB_PUBLISHABLE_KEY>",
-      domain: "<DUB_DOMAIN>",
-    });
-    // Check if this is first launch
-    const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
-    if (isFirstLaunch === null) {
-      await handleFirstLaunch();
-      await AsyncStorage.setItem("is_first_launch", "false");
-    } else {
-      // Handle initial deep link url (Android only)
-      const url = await Linking.getInitialURL();
-      if (url) {
-        await handleDeepLink(url);
-      }
-    }
-    const linkingListener = Linking.addEventListener("url", (event) => {
-      handleDeepLink(event.url);
-    });
-    return () => {
-      linkingListener.remove();
-    };
+    const init = async () => {
+      dub.init({
+        publishableKey: "<DUB_PUBLISHABLE_KEY>",
+        domain: "<DUB_DOMAIN>",
+      });
+      const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
+      if (isFirstLaunch === null) {
+        await handleFirstLaunch();
+        await AsyncStorage.setItem("is_first_launch", "false");
+      } else {
+        const url = await Linking.getInitialURL();
+        if (url) await handleDeepLink(url);
+      }
+    };
+    init();
+    const linkingListener = Linking.addEventListener("url", (event) => {
+      handleDeepLink(event.url);
+    });
+    return () => linkingListener.remove();
   }, []);

And add handleDeepLink:

   const handleFirstLaunch = async (
@@
   };
+
+  const handleDeepLink = async (deepLinkUrl: string): Promise<void> => {
+    try {
+      const response = await dub.trackOpen(deepLinkUrl);
+      const destinationURL = response.link?.url;
+      // Navigate to the destination URL
+    } catch (error) {
+      // Handle error
+    }
+  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```typescript React Native expandable
import { useState, useEffect, useRef } from "react";
import { Linking } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import dub from "@dub/react-native";
export default function App() {
useEffect(() => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});
// Check if this is first launch
const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
if (isFirstLaunch === null) {
await handleFirstLaunch();
await AsyncStorage.setItem("is_first_launch", "false");
} else {
// Handle initial deep link url (Android only)
const url = await Linking.getInitialURL();
if (url) {
await handleDeepLink(url);
}
}
const linkingListener = Linking.addEventListener("url", (event) => {
handleDeepLink(event.url);
});
return () => {
linkingListener.remove();
};
}, []);
const handleFirstLaunch = async (
deepLinkUrl?: string | null | undefined
): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);
const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};
// Return your app...
}
```
import { useState, useEffect, useRef } from "react";
import { Linking } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import dub from "@dub/react-native";
export default function App() {
useEffect(() => {
const init = async () => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});
const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
if (isFirstLaunch === null) {
await handleFirstLaunch();
await AsyncStorage.setItem("is_first_launch", "false");
} else {
const url = await Linking.getInitialURL();
if (url) await handleDeepLink(url);
}
};
init();
const linkingListener = Linking.addEventListener("url", (event) => {
handleDeepLink(event.url);
});
return () => linkingListener.remove();
}, []);
const handleFirstLaunch = async (
deepLinkUrl?: string | null | undefined
): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);
const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};
const handleDeepLink = async (deepLinkUrl: string): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);
const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};
// Return your app...
}

Comment on lines +41 to +56
**Option 2**: Manually initialize the Dub SDK

```typescript
import dub from "@dub/react-native";

export default function App() {
useEffect(() => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});
}, []);

// Return your app...
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing React import for useEffect in manual init snippet

useEffect is used but not imported; snippet won’t compile as-is.

Apply this diff:

-import dub from "@dub/react-native";
+import { useEffect } from "react";
+import dub from "@dub/react-native";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Option 2**: Manually initialize the Dub SDK
```typescript
import dub from "@dub/react-native";
export default function App() {
useEffect(() => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});
}, []);
// Return your app...
}
```
**Option 2**: Manually initialize the Dub SDK
🤖 Prompt for AI Agents
In snippets/dub-client-react-native-install.mdx around lines 41 to 56 the
example calls useEffect but does not import it, which prevents the snippet from
compiling; add a top-level import for React and useEffect (e.g., import React, {
useEffect } from "react") to the code example so the hook is available in the
snippet.

Comment on lines +71 to +117
export default function App() {
useEffect(() => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});

// Check if this is first launch
const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");

if (isFirstLaunch === null) {
await handleFirstLaunch();
await AsyncStorage.setItem("is_first_launch", "false");
} else {
// Handle initial deep link url (Android only)
const url = await Linking.getInitialURL();

if (url) {
await handleDeepLink(url);
}
}

const linkingListener = Linking.addEventListener("url", (event) => {
handleDeepLink(event.url);
});

return () => {
linkingListener.remove();
};
}, []);

const handleFirstLaunch = async (
deepLinkUrl?: string | null | undefined
): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);

const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};

// Return your app...
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

RN example: async useEffect and undefined handleDeepLink

  • await used directly in useEffect callback (invalid).
  • handleDeepLink is referenced but not defined.

Apply this diff to fix both:

   useEffect(() => {
-    dub.init({
-      publishableKey: "<DUB_PUBLISHABLE_KEY>",
-      domain: "<DUB_DOMAIN>",
-    });
-
-    // Check if this is first launch
-    const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
-
-    if (isFirstLaunch === null) {
-      await handleFirstLaunch();
-      await AsyncStorage.setItem("is_first_launch", "false");
-    } else {
-      // Handle initial deep link url (Android only)
-      const url = await Linking.getInitialURL();
-
-      if (url) {
-        await handleDeepLink(url);
-      }
-    }
-
-    const linkingListener = Linking.addEventListener("url", (event) => {
-      handleDeepLink(event.url);
-    });
-
-    return () => {
-      linkingListener.remove();
-    };
+    const init = async () => {
+      dub.init({
+        publishableKey: "<DUB_PUBLISHABLE_KEY>",
+        domain: "<DUB_DOMAIN>",
+      });
+
+      const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
+      if (isFirstLaunch === null) {
+        await handleFirstLaunch();
+        await AsyncStorage.setItem("is_first_launch", "false");
+      } else {
+        const url = await Linking.getInitialURL();
+        if (url) {
+          await handleDeepLink(url);
+        }
+      }
+    };
+    init();
+
+    const linkingListener = Linking.addEventListener("url", (event) => {
+      handleDeepLink(event.url);
+    });
+    return () => linkingListener.remove();
   }, []);

And add handleDeepLink:

   const handleFirstLaunch = async (
     deepLinkUrl?: string | null | undefined
   ): Promise<void> => {
@@
   };
+
+  const handleDeepLink = async (deepLinkUrl: string): Promise<void> => {
+    try {
+      const response = await dub.trackOpen(deepLinkUrl);
+      const destinationURL = response.link?.url;
+      // Navigate to the destination URL
+    } catch (error) {
+      // Handle error
+    }
+  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function App() {
useEffect(() => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});
// Check if this is first launch
const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
if (isFirstLaunch === null) {
await handleFirstLaunch();
await AsyncStorage.setItem("is_first_launch", "false");
} else {
// Handle initial deep link url (Android only)
const url = await Linking.getInitialURL();
if (url) {
await handleDeepLink(url);
}
}
const linkingListener = Linking.addEventListener("url", (event) => {
handleDeepLink(event.url);
});
return () => {
linkingListener.remove();
};
}, []);
const handleFirstLaunch = async (
deepLinkUrl?: string | null | undefined
): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);
const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};
// Return your app...
}
```
export default function App() {
useEffect(() => {
const init = async () => {
dub.init({
publishableKey: "<DUB_PUBLISHABLE_KEY>",
domain: "<DUB_DOMAIN>",
});
const isFirstLaunch = await AsyncStorage.getItem("is_first_launch");
if (isFirstLaunch === null) {
await handleFirstLaunch();
await AsyncStorage.setItem("is_first_launch", "false");
} else {
const url = await Linking.getInitialURL();
if (url) {
await handleDeepLink(url);
}
}
};
init();
const linkingListener = Linking.addEventListener("url", (event) => {
handleDeepLink(event.url);
});
return () => linkingListener.remove();
}, []);
const handleFirstLaunch = async (
deepLinkUrl?: string | null | undefined
): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);
const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};
const handleDeepLink = async (deepLinkUrl: string): Promise<void> => {
try {
const response = await dub.trackOpen(deepLinkUrl);
const destinationURL = response.link?.url;
// Navigate to the destination URL
} catch (error) {
// Handle error
}
};
// Return your app...
}

@steven-tey steven-tey merged commit 825f8c8 into main Sep 24, 2025
2 checks passed
@steven-tey steven-tey deleted the fix-ios-docs-and-add-uikit-and-rn branch September 24, 2025 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants