# Flutter

Pure Dart, no native code of its own. iOS and Android.

## Install

```sh
flutter pub add apparent
```

Android release builds need the internet permission (Flutter's template only adds it for debug):

```xml android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
```

## Start it

In main(), before runApp:

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

await Apparent.start(app: '<app id>', key: '<write key>', apiUrl: 'https://in.apparent.fyi');
```

No need to wait for it. Add `disabled: kDebugMode` if debug builds shouldn't send.

## Screens

```dart
MaterialApp(navigatorObservers: [ApparentNavigatorObserver()], routes: {...});
```

Each page route is sent by its name (`/settings/account` → `settings/account`, `/` → `home`). Dialogs, sheets and unnamed routes are skipped. With go_router, pass it in `GoRouter(observers: [...])`. Your own names: `ApparentNavigatorObserver(screenName: (route) => …)`, or `Apparent.screen('checkout')`.

## Events and users

```dart
Apparent.track('paywall_view', {'source': 'onboarding'});
Apparent.setProperties({'plan': 'pro'});   // on every later event; null removes one
Apparent.identify('user-42');               // after sign-in
Apparent.logout();                          // on sign-out
```

More in [Events and users](https://apparent.fyi/docs/events).

## Purchases

```dart
await Apparent.linkRevenueCat((attrs) => Purchases.setAttributes(attrs));
```

## Errors and opting out

- Flutter errors and uncaught Dart errors are sent as `error` events. Your own handlers (Crashlytics, Sentry) still run.
- Native iOS and Android crashes are not caught.
- Errors you catch: `Apparent.captureError(e, stack: s)`.
- `Apparent.optOut()` / `Apparent.optIn()`, remembered.
- Ask for consent first? Start with `optOutByDefault: true` and call `Apparent.optIn()` once they agree. Nothing is collected before that.
- Nothing showing up? In a debug build the SDK prints `Apparent: app id or key is wrong — events are not being saved` once when the app id or key doesn't match. Copy both again from Connect.
- Simulators and emulators say so themselves (`simulator` in each batch), so they're never counted, even in a release build.

## Links, notifications and your own filter

Flutter can't see links or notification taps by itself, so pass them on from the package you already use.

```dart
// app_links: the link the app was launched with, then each later one
AppLinks().uriLinkStream.listen(Apparent.trackDeepLink);

// firebase_messaging: a tapped notification
final first = await FirebaseMessaging.instance.getInitialMessage();
if (first != null) Apparent.trackPushOpened(first.data, id: first.messageId);
FirebaseMessaging.onMessageOpenedApp.listen((m) => Apparent.trackPushOpened(m.data, id: m.messageId));
```

- **Links** are sent as `deep_link_opened` with the link (secrets like `token=` or `code=` replaced by `[redacted]`) and its `utm_source`, `utm_medium` and `utm_campaign`. A campaign link that opens the app on its first day counts as where that person came from. Turn it off with `trackDeepLinks: false`.
- **Notifications** are sent as `push_opened` with the `campaign` and `title` you put in the notification's data. Never the text people saw. Turn it off with `trackPushOpens: false`.
- **Your own filter.** `beforeSend` sees every event before it is saved. Return it changed, or `null` to drop it:

```dart
Apparent.start(
  app: '<app id>', key: '<write key>',
  beforeSend: (event) {
    if (event.name == 'debug_tap') return null;
    event.props?.remove('email');
    return event;
  },
);
```

## Privacy

The package has no `PrivacyInfo.xcprivacy` of its own (its plugins ship theirs), so add Apparent's rows to your app's. See [Privacy answers](https://apparent.fyi/docs/privacy).
