Agent Skills
Hand an integration to your coding agent instead of doing it by hand. Each skill is one file: install it, name the SDK, and let the agent scan, plan, implement, and validate.
A skill is a runbook the agent loads before it touches your code. It defines what to read first, what to ask (all at once, not one question at a time), what to write, and what has to pass before the work counts as done. The result is an integration that matches your existing router and state management instead of a generic snippet you then have to rewrite.
The runbooks are agent-agnostic. Claude Code, Codex, and Cursor differ in where the file goes, not in what it says — so one text covers all of them.
Choose your coding agent, SDKs, and install scope when prompted; the installer downloads, validates, and places every selected skill for you.
curl -fsSL https://www.deeplinkly.com/install-skill | bashRun the command from your project in macOS, Linux, WSL, or Git Bash. It asks whether you use Claude Code, Codex, Cursor, or all three; whether to install Flutter, Android, iOS, React Native, or all four SDK skills; and whether they should be available in this project or every project for your user. Project scope is recommended because the skills can be reviewed and committed with the code they change.
Using CI or a setup script?
Skip the prompts by passing the agent, SDK selection, and scope:
curl -fsSL https://www.deeplinkly.com/install-skill | bash -s -- --agent codex --sdk all --scope projectValid agents are claude, codex, cursor, and all. Valid SDK values are flutter, android, ios, react-native, and all. Add --force to replace existing copies without a prompt.
Install locations
The installer chooses these project paths automatically. Selecting user scope uses the matching directory under ~ instead.
| Agent | Path | Notes |
|---|---|---|
| Claude Code | .claude/skills/deeplinkly-<sdk>/SKILL.md | Project scope. For every project, use ~/.claude/skills/<slug>/SKILL.md. Invoke it with /<slug>, or let Claude match it. |
| Codex | .agents/skills/deeplinkly-<sdk>/SKILL.md | Project scope. For every project, use ~/.agents/skills/<slug>/SKILL.md. Invoke it with $<slug>, or let Codex match it. |
| Cursor | .cursor/skills/deeplinkly-<sdk>/SKILL.md | Project scope. For every project, use ~/.cursor/skills/<slug>/SKILL.md. Cursor discovers it from the skill metadata. |
| Any other agent | — | Paste the runbook into the chat, or point the agent at the raw Markdown URL and let it fetch. |
Prefer a manual install?
Download any raw skill from /skills/deeplinkly-flutter, /skills/deeplinkly-android, /skills/deeplinkly-ios, or /skills/deeplinkly-react-native, then save it as SKILL.md at the matching path. Each raw file and the runbook shown below use the same source.
Flutter SDK
deeplinkly-flutter
Full integration of flutter_deeplinkly 1.9.x: dependency, native platform setup, deep link routing, deferred attribution, identity, events, and link generation.
- Scans pubspec.yaml, main.dart, AndroidManifest.xml, and Info.plist before asking anything
- Carries the exact per-platform key contract, so it cannot write the DEEPLINKLY_API_KEY that neither platform reads
- Produces an evidence-backed App Settings checklist with proven package, signing, bundle, team, scheme, store, and domain values
- Wires routing through your existing navigator (GoRouter, AutoRoute, Beamer, or Navigator 1.0)
- Runs a validation matrix — cold start, warm start, deferred install, offline — before declaring done
- Guardrails against ATT prompts, fingerprint matching, and click-time signal collection
Android SDK
deeplinkly-android
Full native Android integration: Gradle dependency, manifest API key and domains, verified App Links, cold- and warm-start Intent routing, deferred attribution, identity, events, link generation, privacy controls, and release validation.
- Inspects Gradle files, AndroidManifest.xml, application startup, and navigation before changing code
- Produces an evidence-backed Android App Settings checklist without guessing package, signing, store, scheme, fallback, or domain values
- Configures the exact manifest metadata, intent filters, launch modes, and assetlinks.json contract
- Handles Play Install Referrer attribution plus cold-start, warm-start, and duplicate Intent delivery
- Validates identity, custom events, generated links, privacy levels, offline behavior, and release builds
iOS SDK
deeplinkly-ios
Full native iOS integration: SwiftPM or CocoaPods, Info.plist configuration, Associated Domains and AASA, UIKit or SwiftUI link routing, deferred attribution, identity, events, link generation, privacy controls, and release validation.
- Inspects the Xcode project, app lifecycle, entitlements, Info.plist, and routing architecture first
- Produces an evidence-backed iOS App Settings checklist without guessing bundle, team, store, scheme, fallback, or domain values
- Wires Universal Links through AppDelegate, SceneDelegate, or SwiftUI without duplicate navigation
- Configures first-launch deferred attribution, including pasteboard behavior and the optional paste button
- Checks privacy manifests, IDFA opt-in boundaries, identity, events, generated links, and device testing
React Native SDK
deeplinkly-react-native
Full react-native-deeplinkly integration for Android and iOS: package and CocoaPods setup, native API-key configuration, App Links and Universal Links, delegate forwarding, JavaScript routing, deferred attribution, identity, events, and link generation.
- Inspects package.json, the React Native architecture, navigation, Gradle, AndroidManifest.xml, and the iOS host
- Produces one evidence-backed Android and iOS App Settings checklist and requests all unknown dashboard choices together
- Handles Android Kotlin compatibility, verified App Links, Intent delivery, and Install Referrer attribution
- Wires iOS AppDelegate or SceneDelegate forwarding, Associated Domains, AASA, and deferred attribution
- Connects native delivery to the existing JavaScript navigator and validates both platforms end to end
Runbook
Full integration of flutter_deeplinkly 1.9.x: dependency, native platform setup, deep link routing, deferred attribution, identity, events, and link generation.
---
name: deeplinkly-flutter
description: Integrate, debug, or productionize flutter_deeplinkly in a Flutter app. Use when installing or fixing Deeplinkly, deep-link routing, Android or iOS native setup, deferred attribution, identity, events, link generation, validation, or troubleshooting.
---
# Deeplinkly Flutter SDK Autonomous Integrator
Use this skill when an AI agent must implement, debug, or productionize `flutter_deeplinkly` with minimal human back-and-forth.
## Mission
Complete deep link and deferred deep link integration end-to-end, including platform setup, runtime routing, attribution, event tracking, and verification.
## Step 0: Infer user intent automatically
Classify the request into one or more goals:
- **Integration**: words like install, setup, add SDK, onboarding
- **Routing**: words like open screen, navigate, params missing, wrong page
- **Attribution**: words like campaign, install source, deferred missing
- **Analytics**: words like event tracking, conversion, purchase tracking
- **Link generation**: words like create share link, referral link, invite link
- **Stability**: words like flaky, duplicate events, race condition, resume bug
If intent is broad or ambiguous, run the full integration checklist.
## Step 1: Gather context from codebase
Before asking the user, inspect:
- `pubspec.yaml` for package presence/version
- app startup path (`main.dart`, app entrypoint)
- navigator pattern (`Navigator`, `go_router`, `auto_route`, Router API)
- Android manifest and iOS plist setup
- existing analytics abstraction and user auth flow
Ask user only for values not inferable from project files (API key, domain/scheme confirmation).
Treat `pubspec.lock` as the resolved package version. A constraint such as
`flutter_deeplinkly: ^1.6.0` resolving to 1.11.0 is normal semver behavior, not
a documentation conflict. Use the exact contracts in this skill for released
1.11.x integrations. Inspect package internals only when compilation or runtime
behavior directly contradicts one of them; do not crawl the changelog and
native source merely to reconfirm documented defaults.
## Step 1b: Give the user a dashboard handoff
After the initial scan, tell the user to open
`https://www.deeplinkly.com/dashboard/app-settings`. Provide one consolidated
`Dashboard setup required` checklist containing only values proven by the
project. The agent cannot edit the user's dashboard and must not imply those
settings are complete merely because native files were changed.
Every suggested value must name its evidence, such as a file and key or exact
command output. Do not suggest placeholders, example values, unresolved build
variables, or a value inferred only from an app/display name. If build flavors
or configurations resolve differently, list the proven values by variant and
ask which deployed app the user is configuring.
### Values eligible for a certain recommendation
**API key**
- Never infer or generate a key. Tell the user to copy an active Production API
key from App Settings and put the same key in AndroidManifest.xml and
Info.plist using the exact platform keys below.
**Android Setup**
- **Package name**: suggest the resolved application ID for the target variant.
Prefer Gradle's resolved value over the Java/Kotlin namespace. Account for
applicationIdSuffix; list multiple variants rather than collapsing them.
- **SHA-256 Fingerprints**: suggest only fingerprints returned for the intended
signing configuration or supplied from Play Console → Setup → App signing.
Label debug, upload, and Play app-signing certificates accurately. Never
present a debug/upload fingerprint as the production Play fingerprint.
- **App source / download URL**: suggest only an exact Play listing or custom
download URL already present in project/release metadata. Otherwise tell the
user to select the published app or enter its real download URL.
- **Custom URI Scheme**: suggest only a literal scheme already declared in an
Android VIEW intent filter. Preserve the dashboard format, for example
`myapp://`; omit the field when no custom scheme exists.
**iOS Setup**
- **Bundle Identifiers**: suggest concrete PRODUCT_BUNDLE_IDENTIFIER values
resolved for the target configurations. Do not output `$(...)` variables.
- **Team ID**: suggest a concrete 10-character DEVELOPMENT_TEAM value from the
signed target/build settings. Do not derive it from the bundle ID or company
name.
- **App source / App Store app / custom URL**: suggest only an exact App Store
ID or URL found in project/release metadata. Otherwise ask the user to select
the published app or enter its real fallback URL.
- **URL Scheme**: suggest only a literal CFBundleURLSchemes entry used by the
target. Preserve the dashboard's `myapp://` format.
- **Universal Links**: recommend enabling only when the signed target contains
an `applinks:` Associated Domains entry for the Deeplinkly link host. Do
not guess the separate Native Links toggle.
**Shared settings**
- **Default URL**: suggest only an explicit production web fallback found in
project or release configuration. Do not manufacture one from the app name
or link domain.
- **Link domain**: if one exact Deeplinkly host is already configured in native
files, tell the user to ensure it exists under Dashboard → Domains and use
that same host everywhere. If hosts conflict or contain placeholders, show
the conflict and ask the user which dashboard domain is authoritative.
Start the block with `Dashboard setup required`. Group proven rows under
`Android Setup`, `iOS Setup`, or `Shared settings`, using the format
`- Field: exact value — evidence`. Omit every unproven value row. End with a
`Still needed from you` group that tells the user where to obtain each
missing value without proposing one.
Ask for all missing dashboard choices in that single block. Continue with safe
code changes while the user completes the dashboard, but keep dashboard-dependent
verification explicitly pending.
## Step 2: Execute implementation flow
1. Ensure package is installed and compatible with Flutter SDK.
2. Add `FlutterDeeplinkly.init()` before `runApp()`.
3. Register `deepLinkStream` listener in startup lifecycle.
4. Implement deeplink payload normalizer:
- input: raw map
- output: `{ target, params, source, fallback }`
5. Route safely using centralized resolver (idempotent, no duplicate navigations).
6. Add Android intent filter + `com.deeplinkly.sdk.api_key` `<meta-data>`.
7. iOS setup (all four, none optional — see the platform-key contract below):
- `DeeplinklyApiKey` in `Info.plist`
- `DeeplinklyLinkDomains` array in `Info.plist`, one entry per link domain
- **Associated Domains** capability with `applinks:<host>` per domain, and
`CODE_SIGN_ENTITLEMENTS` actually pointing at the entitlements file
- `CFBundleURLTypes` only if the project uses a custom scheme
8. Add identity bind call `setUserId()` post-login.
9. Add attribution pull via `getInstallAttribution()` and pass to analytics context.
10. Bind the conversion-matching fields where the app already knows them —
at sign-up, at checkout — with `setUserData()`. Every field is optional
and calls merge; a malformed field rejects the whole call and stores
nothing. Pass the app's own product-analytics ids through `customData`.
Call `clearUserData()` on sign-out or consent withdrawal.
11. Report revenue with `logPurchase()` rather than a hand-rolled
`logEvent('purchase', ...)`; pass `orderId` wherever the app has one.
12. Add event wrapper around `logEvent()` with pre-validation.
13. Add link creation helper via `generateLink()`.
14. Add optional debug logging toggle for non-production builds.
## Step 2b: Platform key contract
These names are exact and are a frequent source of silent failure. Do not
normalize them to look consistent with each other — they are genuinely
different on each platform.
| Purpose | Android | iOS |
|---|---|---|
| API key | `com.deeplinkly.sdk.api_key` (`<meta-data>`) | `DeeplinklyApiKey` (`Info.plist`) |
| Link domain allowlist | `com.deeplinkly.sdk.link_domains` (`<meta-data>`, comma separated) | `DeeplinklyLinkDomains` (`Info.plist`, array) |
| Deferred mechanism | Play Install Referrer | Automatic pasteboard read or `DeeplinklyPasteButton` |
| Pasteboard opt-out | n/a — Android has no pasteboard path | `DeeplinklyCheckPasteboardOnInstall` |
| Attribution level | `com.deeplinkly.sdk.attribution_level` | `DeeplinklyAttributionLevel` |
Never write `DEEPLINKLY_API_KEY`. It is read by neither platform; docs
carried it in error until 1.8.0.
**The automatic pasteboard read is on by default.** It does not need
enabling. Still prefer steering the integrator to `DeeplinklyPasteButton` (a
system paste button, iOS 16+, no "Pasted from…" banner because the user's tap is
the grant), and turning the automatic read off in `Info.plist`. Where the app
genuinely cannot show a button on its first-run path, leave the automatic read
on and pair it with `willShowPasteboardBanner()` so the prompt is explained
rather than sprung.
### Released 1.11.x pasteboard source of truth
Do not treat this contract as unresolved:
- With no `DeeplinklyCheckPasteboardOnInstall` entry, the automatic read is
**on**. Released native code evaluates the missing value as `true`.
- Setting the Info.plist value to `false` disables the automatic read before
plugin registration. A Dart call is too late to prevent that first read.
- `flutter_deeplinkly` 1.9.0 and 1.9.1 shipped a stale sentence in
`example/README.md` claiming the default is off. That sentence is wrong;
do not pause implementation to reconcile it with the root README, changelog,
or native source.
Do **not** add `AppDelegate.swift` link-forwarding code. The plugin registers
for both the `UIApplicationDelegate` and `UIScene` callbacks itself, and hand
-wiring `handleUniversalLink` on top of that delivers the link twice. If the
project already has such code from an older integration, remove it.
## Step 3: API behavior contract
Implement these APIs with explicit responsibilities:
- `FlutterDeeplinkly.init()` initializes bridge and lifecycle hooks.
- `deepLinkStream` emits payloads for deep/deferred links.
- `getInstallAttribution()` returns campaign/install metadata map.
- `getDeeplinklyId()` returns stable install-level Deeplinkly id.
- `setUserId()` maps app user to Deeplinkly identity graph.
- `setUserData()` merges the conversion-matching fields plus a `customData`
map of the app's own ids. All-or-nothing: one malformed field stores none of
them. Nothing is validated or hashed in Dart — the caps (10 entries, 64-char
keys, 256-char values) and the normalisation live natively, so there is one
implementation of each rule rather than three that can drift.
- `clearUserData()` erases everything `setUserData` and `setUserId`
recorded, on the device and on Deeplinkly's servers. Not merely "stop
sending": it nulls the columns, and is re-sent until delivered.
- `logEvent()` sends conversion events with strict schema constraints.
- `logPurchase()` is a typed wrapper over `logEvent` that fixes the one
spelling of value and currency that both Meta's Conversions API and Google's
enhanced conversions are built from. Negative or non-finite values, a currency
that is not three letters, and reserved keys in `parameters` are rejected.
- `setPIIHashingEnabled()` / `isPIIHashingEnabled()` toggle on-device
SHA-256 hashing of email, phone and names.
- `generateLink()` creates campaign links from content + options.
## Step 4: Validation and test matrix
Run and record these checks:
- Cold-start deep link -> app opens target screen
- Warm-start deep link -> active session routes correctly
- Fresh install deferred flow -> attribution data available
- Login flow -> `setUserId()` is called once per user switch
- Event logging -> valid payload accepted, invalid payload rejected gracefully
- Link generation -> returned URL opens expected destination
- Offline/timeout behavior -> no app crash, safe fallback path
## Step 5: Troubleshooting decision tree
- **No deep link callbacks**:
- verify `init()` timing
- verify Android/iOS config values
- verify URL scheme/domain consistency
- **Wrong route opened**:
- inspect normalizer mapping and default route selection
- **Deferred attribution empty**:
- check retrieval timing and first-launch lifecycle order
- **iOS**: confirm the pasteboard path is actually available — the automatic
read is **on by default**, but an app that set
`DeeplinklyCheckPasteboardOnInstall` to `false` without adding a
`DeeplinklyPasteButton` has no deferred path at all
- **iOS**: confirm the link host is listed in `DeeplinklyLinkDomains`; an
unlisted custom domain is ignored by design
- check the attribution level: `none` sends no enrichment, so attribution
will look empty server-side even though the link resolved correctly
- **iOS**: the deferred read is once-per-install and the pasteboard is
cleared after it. Reinstall to retest — relaunching will not repeat it
- **iOS**: cannot be tested on the Simulator (no App Store). Use a device
- **iOS**: the interstitial requires a tap; there is no auto-redirect,
because Safari will not write the clipboard without a user gesture
- **Android**: Install Referrer is unavailable on sideloaded builds; test
through Play internal testing
- **Events fail**:
- enforce constraints (`name <= 64`, `params <= 25`, key/value limits)
- **Duplicate opens on resume**:
- add dedupe key and idempotent route gate
## Guardrails
- Never print API keys, auth tokens, or user-sensitive params in logs.
- Do not block app boot on network operations.
- Keep all deeplink handling null-safe and exception-safe.
- Prefer deterministic fallbacks over hard failures.
- Keep changes minimal and compatible with existing app architecture.
- Do not call `ATTrackingManager.requestTrackingAuthorization` on Deeplinkly's
behalf, and do not add `NSUserTrackingUsageDescription` unless the app has
opted into IDFA collection. The SDK never prompts; it reads the status the
app's own prompt produced. IDFA collection is off unless the app sets
`DeeplinklyEnableIDFA`, and enabling it obliges the app to merge the SDK's
`Resources/IDFA/PrivacyInfo.xcprivacy` template and declare tracking itself.
- Do not implement device-signal fingerprint *matching*. Signals are collected
for reporting; attribution is deterministic on the click id or install
referrer. Deriving a device identifier from device signals is prohibited by
App Review Guideline 5.1.2 and the Apple Developer Program License Agreement.
- Do not turn on `setPIIHashingEnabled` as a default "more private" choice.
It is off by design and costs match quality: the digest is computed once under
one normalisation, while advertising destinations disagree about phone
formatting, so a conversion forwarded to a destination whose rules differ will
not match and the service can no longer re-derive per destination. Enable it
only when the user names a compliance requirement that plaintext must not
reach a processor. Gender, country and date of birth are deliberately not
hashed — their value ranges are small enough to reverse a digest by
enumeration.
- Do not collect device signals at click time, in the browser or the
interstitial. All collection is in-app and post-install; that distinction is
the legal basis for it.
## Done criteria
- Integration works on Android and iOS.
- Deep/deferred links route correctly on cold and warm starts.
- Attribution and identity mapping are live.
- Event tracking and link generation are functional.
- Integration docs/snippets are updated for future maintainers.
Runbook
Full native Android integration: Gradle dependency, manifest API key and domains, verified App Links, cold- and warm-start Intent routing, deferred attribution, identity, events, link generation, privacy controls, and release validation.
---
name: deeplinkly-android
description: Integrate, debug, or productionize the native Deeplinkly Android SDK in a Kotlin or Java app. Use when installing or fixing Deeplinkly, verified App Links, Intent routing, Play Install Referrer deferred attribution, identity, events, link generation, privacy controls, testing, or release validation.
---
# Deeplinkly Android SDK integrator
Implement the native Android integration end to end. Adapt it to the existing application, activity, and navigation architecture instead of pasting a demo structure over the app.
## Inspect before editing
Read:
- settings.gradle or settings.gradle.kts and project/app Gradle files
- AndroidManifest.xml in every relevant source set
- the Application class and startup path
- activities that receive VIEW intents and their launch modes
- the navigation/router implementation
- existing consent, analytics, identity, and build-variant configuration
Ask only for values that cannot be inferred: the Deeplinkly API key, production link domain, custom scheme, package name, and signing fingerprint. The API key comes from Deeplinkly Dashboard → App Settings.
## Give the user a dashboard handoff
After inspection, tell the user to open
https://www.deeplinkly.com/dashboard/app-settings and provide one consolidated
`Dashboard setup required` checklist. Suggest only values proven by a file,
resolved build configuration, exact command output, or release metadata, and
cite that evidence beside every value. Never suggest placeholders, unresolved
Gradle variables, values derived from the display name, or guesses.
Eligible Android Setup values:
- **Package name**: use the resolved applicationId for the target variant, not
the namespace. Account for applicationIdSuffix and list distinct flavors.
- **SHA-256 Fingerprints**: use only the intended signing configuration's
verified output or a fingerprint supplied from Play Console → Setup → App
signing. Label debug, upload, and Play app-signing certificates accurately;
never present debug/upload as the production Play fingerprint.
- **App source / download URL**: suggest only an exact Play listing or custom
URL already present in release metadata. Otherwise ask the user to select or
enter the real published destination.
- **Custom URI Scheme**: suggest only a literal scheme declared in a VIEW
intent filter. Format it as the dashboard expects, such as `scheme://`.
- **Default URL**: suggest only an explicit production web fallback. Do not
manufacture one from the application ID, app name, or link domain.
- **Link domain**: when one literal Deeplinkly host is already configured, tell
the user to ensure that same host exists under Dashboard → Domains. If hosts
conflict or contain placeholders, show the conflict and ask which dashboard
domain is authoritative.
Never infer or generate the API key. Under `Still needed from you`, tell the
user to copy an active Production API key from App Settings and place it under
`com.deeplinkly.sdk.api_key`. Format each proven row as
`- Field: exact value — evidence`; omit unproven rows and request all missing
dashboard choices in one block. Keep dashboard-dependent verification pending
until the user confirms those settings.
## Integration workflow
1. Add the published Android dependency to the app module:
implementation("com.deeplinkly:deeplinkly-android:1.3.0")
2. Add exact application metadata. Do not invent or normalize these names:
<meta-data
android:name="com.deeplinkly.sdk.api_key"
android:value="your_api_key_here" />
<meta-data
android:name="com.deeplinkly.sdk.link_domains"
android:value="links.yourapp.com" />
3. Configure the receiving activity with a verified HTTPS App Link filter using android:autoVerify="true". Keep a custom scheme only when the app needs browser fallback or development links. Use singleTop unless the existing navigation design requires another supported launch mode.
4. Initialize once from Application.onCreate:
Deeplinkly.init(this)
Initialization is explicit and idempotent. Do not add an AndroidX Startup initializer or content provider.
5. Attach one process-level listener and route from link.params through the existing router:
Deeplinkly.setDeepLinkListener { link ->
router.open(link.params)
}
6. Forward warm intents from every receiving activity:
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Deeplinkly.onNewIntent(this, intent)
}
Cold-start intents are captured automatically; do not forward them twice.
7. Verify App Links. Deeplinkly serves:
https://<link-domain>/.well-known/assetlinks.json
Dashboard App Settings must contain the package name and SHA-256 signing certificate fingerprint. With Play App Signing, use the Play signing fingerprint, not the upload-key fingerprint.
8. Bind identity after login and clear it on logout:
Deeplinkly.setUserId("user_123")
Deeplinkly.setUserId(null)
Read first-touch data with getInstallAttribution() and the stable install identifier with getDeeplinklyId().
9. Bind the conversion-matching fields where the app already knows them — at
sign-up, at checkout — with setUserData(). Every field is optional and calls
merge; a malformed field rejects the whole call and stores nothing. Pass your
own product-analytics ids through customData (at most 10 entries, 64-char
keys, 256-char values) rather than waiting for a named field. Call
clearUserData() on sign-out or consent withdrawal; it actively erases rather
than merely stopping, and is re-sent until delivered.
Only suggest fields the app's own privacy policy and consent flow cover. Do
not invent values, and do not read them from storage the user has not
pointed you at.
10. Report revenue with logPurchase(value, currency, orderId, quantity,
productId) rather than a hand-rolled logEvent. It is a typed wrapper over
the same pipeline and it fixes the spelling both Meta's Conversions API and
Google's enhanced conversions are built from. Always pass orderId where the
app has one: it is Google's deduplication key. A refund is a separate event,
not a negative value — negatives are rejected.
11. Wrap logEvent() in the app analytics layer. Enforce: non-empty name up to 64 characters, at most 25 custom parameters, keys up to 64, strings up to 256, and no app-supplied _dl_ keys.
12. Add a focused generateLink() helper for campaign, referral, or sharing flows. Preserve typed content metadata and handle unsuccessful results without force-unwrapping a URL.
13. Integrate consent before first-launch reporting when required. setTrackingEnabled(false) overrides attribution level but must not break functional link delivery. Configure a native default with com.deeplinkly.sdk.attribution_level when UI consent arrives too late.
## Android-specific contracts
- Production routing requires a verified HTTPS filter; a custom scheme is not an App Link substitute.
- link_domains is a comma-separated allowlist. Set it when the app claims mixed-purpose HTTPS domains so ordinary web routes are not mistaken for Deeplinkly codes.
- Deferred linking uses Google Play Install Referrer. It is unavailable in sideloaded builds; test through a Play internal track.
- If a link resolves before the listener exists, the persistent queue delivers it later. Keep routing idempotent because lifecycle retries can redeliver after a listener failure.
- isEnabled remains false when the manifest key is absent. In that state network APIs and link handling are no-ops, while getDeeplinklyId() remains available locally.
## Privacy guardrails
- Never print API keys, full attribution payloads, or user identifiers.
- setPIIHashingEnabled(true) SHA-256s email, phone and names on the device so
plaintext never reaches Deeplinkly. It is off by default and should stay off
unless the user names a compliance requirement that plaintext must not reach a
processor — it costs match quality, because the digest is computed once under
one normalisation while advertising destinations disagree about phone
formatting. Do not enable it as a default "more private" choice; surface the
trade and let the user decide. Gender, country and date of birth are not
hashed by design.
- Do not add the Advertising ID dependency or AD_ID permission unless the host app explicitly opts in and its consent policy permits collection.
- Do not implement probabilistic device fingerprint matching. Attribution is deterministic through click id or Install Referrer.
- Do not collect browser/device signals at click time.
- Keep link routing functional when reporting is reduced or disabled.
## Validation
Run the checks the project supports, then test:
1. Clean build for every changed variant.
2. assetlinks.json returns 200 and adb shell pm get-app-links reports verified.
3. Cold start from a generated HTTPS link.
4. Warm start through onNewIntent.
5. Link while offline, followed by a retry after relaunch.
6. Deferred install through Play internal testing.
7. Unknown or malformed params fall back safely.
8. Login/logout identity binding.
9. Accepted and rejected event payloads.
10. Link generation success and failure.
11. Tracking-disabled and reduced-attribution behavior.
Enable Deeplinkly.setDebugMode(true) only in non-production builds. Use the Deeplinkly Logcat tag and disable verbose logging before completion.
## Troubleshooting
- Link opens a browser: fix intent filter, assetlinks.json, package name, or Play signing fingerprint.
- Cold link works but warm link does not: add onNewIntent forwarding to the receiving activity.
- Deferred attribution is empty: test a fresh Play-installed build, not a sideload.
- SDK stays disabled: confirm the exact manifest key is inside application and available in the active source set.
- Duplicate navigation: dedupe by click id or a stable payload key at the centralized router.
- Events return false: check name, count, key, value, and reserved-prefix validation.
## Done
Do not declare completion until the user has received the evidence-backed dashboard checklist, Android App Links verify, cold and warm routing work, the Play deferred path is tested or explicitly documented as untestable, identity/events/link generation are wired, privacy choices are preserved, and project tests/builds pass.
Runbook
Full native iOS integration: SwiftPM or CocoaPods, Info.plist configuration, Associated Domains and AASA, UIKit or SwiftUI link routing, deferred attribution, identity, events, link generation, privacy controls, and release validation.
---
name: deeplinkly-ios
description: Integrate, debug, or productionize the native Deeplinkly iOS SDK in a Swift, UIKit, or SwiftUI app. Use when installing or fixing Deeplinkly, Universal Links, Associated Domains and AASA, AppDelegate or SceneDelegate routing, pasteboard deferred attribution, identity, events, link generation, privacy, testing, or release validation.
---
# Deeplinkly iOS SDK integrator
Implement the native iOS integration end to end. Preserve the app's existing lifecycle and navigation architecture.
## Inspect before editing
Read:
- Package.swift, project package references, or Podfile
- deployment target and app target membership
- Info.plist, entitlements, and CODE_SIGN_ENTITLEMENTS settings
- AppDelegate, SceneDelegate, and SwiftUI App entry points
- the navigation coordinator/router
- existing consent, ATT, analytics, identity, and privacy-manifest configuration
Ask only for values not present in the project: API key, production link domains, optional custom scheme, Team ID, and bundle ID. The API key comes from Deeplinkly Dashboard → App Settings.
## Give the user a dashboard handoff
After inspection, tell the user to open
https://www.deeplinkly.com/dashboard/app-settings and provide one consolidated
`Dashboard setup required` checklist. Suggest only values proven by a file,
resolved Xcode build setting, signing output, or release metadata, and cite
that evidence beside every value. Never suggest placeholders, unresolved
`$(...)` variables, values derived from the product name, or guesses.
Eligible iOS Setup values:
- **Bundle Identifiers**: use concrete PRODUCT_BUNDLE_IDENTIFIER values
resolved for the target configurations. List distinct app targets/variants.
- **Team ID**: use only a concrete 10-character DEVELOPMENT_TEAM value from the
signed target or resolved build settings. Never derive it from a bundle ID or
company name.
- **App source / App Store app / custom URL**: suggest only an exact App Store
ID/URL or custom destination found in release metadata. Otherwise ask the
user to select or enter the real published destination.
- **URL Scheme**: suggest only a literal CFBundleURLSchemes value used by the
target. Format it as the dashboard expects, such as `scheme://`.
- **Universal Links**: recommend enabling only when the signed target has an
`applinks:` Associated Domains entry for the Deeplinkly host. Do not guess
the dashboard's separate Native Links toggle.
- **Default URL**: suggest only an explicit production web fallback. Do not
manufacture one from the bundle ID, product name, or link domain.
- **Link domain**: when one literal Deeplinkly host is already configured, tell
the user to ensure that same host exists under Dashboard → Domains. If hosts
conflict or contain placeholders, show the conflict and ask which dashboard
domain is authoritative.
Never infer or generate the API key. Under `Still needed from you`, tell the
user to copy an active Production API key from App Settings and place it under
`DeeplinklyApiKey`. Format each proven row as
`- Field: exact value — evidence`; omit unproven rows and request all missing
dashboard choices in one block. Keep dashboard-dependent verification pending
until the user confirms those settings.
## Integration workflow
1. Install Deeplinkly 1.2.1 or later with Swift Package Manager or CocoaPods:
.package(
url: "https://github.com/Deeplinkly/ios_deeplinkly.git",
from: "1.2.1"
)
Link the Deeplinkly product to the app target. The minimum supported deployment target is iOS 12.
2. Add exact Info.plist keys:
<key>DeeplinklyApiKey</key>
<string>your_api_key_here</string>
<key>DeeplinklyLinkDomains</key>
<array>
<string>links.yourapp.com</string>
</array>
Add CFBundleURLTypes only when the app uses a custom scheme.
3. Enable Associated Domains on the signed app target and add one entry per production host:
applinks:links.yourapp.com
Confirm the entitlement file is included in the target and CODE_SIGN_ENTITLEMENTS points to it. Verify the domain's apple-app-site-association file contains the correct Team ID and bundle ID.
4. Create one DeeplinklyDeepLinkListener that normalizes payload params and calls the existing router. Attach it before initialization:
Deeplinkly.setDeepLinkListener(router)
Deeplinkly.initialize()
5. Forward every URL entry point used by the app:
- AppDelegate open URL for custom schemes
- AppDelegate continue userActivity for Universal Links
- SceneDelegate willConnect connectionOptions for cold scene launches
- SceneDelegate openURLContexts and continue userActivity for warm links
- SwiftUI onOpenURL when the app uses the SwiftUI lifecycle
Always call Deeplinkly.handleLink(url). Do not wire inactive lifecycle paths merely because every example exists; follow the host app.
6. Configure deferred linking deliberately. iOS has no Install Referrer API. Deeplinkly restores a pre-install destination through pasteboard on a real device.
- Keep the automatic first-install check only when the product accepts the system paste banner.
- To control consent or explanation, set DeeplinklyCheckPasteboardOnInstall to false and use willShowPasteboardBanner plus checkPasteboardNow.
- On iOS 16+, prefer a user-initiated UIPasteControl when the UX requires explicit paste.
7. Bind identity after login and clear it on logout:
Deeplinkly.setUserId("user_123")
Deeplinkly.setUserId(nil)
Read persisted first-touch attribution with getInstallAttribution() and the stable local install identifier with getDeeplinklyId().
8. Bind the conversion-matching fields where the app already knows them — at
sign-up, at checkout — with setUserData(). Every field is optional and calls
merge; a malformed field rejects the whole call and stores nothing. Pass the
app's own product-analytics ids through customData (at most 10 entries,
64-char keys, 256-char values). Call clearUserData() on sign-out or consent
withdrawal; it actively erases rather than merely stopping, and is re-sent
until delivered. Only suggest fields the app's own privacy policy and consent
flow cover.
9. Report revenue with logPurchase(value:currency:orderId:quantity:productId:)
rather than a hand-rolled logEvent. It is a typed wrapper over the same
pipeline and fixes the spelling both Meta's Conversions API and Google's
enhanced conversions are built from. Pass orderId wherever the app has one:
it is Google's deduplication key. A refund is a separate event, not a
negative value — negatives are rejected.
10. Wrap logEvent() in the app analytics layer. Enforce: non-empty name up to 64 characters, at most 25 custom parameters, keys up to 64, string values up to 256, supported JSON types only, and no app-supplied _dl_ keys.
11. Add a generateLink(payload:) helper that passes the API's content/options map, checks success, and handles error_code/error_message without force-unwrapping.
12. Apply consent before initialize() when it must govern first-launch work. setTrackingEnabled(false) overrides the attribution level while link resolution continues. Use DeeplinklyAttributionLevel in Info.plist for a restricted native default.
## iOS-specific contracts
- Universal Links require all three: Associated Domains entitlement, correct code signing, and a valid AASA response. Info.plist alone cannot enable them.
- A cold UIScene launch arrives in connectionOptions, not the warm continue callback.
- A custom-scheme URL without a Deeplinkly click id remains app-owned. Allowlisted HTTPS hosts may resolve their first path segment as a Deeplinkly code.
- Deferred testing requires a real device and a genuinely fresh install. The Simulator has no App Store path.
- Offline pending resolves persist and retry on a later launch.
## Privacy and ATT guardrails
- Never print API keys, full attribution payloads, pasteboard contents, or user identifiers.
- setPIIHashingEnabled(true) SHA-256s email, phone and names on the device so
plaintext never reaches Deeplinkly. Off by default, and it should stay off
unless the user names a compliance requirement that plaintext must not reach a
processor — it costs match quality, because the digest is computed once under
one normalisation while advertising destinations disagree about phone
formatting. Do not enable it as a default "more private" choice; surface the
trade and let the user decide. Gender, country and date of birth are not
hashed by design.
- Do not request ATT authorization on Deeplinkly's behalf.
- Do not add NSUserTrackingUsageDescription or enable IDFA unless the host app explicitly opts in.
- If IDFA is enabled, merge the SDK's IDFA privacy-manifest template and ensure the app declares tracking correctly.
- Do not implement device-fingerprint matching or collect browser signals at click time.
- Preserve functional link delivery at every attribution level, including none.
## Validation
Run project tests/builds, then verify on an appropriately signed real device:
1. AASA returns the correct JSON without redirects or HTML.
2. Installed Universal Link cold start.
3. Warm Universal Link.
4. Custom-scheme fallback when configured.
5. SceneDelegate or SwiftUI lifecycle path used by the actual app.
6. Deferred install and pasteboard UX on a fresh install.
7. Offline resolve followed by retry.
8. Unknown params and unavailable destinations fall back safely.
9. Login/logout identity binding.
10. Accepted and rejected events.
11. Link generation success and failure.
12. Tracking-disabled, reduced-level, and ATT/IDFA boundaries.
Use Deeplinkly.setDebugMode(true) only in development and confirm Deeplinkly.isEnabled before debugging routing.
## Troubleshooting
- Universal Link opens Safari: inspect entitlement signing, AASA content/headers, Team ID, bundle ID, and device association cache.
- Warm links work but cold links do not: handle launchOptions or SceneDelegate connectionOptions for the active lifecycle.
- No link reaches the listener: attach it before initialize and forward the correct app/scene callbacks.
- Paste banner is unexpected: disable the automatic check and use a primed or user-initiated flow.
- Deferred attribution is empty: verify DeeplinklyLinkDomains and test a fresh real-device install.
- Duplicate navigation: dedupe centrally by click id or normalized payload key.
## Done
Do not declare completion until the user has received the evidence-backed dashboard checklist, Universal Links work on the signed target, cold and warm lifecycle paths are covered, deferred behavior is tested or explicitly documented, identity/events/link generation are wired, privacy and ATT choices remain app-owned, and project tests/builds pass.
Runbook
Full react-native-deeplinkly integration for Android and iOS: package and CocoaPods setup, native API-key configuration, App Links and Universal Links, delegate forwarding, JavaScript routing, deferred attribution, identity, events, and link generation.
---
name: deeplinkly-react-native
description: Integrate, debug, or productionize react-native-deeplinkly for Android and iOS. Use when installing or fixing the native module, React Navigation routing, Kotlin compatibility, App Links, Universal Links, AppDelegate or SceneDelegate forwarding, deferred attribution, identity, events, link generation, privacy, testing, or new and legacy architecture builds.
---
# Deeplinkly React Native SDK integrator
Complete both native hosts and the JavaScript routing layer. A JavaScript-only package install is not a Deeplinkly integration.
## Inspect before editing
Read:
- package.json and the npm/yarn/pnpm lockfile
- React Native version and new/legacy architecture settings
- app-root composition and navigator readiness
- android/build.gradle, app/build.gradle, AndroidManifest.xml, Application, and MainActivity
- ios/Podfile, Info.plist, entitlements, AppDelegate, SceneDelegate, and Swift/Objective-C language
- existing consent, analytics, identity, and build-variant configuration
Ask only for values that cannot be inferred: API key, production link domains, custom scheme, package/bundle identifiers, and signing identities. The API key comes from Deeplinkly Dashboard → App Settings.
## Give the user a dashboard handoff
After inspecting both native hosts, tell the user to open
https://www.deeplinkly.com/dashboard/app-settings and provide one consolidated
`Dashboard setup required` checklist. Suggest only values proven by native
files, resolved build settings, exact signing output, or release metadata, and
cite that evidence beside every value. Never suggest placeholders, unresolved
variables, identifiers derived from the app/display name, or guesses.
Eligible Android Setup values:
- **Package name**: the resolved applicationId for the target variant, including
flavor suffixes; never substitute the Java/Kotlin namespace.
- **SHA-256 Fingerprints**: only the intended certificate's verified output or
a Play Console app-signing fingerprint. Label debug, upload, and Play signing
certificates accurately.
- **App source / download URL**: only an exact Play listing or custom URL found
in release metadata.
- **Custom URI Scheme**: only a literal Android VIEW intent-filter scheme,
formatted as `scheme://` for the dashboard.
Eligible iOS Setup values:
- **Bundle Identifiers**: concrete resolved PRODUCT_BUNDLE_IDENTIFIER values for
the target configurations; never output `$(...)` variables.
- **Team ID**: only a concrete 10-character DEVELOPMENT_TEAM from the signed
target or resolved build settings.
- **App source / App Store app / custom URL**: only an exact App Store ID/URL or
custom destination found in release metadata.
- **URL Scheme**: only a literal CFBundleURLSchemes value, formatted as
`scheme://`.
- **Universal Links**: recommend enabling only when the signed target contains
a matching `applinks:` Associated Domains entry. Do not guess the separate
Native Links toggle.
For shared settings, suggest a **Default URL** only when an explicit production
web fallback exists. If one literal Deeplinkly host is configured consistently,
tell the user to ensure it exists under Dashboard → Domains; show conflicts and
ask which host is authoritative. Never infer or generate the API key. Under
`Still needed from you`, tell the user to copy an active Production API key
and place it under both exact native keys. Format proven rows as
`- Field: exact value — evidence`, omit unproven rows, and ask for all missing
dashboard choices in one block. Keep dashboard-dependent verification pending
until the user confirms the settings.
## Integration workflow
1. Use the repository's package manager:
npm install react-native-deeplinkly
cd ios && pod install
Rebuild both native apps. A Metro reload cannot link a new native module.
2. Check Android Kotlin compatibility. The native dependency requires Kotlin 2.2.0. Set kotlinVersion and explicitly version the kotlin-gradle-plugin classpath; a bare classpath can ignore ext.kotlinVersion.
3. Configure Android with exact native keys:
com.deeplinkly.sdk.api_key
com.deeplinkly.sdk.link_domains
Add a verified HTTPS App Link filter and retain a custom scheme only for fallback. Follow the app's existing launch mode; when using singleTask or singleTop, preserve React Native and Deeplinkly Intent delivery.
4. Verify assetlinks.json using the production package name and Play signing SHA-256 fingerprint. Test adb shell pm get-app-links and fix verification before debugging JavaScript.
5. Configure iOS with exact native keys:
DeeplinklyApiKey
DeeplinklyLinkDomains
Add Associated Domains entries for every production link host and validate AASA against the Team ID and bundle ID. Add CFBundleURLTypes only when a custom scheme is required.
6. Forward iOS lifecycle callbacks through RNDeeplinklyLinking:
- handleLaunchOptions for an AppDelegate cold launch
- handleUserActivity for Universal Links
- handleURL for custom schemes
- handleSceneConnectionOptions for a cold UIScene launch
- handleOpenURLContexts for scene custom-scheme delivery
Preserve RCTLinkingManager calls so existing React Native Linking behavior continues. For Objective-C AppDelegate files, import the generated react_native_deeplinkly Swift header.
7. Subscribe once at the app root, not inside a screen:
const sub = Deeplinkly.addListener(({ click_id, params }) => {
routeSafely(params);
});
return () => sub.remove();
Subscription signals JavaScript readiness and flushes the native persistent queue. Make the centralized route resolver idempotent and navigator-ready.
8. Do not add a JavaScript initialize() call. Native construction reads AndroidManifest.xml and Info.plist. In debug builds, assert await Deeplinkly.isAvailable() and enable native debug logging only when needed.
9. Handle iOS deferred linking deliberately. The automatic pasteboard read can restore a pre-install destination on a fresh real-device install. If the app controls the explanation, disable DeeplinklyCheckPasteboardOnInstall and call willShowPasteboardBanner/checkPasteboardNow. On iOS 16+, DeeplinklyPasteButton provides a user-initiated option.
10. Bind identity after login and clear on logout:
Deeplinkly.setUserId("user_123")
Deeplinkly.setUserId(null)
Read getInstallAttribution() after startup settles and getDeeplinklyId() for the stable per-install identifier.
11. Bind the conversion-matching fields where the app already knows them — at
sign-up, at checkout — with setUserData(data). Every field is optional and
calls merge; a malformed field rejects the whole call and stores nothing.
Pass the app's own product-analytics ids through customData (at most 10
entries, 64-char keys, 256-char values). Call clearUserData() on sign-out or
consent withdrawal; it actively erases rather than merely stopping, and is
re-sent until delivered. Only suggest fields the app's own privacy policy
and consent flow cover.
12. Report revenue with logPurchase({ value, currency, orderId, quantity,
productId }) rather than a hand-rolled logEvent. It is a typed wrapper over
the same pipeline and fixes the spelling both Meta's Conversions API and
Google's enhanced conversions are built from. Pass orderId wherever the app
has one: it is Google's deduplication key. A refund is a separate event, not
a negative value — negatives are rejected.
13. Wrap logEvent() in the existing analytics layer. Enforce a trimmed name up to 64 characters, at most 25 custom parameters, keys up to 64, string/encoded values up to 256, supported JSON types only, no nulls, and no app-supplied _dl_ keys.
14. Wrap generateLink() and read its JavaScript result as:
{ success, url?, errorCode?, errorMessage? }
errorCode and errorMessage are camelCase in JavaScript even though the native wire maps are snake_case. The deep-link envelope retains click_id and params. tags must be a list, not an object.
15. Integrate consent. setTrackingEnabled(false) disables reporting, events, error reports, pasteboard reads, and pending reporting retries while functional links still resolve. resetPrivacyData() deletes local identity/attribution state and leaves tracking disabled. Set native attribution defaults before JavaScript when first-launch consent requires it.
## Platform contracts
- Android deferred attribution uses Play Install Referrer and must be tested through Play internal testing.
- iOS deferred attribution uses pasteboard and must be tested on a fresh real-device install.
- A React Native module cannot receive iOS app-delegate callbacks by itself; forwarding is mandatory.
- Cold-start links are queued natively until addListener registers. Do not replace this with an ad hoc getInitialURL race.
- Both TurboModule/new architecture and legacy NativeModules builds are supported; preserve the project's chosen architecture.
- Missing native keys degrade with documented failure values instead of throwing. Treat isAvailable() false as a configuration bug.
## Privacy guardrails
- Never print API keys, full attribution payloads, pasteboard contents, or user identifiers.
- Do not add ATT/IDFA or Android Advertising ID collection without explicit host-app consent and disclosure.
- Do not implement probabilistic device fingerprint matching or browser signal collection.
- Keep deep-link routing functional when tracking is disabled or attribution level is none.
- setPIIHashingEnabled(true) SHA-256s email, phone and names on the device so
the plaintext never leaves it. Off by default, and it should stay off unless
the user names a compliance requirement that plaintext must not leave the
device — it costs match quality, because the digest is computed once under one
normalisation while advertising destinations disagree about phone formatting.
Do not enable it as a default "more private" choice; surface the trade and let
the user decide. Gender, country and date of birth are not hashed by design.
## Validation matrix
Build and test both platforms:
1. Native module resolves after a clean rebuild.
2. Android App Links verify; iOS AASA and entitlements match.
3. Cold and warm installed link on Android.
4. Cold and warm Universal Link on iOS.
5. JavaScript bundle starts slowly and still receives the queued cold link.
6. Navigator is not ready immediately and routing waits without duplication.
7. Android deferred install through Play internal testing.
8. iOS deferred install on a fresh real device.
9. Offline resolve and later retry.
10. Identity set/clear, accepted/rejected events, and generated-link errors.
11. Tracking-disabled and resetPrivacyData behavior.
12. Debug and release builds for the enabled React Native architecture.
## Troubleshooting
- Native module is not linked: run pod install, clean, and rebuild; do not rely on a JS reload.
- Android Kotlin metadata failure: explicitly apply Kotlin 2.2.0 to the buildscript classpath.
- iOS listener receives nothing: forward the active AppDelegate or SceneDelegate callbacks.
- Cold links disappear: keep addListener at app root and do not unsubscribe during delivery.
- Deferred attribution is empty: verify native domain allowlists and use the platform's real install path.
- generateLink error is undefined: read errorCode/errorMessage, not error_code/error_message.
- logEvent resolves false: enforce native validation limits and reject null/reserved keys before calling.
## Done
Do not declare completion until the user has received the evidence-backed dashboard checklist, both native platforms are configured, JavaScript routing survives cold-start timing, installed and deferred flows are tested or explicitly documented, identity/events/link generation/privacy are wired, and Android plus iOS project checks pass.