# Deep linking

> Handle incoming deep links, links tapped inside your app and SDK errors in your Flutter app with the Grovs SDK.

Source: https://docs.grovs.io/docs/sdk/flutter/deep-linking

Subscribe to `onDeeplinkReceived` for link payloads and `onError` for SDK failures. Pass links tapped inside your own app to `handleUrl`.

## Receiving deep link events

`onDeeplinkReceived` emits a `DeeplinkDetails` with the link, its custom payload and its tracking metadata. Subscribe in `initState` of a widget that stays mounted, such as the root widget:

```dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:grovs_flutter_plugin/grovs.dart';
import 'package:grovs_flutter_plugin/models/grovs_link.dart';

final navigatorKey = GlobalKey<NavigatorState>();

class App extends StatefulWidget {
  const App({super.key});

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  final _grovs = Grovs();
  StreamSubscription<DeeplinkDetails>? _subscription;

  @override
  void initState() {
    super.initState();
    _subscription = _grovs.onDeeplinkReceived.listen((details) {
      final link = details.link;
      final payload = details.data;
      final tracking = details.tracking;

      debugPrint('Opened from $link with $payload, campaign ${tracking?.utmCampaign}');

      // Route the user based on the payload
      if (payload?['screen'] == 'product') {
        navigatorKey.currentState?.pushNamed(
          '/product',
          arguments: payload?['productId'],
        );
      }
    });
  }

  @override
  void dispose() {
    _subscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      routes: {
        '/': (_) => const HomeScreen(),
        '/product': (_) => const ProductScreen(),
      },
    );
  }
}
```

It fires for links that open the app, links delivered while it runs, deferred links matched after a fresh install, links passed to [`handleUrl`](#links-tapped-inside-your-app) and taps on Grovs [push campaigns](https://docs.grovs.io/docs/sdk/flutter/push-notifications#what-your-app-receives). `tracking` is a `TrackingParams` with `utmCampaign`, `utmSource` and `utmMedium`, each `null` when unset.

### Delivery details

- **Early links are buffered.** A link resolved before Dart subscribes (the launch link, say) is held and delivered when the first listener attaches. Up to 20 events per platform; older ones are dropped.
- **Broadcast stream.** Several widgets can listen; cancelling one doesn't affect others. Buffered events replay only to the first subscription.
- **Nothing while disabled.** Links arriving under `setSDK(false)` are dropped and the buffer cleared. A link that launched the app while disabled is resolved and delivered after `setSDK(true)`. See [Privacy & Consent](https://docs.grovs.io/docs/sdk/flutter/privacy#enabling-again).
- **`link` can be `null`.** A tapped push without a link has no `link`, and iOS may report a payload without a link string. Treat every `DeeplinkDetails` field as optional on both platforms.

## Links tapped inside your app

iOS and Android don't hand a universal link or App Link back to the app that opened it, so a Grovs link shown in your own UI (a chat message, say) opens in a browser. Pass it to `handleUrl` instead:

```dart
final handled = await Grovs().handleUrl('https://your_app_host/abc');
```

The result arrives on `onDeeplinkReceived`, like any other deep link.

- The URL must be absolute, with a scheme and a host. Otherwise `handleUrl` throws a `GrovsException` with code `INVALID_ARGUMENT`.
- Returns `true` when the link was handed to the native SDK.
- Returns `false` and drops the link while the SDK is disabled, and on Android when no activity is attached.

## Handling SDK errors

`onError` emits a `GrovsError` when the native SDK reports an asynchronous failure. Errors are informational and the SDK recovers on its own, so use it for logging, not control flow.

```dart
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:grovs_flutter_plugin/grovs.dart';
import 'package:grovs_flutter_plugin/models/grovs_link.dart';

StreamSubscription<GrovsError>? _errorSubscription;

_errorSubscription = Grovs().onError.listen((error) {
  debugPrint('Grovs ${error.code.nativeName}: ${error.message}');
});
```

| `GrovsErrorCode`       | Native name              | Meaning                                                                  |
| ---------------------- | ------------------------ | ------------------------------------------------------------------------ |
| `authenticationFailed` | `authentication_failed`  | Authentication with the backend failed. Check the API key and bundle ID. |
| `networkRequestFailed` | `network_request_failed` | A request failed after retries. The SDK backs off and retries later.     |
| `eventDispatchFailed`  | `event_dispatch_failed`  | An event batch failed to send. The events stay queued.                   |
| `linkGenerationFailed` | `link_generation_failed` | Link generation failed.                                                  |

> **Warning:** Only the iOS bridge feeds `onError`; on Android the stream stays silent. iOS buffers the 20 most recent errors raised before you subscribe and replays them to the first listener. Errors with an unknown code are dropped. An in-app message that couldn't be shown arrives on [`onInAppMessage`](https://docs.grovs.io/docs/sdk/flutter/in-app-messages#react-to-messages) as `InAppMessageFailed`, not here.

Failures of your own calls, such as `generateLink`, throw `GrovsException` from the `Future` instead. See [Error codes](https://docs.grovs.io/docs/sdk/flutter/api-reference#error-codes).

## Clipboard-assisted deferred deep linking

When a copy-to-clipboard link opens in a browser, the preview page copies it to the clipboard before sending the visitor to the store. On first launch after install, the native SDK reads the clipboard once to attribute the install deterministically. The link arrives on `onDeeplinkReceived`.

No Dart is needed; the native SDKs handle it, including when the clipboard is read and the iOS paste notice. Rules: [iOS Deep Linking](https://docs.grovs.io/docs/sdk/ios/deep-linking#clipboard-assisted-deferred-deep-linking) and [Android Deep Linking](https://docs.grovs.io/docs/sdk/android/deep-linking#clipboard-assisted-deferred-deep-linking). Starting disabled with `GrovsEnabled` / `grovs_enabled` prevents the read; there's no separate opt-out.

If your links use a custom domain, list it. iOS takes an array of strings, Android one comma-separated string:

```xml
<!-- Info.plist -->
<key>GrovsClipboardDomains</key>
<array>
    <string>links.yourdomain.com</string>
</array>
```

```xml
<!-- AndroidManifest.xml, inside <application> -->
<meta-data
    android:name="grovs_clipboard_domains"
    android:value="links.yourdomain.com" />
```

Enable copy-to-clipboard per link with [`copyToClipboardIos` and `copyToClipboardAndroid`](https://docs.grovs.io/docs/sdk/flutter/link-creation#parameters) on `GenerateLinkParams`, or set the project default in the dashboard. Flow, settings and limits: [Copy to Clipboard](https://docs.grovs.io/docs/links/copy-to-clipboard).

## What the plugin does not expose

The iOS past-payload queries (`lastReceivedPayload`, `allReceivedPayloadsSinceStartup`) and the Android `openedLinkDetails` flow have no Dart counterpart. To look up a link by path, use [`linkDetails`](https://docs.grovs.io/docs/sdk/flutter/link-creation#link-details).

## Next steps

- [Generate smart links →](https://docs.grovs.io/docs/sdk/flutter/link-creation)
- [Track events and screens →](https://docs.grovs.io/docs/sdk/flutter/analytics)
- [Full API reference →](https://docs.grovs.io/docs/sdk/flutter/api-reference)
