Deep Blue Links
Documentation menu

Capacitor

Open your Capacitor app from its links, resolve each link with the API, and find the link that led to an install.

This guide uses @capacitor/app for incoming links and @capgo/capacitor-install-referrer for the Android install referrer. Replace demo with your app's name and YOUR_API_KEY with its key: the sandbox key in debug builds and the live key in release builds.

npm install @capacitor/app @capgo/capacitor-install-referrer
npx cap sync

1. Set up iOS and Android

iOS: open ios/App/App.xcodeproj in Xcode (ios/App/App.xcworkspace in projects created before Capacitor 8, which use CocoaPods), select the App target, open Signing & Capabilities, add Associated Domains and enter applinks:demo.dev.thedbl.ink, plus one entry per custom domain. If your app has a URL scheme, add it under Info › URL Types too.

Android: in android/app/src/main/AndroidManifest.xml, add a verified intent filter to the main activity, plus a filter for your URL scheme if your app has one:

android/app/src/main/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="demo.dev.thedbl.ink" />
</intent-filter>

<intent-filter>
    <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>

Your app's settings must have the iOS team and bundle IDs and the Android package and signing fingerprints the builds use.

Your web code calls the API with fetch; the API accepts requests from any origin, including your app's WebView.

src/deepBlueLinks.ts
import { App } from '@capacitor/app'

const API_KEY = 'YOUR_API_KEY'

export type ResolvedLink = { id: number; longUrl: string; shortUrl: string; isSandbox?: boolean }

export async function post(path: string, body: unknown): Promise<ResolvedLink | null> {
  const response = await fetch(`https://dev.thedbl.ink${path}`, {
    method: 'POST',
    headers: { 'Api-Key': API_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
  if (response.status === 404) return null
  if (!response.ok) throw new Error(`Deep Blue Links answered ${response.status}`)
  return response.json()
}

export const resolveLink = (url: string) => post('/api/links/resolve', { url })

let lastUrl: string | undefined

async function openUrl(url: string) {
  if (url === lastUrl) return
  lastUrl = url
  const opened = new URL(url)
  if (opened.protocol === 'myapp:') {
    router.open('/' + opened.host + opened.pathname + opened.search)
    return
  }
  let link: ResolvedLink | null = null
  try {
    link = await resolveLink(url)
  } catch (error) {
    console.warn(`Couldn't resolve ${url}`, error)
  }
  const destination = new URL(link?.longUrl ?? url)
  router.open(destination.pathname + destination.search)
}

export function listenForLinks() {
  App.addListener('appUrlOpen', ({ url }) => openUrl(url))
  App.getLaunchUrl().then((launch) => { if (launch?.url) openUrl(launch.url) })
}

Call listenForLinks() when your app starts. Both calls can report the URL that launched the app, so openUrl skips a URL it just handled. router stands for your app's navigation. A 404 from resolve means the URL isn't one of your short links, such as a link's full URL, so it's routed as it is, and so is a URL whose resolve fails, such as with no network. The myapp: branch handles your URL scheme, whose host is the link's first path segment. When link.isSandbox is true, load the destination's content from your sandbox backend.

On Android, read the short link from the Play install referrer. Elsewhere, call check-deferred with the timezone and the screen in physical pixels, measured the same way as the preview page measures it:

src/deepBlueLinks.ts
import { Capacitor } from '@capacitor/core'
import { InstallReferrer } from '@capgo/capacitor-install-referrer'

export async function findInstallLink(): Promise<ResolvedLink | null> {
  if (Capacitor.getPlatform() === 'android') {
    const result = await InstallReferrer.getReferrer()
    const link = new URLSearchParams(result.referrer ?? '').get('link')
    return link ? resolveLink(link) : null
  }
  return post('/api/links/check-deferred', {
    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    device: {
      screenWidth: Math.round(screen.width * devicePixelRatio),
      screenHeight: Math.round(screen.height * devicePixelRatio),
      language: navigator.language,
    },
  })
}

Call findInstallLink() once, on the first launch (store a flag, for example in localStorage or with @capacitor/preferences), and route like a resolved link when it returns one. A null result is normal: most launches don't follow a link.

Test it

  • iOS universal links work on a device: paste a short link into Notes and tap it.
  • On Android, check verification with adb shell pm get-app-links com.example.demo, then open a link with adb shell am start -a android.intent.action.VIEW -d "https://demo.dev.thedbl.ink/Xy3kP9aQ".
  • The install referrer is only present for installs from Google Play, such as an internal testing track opened from a link's preview page.