# Deep link doesn't open the app

> Debug links that open the browser instead of your app: Universal Links on iOS, App Links on Android, and native config for Flutter and React Native.

Source: https://docs.grovs.io/docs/troubleshooting/deep-link-not-opening-app

Debug a Grovs link that opens the browser or the store instead of your installed app. The cause is almost always platform link verification, not the SDK.

## Quick triage

Check these first. Each row names a cause and how to confirm it.

| Cause                                                | How to confirm                                                                                                                                                                                             |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The app isn't installed                              | That's the [deferred deep linking](https://docs.grovs.io/docs/troubleshooting/deferred-deep-linking) flow, not this one.                                                                                   |
| Only one environment is configured                   | The dashboard gives you two link domains and two URL schemes, one per environment. Your `applinks:` entries and intent filters must list both.                                                             |
| The build and the link are in different environments | A `useTestEnvironment = true` build handles test links only. See [environment mismatch](https://docs.grovs.io/docs/troubleshooting/sdk-configuration-issues#test-vs-production-environment-mismatch).      |
| The domain serves no association file                | Open `https://<link-domain>/.well-known/apple-app-site-association` or `/.well-known/assetlinks.json`. A 404 means the platform isn't set up in the dashboard ([details](#1-check-the-file-grovs-serves)). |
| Wrong Team ID, bundle ID, package name or SHA-256    | The file lists a different app ID or fingerprint than your build.                                                                                                                                          |
| You're testing the wrong way (iOS)                   | You typed the URL into Safari, or used the Simulator. Tap the link in Notes instead.                                                                                                                       |
| The app opens but your handler never runs            | OS config is fine; the SDK isn't getting the link. Check [iOS delegate forwarding](#7-confirm-the-sdk-received-the-link) or [Android lifecycle forwarding](#app-opens-but-the-listener-never-fires).       |

If links work on one platform but not the other, the SDK integration is fine. The broken platform's native config is the cause.

## iOS: Universal Links

### How iOS verifies a link

1. Your app's **Associated Domains** entitlement lists `applinks:<link-domain>`.
2. When the app is installed or updated, the device asks **Apple's CDN** for each domain's `apple-app-site-association` (AASA) file. The device doesn't fetch it from the domain.
3. iOS checks that the CDN's copy lists your app as `TEAMID.bundle.identifier`.
4. If it does, tapping a link on that domain opens your app and the SDK receives the URL.

A fix on the domain reaches a device only after the CDN refreshes and the app is reinstalled. [Developer mode](#5-bypass-apples-cdn-with-developer-mode) skips the CDN while you debug.

### 1. Check the file Grovs serves

```bash
curl -s https://medly.sqd.link/.well-known/apple-app-site-association
```

Run it for each link domain in the dashboard, test and production. A correct response:

```json
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "ABCDE12345.com.medly.app",
        "paths": ["NOT /__/auth/action/", "NOT /__/auth/handler/", "NOT /_/*", "/*"]
      }
    ]
  }
}
```

`appID` is your [Apple App Prefix (Team ID)](https://docs.grovs.io/docs/how-to-guides/ios/apple-app-prefix), a dot, then your [bundle identifier](https://docs.grovs.io/docs/how-to-guides/ios/bundle-identifier). Both must match the build exactly, including case.

A 404 returns a JSON `error`:

| `error`                                            | Fix                                                                                                                                      |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `Domain not found`                                 | The hostname isn't a Grovs link domain. Check for typos, or your [custom domain](https://docs.grovs.io/docs/links/custom-domains) setup. |
| `Application not found` or `Configuration not set` | The iOS app isn't registered. Add the bundle ID and Team ID in the dashboard.                                                            |
| `Configuration not enabled`                        | iOS isn't set to **Redirect to App** in [redirect rules](https://docs.grovs.io/docs/getting-started/project-setup#redirect-rules).       |

### 2. Check Apple's CDN copy

```bash
curl -s https://app-site-association.cdn-apple.com/a/v1/medly.sqd.link
```

This is the copy devices receive. If it differs from step 1, the CDN hasn't refreshed yet. Use [developer mode](#5-bypass-apples-cdn-with-developer-mode) until it does.

### 3. Check the entitlement in the build

In Xcode, **Signing & Capabilities → Associated Domains** must list one entry per link domain:

```text
applinks:medly.sqd.link
applinks:medly.test-sqd.link
```

Use the exact hostnames from your dashboard, plus any custom domain. See [How to Add an Associated Domain](https://docs.grovs.io/docs/how-to-guides/ios/associated-domain).

The entitlement must also be in the provisioning profile. Confirm the built app carries it:

```bash
codesign -d --entitlements :- /path/to/Medly.app | grep -A3 associated-domains
```

The **Team** that signs the build must be the one whose ID is in the AASA `appID`.

> **Warning:** iOS reads entitlements at install. After changing Associated Domains, the Team ID or the bundle ID, delete the app and reinstall it.

### 4. Test the right way

- **Tap the link from another app.** Paste it into Notes or Messages and tap it.
- **Long-press the link.** If the association works, the menu shows **Open in "Medly"**. If it only offers Safari, iOS has no verified association.
- **Don't type or paste into Safari's address bar.** That never opens an app, by design.
- **Don't tap a link to the domain you're on.** In Safari, a link to the current page's domain stays in Safari.
- **Undo an opt-out.** If anyone tapped **Open in Safari** on the Universal Link banner, iOS remembers it for that domain on that device. Long-press the link and choose **Open in "Medly"** to restore it.
- **Use a physical device.** Universal Links don't work in the Simulator. Test the [URL scheme](https://docs.grovs.io/docs/how-to-guides/ios/url-scheme) path there instead: typing `yoururlscheme://` into Safari should open the app.

### 5. Bypass Apple's CDN with developer mode

Developer mode makes the device fetch the AASA from the link domain directly, so a fix shows on the next install.

### 1. Add the developer mode suffix

In a development build, append `?mode=developer` to each entry:

```text
applinks:medly.sqd.link?mode=developer
applinks:medly.test-sqd.link?mode=developer
```

It only applies to apps signed with a development profile. Remove it before release.

### 2. Turn on Developer Mode

On iOS 16 and later, open **Settings › Privacy & Security › Developer Mode** and turn it on. The device restarts.

### 3. Turn on Associated Domains Development

Open **Settings › Developer** and turn on **Associated Domains Development**. **Settings › Developer** appears once the device has been used with Xcode.

### 4. Reinstall and test

Delete the app, install it again from Xcode, and tap a link in Notes.

### 6. Read what iOS decided

**Diagnostics on the device.** Open **Settings › Developer › Universal Links › Diagnostics**, paste a Grovs link and tap **Diagnose**.

| Result                                               | Meaning                                                                                            |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Opens your app, with your `TEAMID.bundle.identifier` | The association is verified. Check how you test (step 4) or the SDK (step 7).                      |
| Opens in Safari, or no matching apps                 | No verified association. Recheck steps 1 to 3, then reinstall.                                     |
| An error fetching or parsing the file                | The file isn't reachable over HTTPS with a valid certificate. Custom domains fail here most often. |

**Console.app.** Connect the device to a Mac, open **Console.app**, select the device and filter on the process `swcd`. Reinstall the app to watch each domain's fetch and any error.

**swcutil on macOS.** `swcutil` talks to the Mac's own `swcd` and needs `sudo`.

```bash
# Download a domain's AASA the way swcd does, and show errors
sudo swcutil dl -d medly.sqd.link

# Stream swcd logging live
sudo swcutil watch --verbose

# Show swcd's stored state (Mac Catalyst or Designed for iPad builds)
sudo swcutil show
```

**sysdiagnose.** For a device, `swcutil_show.txt` inside a sysdiagnose holds the same state. Press both volume buttons and the side button together for about a second. After a few minutes, share it from **Settings › Privacy & Security › Analytics & Improvements › Analytics Data** (named `sysdiagnose_...`). Find your domain:

```text
Service:    applinks
App ID:     ABCDE12345.com.medly.app
Domain:     medly.sqd.link
User Approval: true
Site/Fmwk Approval: true
```

| Field                       | Meaning                                                                        |
| --------------------------- | ------------------------------------------------------------------------------ |
| `Site/Fmwk Approval: true`  | The file was fetched and lists your app.                                       |
| `Site/Fmwk Approval: false` | The fetch failed or the App ID isn't listed. An `Error:` line names the cause. |
| `User Approval: false`      | The user opted out on this device. Long-press the link to restore.             |
| No entry for a domain       | That `applinks:` entry isn't in the installed build.                           |

### 7. Confirm the SDK received the link

If the app opens but nothing happens, the link didn't reach the SDK. Turn on info logging before `configure`:

```swift
Grovs.setDebug(level: .info)
Grovs.configure(APIKey: "your-api-key", useTestEnvironment: true, delegate: self)
```

Lines start with `GROVS [INFO]` in the Xcode console, under the `io.grovs.sdk` subsystem. Release builds redact them to `<private>`, so use a debug build. At startup you should see `URL Scheme properly configured.` If you see the mismatch error instead, add the environment's scheme under **Info → URL Types**.

Log in the delegate to see each link arrive:

```swift
func grovsReceivedPayloadFromDeeplink(link: String?, payload: [String: Any]?, tracking: [String: Any]?) {
    print("Grovs link: \(link ?? "nil") payload: \(payload ?? [:])")
}

func grovsDidEncounterError(_ error: GrovsError, message: String) {
    print("Grovs error: \(error) \(message)")
}
```

Both run on the main thread. `Grovs.lastReceivedPayload { payload in ... }` returns the last payload if you need to check later.

If the delegate never fires:

- Forward `openURLContexts`, `continue userActivity` and `willConnectTo` (or the AppDelegate equivalents) exactly as in [Delegate setup](https://docs.grovs.io/docs/sdk/ios/quick-start#delegate-setup). Without `willConnectTo`, cold-start links are lost.
- Pass the delegate to `configure`, or set `Grovs.delegate` after it. A delegate set before `configure` is dropped.
- Keep a strong reference to the delegate. The SDK holds it weakly.

For an end-to-end pass, use the [iOS setup checklist](https://docs.grovs.io/docs/troubleshooting/ios-setup-checklist). See also [iOS Deep Linking](https://docs.grovs.io/docs/sdk/ios/deep-linking).

## Android: App Links

### Check the file Grovs serves

```bash
curl -s https://medly.sqd.link/.well-known/assetlinks.json
```

A correct response lists your package name and every SHA-256 from the dashboard:

```json
[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.medly.app",
      "sha256_cert_fingerprints": ["F5:09:10:F7:..."]
    }
  }
]
```

A 404 returns the same `error` values as the iOS file. `Configuration not enabled` means Android isn't set to **Redirect to App**.

### Check the package name and fingerprints

1. The **package name** in the dashboard must match your `applicationId` exactly. See [How to Get the Package Name](https://docs.grovs.io/docs/how-to-guides/android/package-name).
2. Add the **SHA-256** of every key that signs a build you test, debug and release. See [How to Get the SHA-256 Fingerprint](https://docs.grovs.io/docs/how-to-guides/android/sha256-fingerprint).
3. With **Play App Signing**, Google re-signs your app before distribution, so store installs carry Google's key, not your upload key. In the [Play Console](https://play.google.com/console), open **Setup → App integrity** and add the SHA-256 under **App signing key certificate**.

If links work in debug builds but not from the Play Store, step 3 is the cause.

### Check the intent filter

Your `https` intent filter needs `android:autoVerify="true"`, the `VIEW` action, the `DEFAULT` and `BROWSABLE` categories, and both link hosts from the dashboard. Copy it from the [Android Quick Start](https://docs.grovs.io/docs/sdk/android/quick-start#3-add-intent-filters). For the URL scheme filter, see [How to Add a New Intent Filter](https://docs.grovs.io/docs/how-to-guides/android/intent-filter).

Verification runs at install. Reinstall the app after changing the filter or the dashboard values.

### Check verification with adb

```bash
# Verification state of each domain (Android 12+)
adb shell pm get-app-links com.medly.app

# Re-run verification after a fix
adb shell pm verify-app-links --re-verify com.medly.app
```

Each link domain should show `verified`. On Android 12 and later, a domain that fails verification opens in the browser, not an app chooser.

### Test with adb

```bash
# App Link
adb shell am start -W -a android.intent.action.VIEW -d "https://medly.sqd.link/summer-sale"

# URL scheme fallback
adb shell am start -W -a android.intent.action.VIEW -d "myapp://example.com/path"
```

If the scheme test fails too, the intent filter is missing or incomplete.

In Chrome, a link to the domain of the page you're on stays in the browser. Test from another app, such as a messaging app.

### App opens but the listener never fires

The link reached your activity, but not the SDK.

- Call `Grovs.onStart(this)` in your launcher activity's `onStart()` and `Grovs.onNewIntent(intent, this)` in `onNewIntent()`. See [Forward lifecycle events](https://docs.grovs.io/docs/sdk/android/quick-start#2-forward-lifecycle-events).
- Register a listener with `Grovs.setOnDeeplinkReceivedListener`. See [Deep Linking](https://docs.grovs.io/docs/sdk/android/deep-linking).

## Flutter, React Native and Unity

The native layer opens the app. Dart, JS and C# only receive the payload afterwards, so the iOS and Android sections apply unchanged.

- **Flutter:** check the `Info.plist` keys, Associated Domains, URL schemes and Android intent filters in the [Flutter Quick Start](https://docs.grovs.io/docs/sdk/flutter/quick-start). From plugin 3.1, `FlutterDeepLinkingEnabled` and `flutter_deeplinking_enabled` must be `false`, otherwise links can open Safari or push a route.
- **React Native:** check the native setup in the [React Native Quick Start](https://docs.grovs.io/docs/sdk/react-native/quick-start).
- **Unity:** check the URI scheme, link domains and signing fingerprints in **Project Settings › Grovs**. See the [Unity Quick Start](https://docs.grovs.io/docs/sdk/unity/quick-start).
- **Expo:** the config plugin writes the native config. Check that `scheme` and `associatedDomains` in `app.json` match the dashboard, then re-run `npx expo prebuild`. It needs a development build, not Expo Go.

## Still stuck

Email <support@grovs.io> with:

- Platform, OS version and SDK version
- The link you tapped, and where you tapped it
- The environment (test or production) and the build's `useTestEnvironment` value
- The `curl` output for the domain's association file
- iOS: the Diagnostics result and your `applinks:` entries
- Android: the output of `adb shell pm get-app-links <package>`
- SDK logs at info level, from app launch to the link tap
