# Usage

Detour delivers links to your app through a single object. `DetourService` starts the native bridge, collects every resolved link, whether deferred, Universal/App, or custom scheme, into one pending intent, and notifies you when a link arrives. You read the intent, navigate, and mark it consumed.

## Service setup

`DetourService` is the recommended orchestration layer:

- It configures the native bridge once.
- It merges initial and runtime links into a single pending intent.
- `isInitialLinkProcessed` tells you when the startup flow has finished.
- `consumePendingIntent()` clears the intent explicitly. An intent stays pending until you clear it.
- The same link arriving twice within 2 seconds is emitted once.

```dart
import 'package:detour_flutter_plugin/detour_flutter_plugin.dart';
import 'package:flutter/material.dart';

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final detour = DetourService();

  @override
  void initState() {
    super.initState();
    detour.addListener(_onDetourChanged);
    _startDetour();
  }

  Future<void> _startDetour() async {
    await detour.start(
      const DetourConfig(
        apiKey: '<REPLACE_WITH_YOUR_API_KEY>',
        appID: '<REPLACE_WITH_APP_ID_FROM_PLATFORM>',
        shouldUseClipboard: true,
        linkProcessingMode: LinkProcessingMode.all,
      ),
    );
  }

  void _onDetourChanged() {
    final intent = detour.pendingIntent;
    if (intent == null) return;

    // Route once, then mark as consumed.
    // context.go(intent.link.route);
    detour.consumePendingIntent();
  }
}
```

:::note[Start is idempotent]
`start()` is safe to call more than once. Until `stop()` resets the service,
only the first call configures the native SDK and subscribes to the link stream.
:::

For a complete, runnable integration, see [Examples](https://detour.swmansion.com/docs/sdk/flutter/examples).

## Handle resolved links

`DetourService` extends `ChangeNotifier`. Every time a link resolves, listeners fire and `pendingIntent` holds the result until you consume it.

A `DetourIntent` carries three fields:

- `intent.link` — the resolved link (see below)
- `intent.source` — where it came from, one of `initial`, `runtime`, or `manual`
- `intent.receivedAt` — when the service registered it

Call `consumePendingIntent()` after you navigate. Until you do, `pendingIntent` keeps returning the same intent, and a rebuild would route again.

### `link` structure

<PropertyList>
  <Property name="link.route" type={"String"}>
    Full in-app route, query string included: `/details?utm=1`.
  </Property>
  <Property name="link.pathname" type={"String"}>
    Path without the query: `/details`.
  </Property>
  <Property name="link.params" type={"Map<String, String>"}>
    Query params, already parsed.
  </Property>
  <Property name="link.url" type={"String"}>
    The original URL.
  </Property>
  <Property name="link.type" type={"LinkType"} typeHref="/sdk/flutter/api-reference#linktype">
    Where the link came from: `deferred`, `verified`, or `scheme`.
  </Property>
</PropertyList>

## Link processing modes

`DetourConfig.linkProcessingMode` controls which link sources the plugin handles:

| Value | Deferred | Universal / App | Custom scheme |
| --- | --- | --- | --- |
| `LinkProcessingMode.all` (default) | Yes | Yes | Yes |
| `LinkProcessingMode.webOnly` | Yes | Yes | No |
| `LinkProcessingMode.deferredOnly` | Yes | No | No |

Use `deferredOnly` when your navigation framework already handles runtime and initial URLs.

## Manual processing

You can pass any URL to the plugin yourself:

```dart
final result = await detour.processLink(
  'https://example.godetour.link/abc123?source=push',
);

if (result.link != null) {
  // Navigate with result.link!.route
}
```

`processLink` returns a `DetourResult` with two fields:

- `processed` — whether the native SDK ran the URL through link resolution
- `link` — the resolved link, or `null`

The result is also pushed into the service as a pending intent. Pass `emitIntent: false` to inspect it without triggering your routing listener.

## Low-level API

If you need full manual control, use `DetourFlutterPlugin` instead of `DetourService`. You are then responsible for configuration, the initial resolve, and the runtime stream.

```dart
final plugin = DetourFlutterPlugin();

await plugin.configure(
  const DetourConfig(
    apiKey: '<REPLACE_WITH_YOUR_API_KEY>',
    appID: '<REPLACE_WITH_APP_ID_FROM_PLATFORM>',
  ),
);

final initial = await plugin.resolveInitialLink();
final stream = plugin.linkStream;
final processed = await plugin.processLink('https://example.godetour.link/path');
```

## Click limits

Before it delivers an App Link or Universal Link, the native SDK checks the click against your plan's limit. Within the limit, links resolve normally.

### Over the limit

The link is **not** delivered, and the plugin reports nothing. The native result comes back without a link, so `DetourService` registers no pending intent and your listener never fires. `processLink()` returns a `DetourResult` with `processed: true` and `link: null`, the same shape as a URL that had no match.

The only signal is in the native log, where the SDK writes `[Detour:CLICK_LIMIT_ERROR]` with `clicksInPeriod` and `effectiveLimit`:

```bash
adb logcat | grep -i detour                                            # Android
xcrun simctl spawn booted log stream \
  --predicate 'subsystem == "com.swmansion.detour"'                    # iOS simulator
```

### Other link types

This check applies only to App Links and Universal Links.

- **Deferred and short links** — the same plan limits apply on the backend.
  On iOS no click-limit error is raised, an over-limit link fails to resolve, and
  `link` stays `null`. On Android an over-limit deferred match makes
  `resolveInitialLink()` raise [`NATIVE_ERROR`](https://detour.swmansion.com/docs/sdk/flutter/testing#native-error)
  with a `[402]` message. A short link opened as an App Link returns no link and
  no error.
- **Custom scheme links** — resolved on device, never limit-checked.

:::note[Fail-open by design]
If the limit check cannot complete because of a network or backend error, the SDK delivers the link, so a temporary outage does not block navigation. Limits are enforced only on an explicit "denied" response.
:::

For per-plan limits, pay-as-you-go overage, and how to raise them, see [Billing & Payments](https://detour.swmansion.com/docs/platform/fundamentals/billing).

## Analytics

Starting the service turns analytics on. With `start()` called, the native SDK logs one event by itself: `app_open`, a retention event sent once per session on cold start.

Everything else you log yourself:

```dart
await detour.logEvent(
  DetourEventName.purchase,
  data: {'value': 9.99, 'currency': 'USD'},
);

await detour.logRetention('home_screen_viewed');
```

`logEvent` takes a `DetourEventName` value, so you can log only the predefined
event names. `logRetention` takes any string. The full enum is in the [API
Reference](https://detour.swmansion.com/docs/sdk/flutter/api-reference).

:::note[SDK version header]
Every request the SDK sends carries an `X-SDK: flutter/<plugin-version>` header, used for diagnostics and SDK-usage analysis. It needs no configuration.
:::

## Related pages

- [Examples](https://detour.swmansion.com/docs/sdk/flutter/examples)
- [API Reference](https://detour.swmansion.com/docs/sdk/flutter/api-reference)
- [Custom Storage](https://detour.swmansion.com/docs/sdk/flutter/custom-storage)
- [Testing & Troubleshooting](https://detour.swmansion.com/docs/sdk/flutter/testing)