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.

What a skill is

One Markdown file that turns a coding agent from a docs reader into an implementer.

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.

Install with one command

Choose your coding agent, SDKs, and install scope when prompted; the installer downloads, validates, and places every selected skill for you.

Interactive installer
curl -fsSL https://www.deeplinkly.com/install-skill | bash

Run 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:

Non-interactive Codex install
curl -fsSL https://www.deeplinkly.com/install-skill | bash -s -- --agent codex --sdk all --scope project

Valid 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.

AgentPathNotes
Claude Code.claude/skills/deeplinkly-<sdk>/SKILL.mdProject 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.mdProject 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.mdProject scope. For every project, use ~/.cursor/skills/<slug>/SKILL.md. Cursor discovers it from the skill metadata.
Any other agentPaste 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.

Catalog

Production-ready runbooks for Flutter, Android, iOS, and React Native.

Flutter SDK

deeplinkly-flutter

Available

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

Available

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

Available

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

Available

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

Flutter SDK skill

Full integration of flutter_deeplinkly 1.9.x: dependency, native platform setup, deep link routing, deferred attribution, identity, events, and link generation.

/skills/deeplinkly-flutter
---
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

Android SDK skill

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.

/skills/deeplinkly-android
---
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

iOS SDK skill

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.

/skills/deeplinkly-ios
---
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

React Native SDK skill

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.

/skills/deeplinkly-react-native
---
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.