# DeeplinkServiceProvider

- [Introduction](#introduction)
- [Two-Phase Bootstrap](#two-phase-bootstrap)
- [Register Phase](#register-phase)
- [Boot Phase](#boot-phase)
    - [Driver Initialization](#driver-initialization)
    - [Stream Wiring](#stream-wiring)
    - [Initial Link Handling](#initial-link-handling)
    - [OneSignal Handler Setup](#onesignal-handler-setup)
- [Optional Dependency Handling](#optional-dependency-handling)
- [Provider Teardown](#provider-teardown)
- [Registering the Provider](#registering-the-provider)
- [Related](#related)

## Introduction

`DeeplinkServiceProvider` is the bootstrap entry point for the magic_deeplink plugin. It wires together the platform driver, the singleton manager, and optional third-party handlers (OneSignal) using the Magic Framework's IoC container and two-phase provider lifecycle.

The provider lives at `lib/src/providers/deeplink_service_provider.dart` and extends `ServiceProvider` from `package:magic/magic.dart`.

## Two-Phase Bootstrap

The Magic Framework calls providers in two ordered phases:

| Phase | Method | Constraint |
|-------|--------|------------|
| 1 | `register()` | Sync. Only bind into the container: no other service may be accessed yet. |
| 2 | `boot()` | Async. All providers have been registered. Safe to resolve, configure, and wire services. |

Splitting into two phases guarantees that when `boot()` runs, every binding registered by every other provider is already resolvable from the container.

## Register Phase

```dart
@override
void register() {
  app.singleton('deeplinks', () => DeeplinkManager());
}
```

`register()` binds a single singleton into the container under the key `'deeplinks'`. `DeeplinkManager` uses the standard singleton factory pattern (`factory DeeplinkManager() => _instance`), so the closure and the factory both guarantee one shared instance exists for the lifetime of the app.

Nothing else happens here. Config is not read, the driver is not created, and no other service is accessed: all of that is deferred to `boot()`.

## Boot Phase

`boot()` is `async`. It resolves the already-registered manager and config, then sets up the driver, the live-link stream, the initial link, and the optional OneSignal handler.

```dart
@override
Future<void> boot() async {
  final config = app.make<ConfigRepository>('config');
  final manager = app.make<DeeplinkManager>('deeplinks');

  if (config.get<bool>('deeplink.enabled') == false) {
    return;
  }

  final driverName = config.get<String>('deeplink.driver');
  final driver = driverName == 'app_links' ? _driverFactory() : null;

  // ... driver and handler wiring
}
```

An absent `deeplink.enabled` key means enabled, matching what `doc/getting-started/configuration.md` documents (`Config.get<bool>('deeplink.enabled', true)`); only an explicit `false` skips everything below, including the OneSignal handler setup further down. `_driverFactory` defaults to `AppLinksDriver.new` and is a constructor parameter so a test can substitute a fake; the real driver reads `isSupported` from the host platform, which a test cannot control.

### Driver Initialization

The provider reads `deeplink.driver` from config and creates the matching platform driver. Currently the only supported value is `'app_links'`. The driver is only wired further when it also reports `isSupported`:

```dart
if (driver != null && driver.isSupported) {
  manager.setDriver(driver);
  await driver.initialize(config.get<Map<String, dynamic>>('deeplink') ?? {});

  // ...
}
```

A driver that answers `isSupported == false` (the web arm of `AppLinksDriver`, unconditionally) is never handed to the manager and never initialized: doing so would leave `manager.driver` answering with something that can never deliver a link, and would carry a driver for a mechanism the platform does not have.

`driver.initialize()` is awaited: it must complete before the stream is wired. The entire `deeplink` config map is passed to the driver so it can read any platform-specific keys it requires.

### Stream Wiring

After initialization the driver's `onLink` stream is subscribed, and each URI is delivered once the app's first frame has rendered:

```dart
final firstFrame = WidgetsFlutterBinding.ensureInitialized().endOfFrame;

driver.onLink.listen((uri) {
  unawaited(_deliver(manager, uri, firstFrame));
});
```

`_deliver` awaits `firstFrame`, then calls `manager.handleUri(uri, source: DeeplinkSource.osLink)` inside a `try`/`catch` that reports a failure through magic's `Log` (guarded by `Magic.bound('log')`) rather than letting it escape as an unhandled async error. Every URI the driver emits, Universal Links on iOS and App Links on Android, is dispatched this way, fanning out to all registered handlers and emitting on `manager.onLink`.

### Initial Link Handling

When the app is cold-started from a deep link, `app_links` serves that link as the first emission of the same `onLink` stream the block above already subscribes to (its own documentation describes the stream as carrying "all events: initial link and further"), so there is exactly one delivery path rather than two. An earlier version of this provider also called `manager.getInitialLink()` separately; on Android that read the same cold-start link `app_links` had already put on the stream, so a single tap ran the whole handler chain twice. That separate call is gone. `DeeplinkManager.getInitialLink()` remains on the manager for a consumer that wants to ask directly, and nothing in the provider calls it.

Fetching the link synchronously during boot would be unsafe regardless: the app's router is not yet mounted at that point, and `_deliver` accounts for this by awaiting `WidgetsFlutterBinding.ensureInitialized().endOfFrame`, captured once before the subscription is created. `endOfFrame` is a stronger guarantee than the `Future.delayed(Duration.zero, ...)` this replaced: it names the event itself (the router finishes building while the first frame is being built) rather than guessing a duration, and it still resolves via `SchedulerBinding`'s own scheduling even when nothing is actively drawing a frame, so a link delivered to an idle application is not left waiting for a frame that never comes.

### OneSignal Handler Setup

If the magic_notifications plugin is present and bound, the provider attaches a `OneSignalDeeplinkHandler` that listens for tapped-push events and converts them to deep link URIs:

```dart
if (app.bound('notifications')) {
  final handler = OneSignalDeeplinkHandler();

  try {
    handler.setup(manager, app.make('notifications'));
    _pushClicks = handler;
  } catch (error) {
    handler.dispose();

    if (Magic.bound('log')) {
      Log.error(
        '[deeplink] Resolving the bound `notifications` manager failed, so '
        'a tapped push notification cannot open a deep link: $error',
      );
    }
  }
}
```

The handler is HELD rather than discarded, so [`dispose()`](#provider-teardown) can reach it.

`setup()` takes the notification MANAGER, not a stream, and subscribes to its `onPushClicked` stream, which the manager owns from construction and republishes onto whenever a driver is attached later. That is what makes this independent of provider order: only the `'notifications'` binding needs to exist at this point, never a resolved push driver, because every provider has registered by the time any of them boots. `OneSignalDeeplinkHandler` then extracts a URI from the notification payload (checking keys `url`, `deep_link`, `link`, `uri`) and calls `manager.handleUri(uri, source: DeeplinkSource.push, payload: data)` for any non-null result, carrying the whole push payload with it.

## Optional Dependency Handling

The integration with magic_notifications is entirely optional. The pattern used has three layers of defence:

1. **`app.bound('notifications')`**: guards the entire block. If the plugin was never registered, the block is skipped without error.
2. **`dynamic` resolution inside the handler**: `app.make('notifications')` is handed to the handler untyped, and `OneSignalDeeplinkHandler` reads `onPushClicked` off it structurally rather than importing `magic_notifications` and naming its type. This keeps magic_deeplink free of a hard package dependency. When the bound manager is too old to publish `onPushClicked`, or publishes something that is not a `Stream`, the handler reports the mismatch through magic's `Log` (guarded by `Magic.bound('log')`) instead of throwing, so a notifications version mismatch degrades deep linking rather than breaking app boot.

3. **A `try`/`catch` around the resolution**, reporting at error level through the same `Log` seam. `app.make('notifications')` runs the binding factory and `onPushClicked` is a getter, so either can throw, and the handler only answers `NoSuchMethodError` by name (that one means "this build is too old"). Anything else, a `StateError` out of an uninitialised manager for instance, would otherwise escape. That matters more here than it looks: magic's `Application.boot` awaits providers in a bare loop with no error handling of its own, so an escaping throw does not degrade deep linking, it aborts app boot and every provider registered after this one, over a plugin that is optional by design.

This is not a swallowing catch. The wiring this replaced had a `catch (e)` whose body was two comment lines, and that is why the feature stayed inert across two years of releases. This one names what failed, then lets boot continue.

This pattern should be followed whenever magic_deeplink optionally integrates with another plugin.

## Provider Teardown

`DeeplinkServiceProvider.dispose()` drops everything `boot()` wired: the push-click handler, the driver's link subscription, the driver itself, and the manager's own reference to it (`DeeplinkManager.forgetDriver()`, which the manager has always exposed and nothing called). `_deliver` checks a `_disposed` flag on both sides of its await, since an in-flight delivery waiting on `endOfFrame` has no handle a teardown could cancel it through.

```dart
await provider.dispose();
```

It is provider-level rather than handler-level on purpose. `doc/basics/handlers.md` tells a consumer to call teardown from their service provider, so a `dispose()` reaching only the push handler would read as tearing the provider down while leaving the driver running. It is also idempotent, because a consumer calling it does not know which parts a given deployment actually wired.

## Registering the Provider

Add the provider to the `providers` list in your app's `config/app.dart`:

```dart
import 'package:magic_deeplink/magic_deeplink.dart';

Map<String, dynamic> get appConfig => {
  'app': {
    'providers': [
      // ... other providers
      (app) => DeeplinkServiceProvider(app),
    ],
  },
  'deeplink': {
    'enabled': true,
    'driver': 'app_links',
    // platform-specific keys passed through to driver.initialize()
  },
};
```

The provider's position relative to any provider registering `'notifications'` does not matter. `app.bound('notifications')` is checked during boot, after every provider has registered, and the OneSignal handler subscribes to the notification manager's own stream rather than to a driver, so it does not need that provider to have booted yet either.

## Related

- [DeeplinkManager](https://magic.fluttersdk.com/packages/deeplink/architecture/deeplink-manager): singleton manager: handler chain, stream, initial link cache
- [Drivers](https://magic.fluttersdk.com/packages/deeplink/basics/drivers): platform driver wrapping the `app_links` package
- [Handlers](https://magic.fluttersdk.com/packages/deeplink/basics/handlers): notification-to-URI bridge
- [Magic Framework: Service Providers](https://magic.fluttersdk.com/getting-started/service-providers): two-phase lifecycle reference
