AppsFlyer Flutter Plugin

repository·master·Indexed 41 days ago

https://github.com/appsflyersdk/appsflyer-flutter-plugin

A Flutter plugin providing mobile attribution and analytics for Android and iOS using the AppsFlyer SDK. Version 1.0.0 (SDK v7.x.x) utilizes an RPC layer architecture and requires Flutter 3.35.0+ and Dart 3.9.0. Key capabilities include deep linking via OneLink, in-app event logging, ad revenue tracking, and a Purchase Connector for IAP validation. Includes support for DMA/GDPR compliance and Android AD_ID permission management.

Tokens
97K
Snippets
245
Records
392
Agent score
40%

What's inside AppsFlyer

  1. Overview of AppsFlyer Flutter Plugin Features

    master

    The AppsFlyer Flutter Plugin provides a comprehensive suite of features for mobile attribution, event tracking, and growth marketing. Features are organized into six functional categories:

    1. sdkCore: Handles the SDK lifecycle, identity (CUID, UID), privacy/consent (GDPR, DMA, TCF), and low-level configuration (custom hosts, debug logging).
    2. eventsAndRevenue: Enables reporting of in-app events, ad revenue, and monetary context (currency codes, custom data).
    3. purchaseValidation: Facilitates server-side validation of purchases and subscriptions via the Purchase Connector (V2 cross-platform API).
    4. deepLinking: Manages deep-link resolution, Unified Deep Linking (UDL) callbacks, and push notification data handling.
    5. oneLinkAndGrowth: Supports OneLink-based user invite/referral link generation and cross-promotion tracking.
    6. platformIntegration: Provides hooks for partner ecosystems (SKAdNetwork, Facebook) and platform-specific attribution (Android preinstall, iOS device data controls).
  2. What is created by the AI Delivery Workflow Template

    master

    The AI Delivery Workflow Template sets up a structured environment for AI-driven development using Claude Code. It creates a .claude/ directory containing workflow maps, specialized agent skills (PM, Engineer, Researcher, Analyst), and custom commands. It also establishes a documentation structure in docs/ for features, issue cases, PRDs, tech designs, and research logs to maintain a high-fidelity knowledge base for the project.

    .claude/
    ├── WORKFLOW.md                      — skill communication map
    ├── commands/
    │   ├── af-ship.md                   — /af-ship entry point
    │   ├── af-ship-from-prd.md          — /af-ship-from-prd entry point
    │   ├── af-ship-from-tech-design.md  — /af-ship-from-tech-design entry point
    │   └── af-quiz-me.md                — /af-quiz-me command
    ├── skills/
    │   ├── af-ship-orch/SKILL.md        — workflow router
    │   ├── alice-pm/SKILL.md            — PM agent (PRD, challenges)
    │   ├── dave-<domain>-engineer/SKILL.md — engineering agent (code, tech design)
    │   ├── bob-<domain>-researcher/SKILL.md — research agent (platform/API)
    │   └── erin-<domain>-analyst/SKILL.md — payload/schema agent
    └── prompts/
        ├── generate-feature-catalog.md    — feature catalog builder
        └── generate-issue-cases.md        — issue case bank builder
    CLAUDE.md                            — project rules and agent contracts
    templates/
    └── af-tech-quiz-template.html       — quiz UI template
    
    # After running prompts:
    docs/
    ├── features/
    │   ├── INDEX.md                     — feature catalog index
    │   ├── DIAGRAM.md                   — runtime + initialization dependency diagrams
    │   └── F-NNN-*.md                   — one file per feature
    ├── issue-cases/
    │   ├── INDEX.md                     — issue case index + hot zones map
    │   ├── GUARDRAILS.md                — engineering rules derived from past bugs
    │   └── IC-NNN-*.md                  — one file per issue case
    ├── prds/                            — PRDs written during /af-ship
    ├── tech-designs/                    — tech designs written during /af-ship
    └── researches/                      — research logs written by Bob
  3. Key files and responsibilities in the AppsFlyer Flutter Plugin

    master

    The plugin is organized into several layers. Understanding which files govern specific behaviors helps in debugging and extending the SDK:

    Public Library & Dart SDK

    • lib/appsflyer_sdk.dart: The main entry point containing library exports.
    • lib/src/appsflyer_sdk.dart: Implements the Public API, handles per-platform payload adaptation, RPC invocation, and typed event callbacks.
    • lib/src/appsflyer_event.dart: Responsible for decoding and normalizing native events into Dart models.
    • lib/src/appsflyer_exception.dart: Defines public SDK exceptions.
    • lib/src/appsflyer_listener_registry.dart: Manages private event dispatching (one callback per event for af-events).

    Platform Implementations

    • Android:
      • android/src/main/kotlin/com/appsflyer/appsflyersdk/AppsflyerSdkPlugin.kt: Handles channels, RPC dispatch, lifecycle forwarding, and the af-events sink adapter.
      • android/src/main/kotlin/com/appsflyer/appsflyersdk/AppsFlyerEventBus.kt: Manages process-scoped event buffering and FIFO replay.
      • android/src/main/kotlin/com/appsflyer/appsflyersdk/AppsFlyerRpcBridge.kt: Owns the Android RPC bridge.
    • iOS:
      • ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.swift: Handles channels, RPC dispatch, and lifecycle forwarding.
      • ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.swift: Manages Universal Link/URL callback queuing.
      • ios/appsflyer_sdk/Sources/appsflyer_sdk/AFRPCBridge.swift: Provides access to the @MainActor-isolated AppsFlyerRPCBridge.

    Optional Components

    • Purchase Connector: Located in lib/src/purchase_connector/ (Dart), android/src/main/include-connector/ (Android), and ios/PurchaseConnector/ (iOS). This is an optional non-core channel for purchase-related functionality.
  4. Configure optional AppsFlyer features

    master

    Beyond the core integration, the plugin supports several advanced modules:

    • Advanced features: Includes uninstall measurement, user invites, purchase validation, and Android out-of-store attribution.
    • Privacy, identity, consent & DMA: Provides tools for configuring anonymization, SDK opt-out, hashed PII, and compliance with DMA/GDPR consent requirements.
    • Purchase Connector: Enables automatic IAP (In-App Purchase) and subscription revenue validation. Note that for iOS, this requires CocoaPods.
  5. What is the Purchase Connector and when to use it

    master

    The Purchase Connector is an optional enhancement for the AppsFlyer SDK Flutter Plugin designed for apps measuring in-app purchase (IAP) or subscription revenue. It automatically validates and measures revenue from in-app purchases and auto-renewable subscriptions to provide accurate ROAS measurements and customer lifecycle insights.

    Key Requirements & Constraints:

    • Subscription Required: Requires an ROI360 subscription.
    • Avoid Duplication: When using Purchase Connector, do not also send the same purchase via validateAndLogInAppPurchase or an in-app event containing revenue, as this will result in duplicate revenue reporting.
    • Implementation Model: The Purchase Connector observes purchases; it does not implement the purchase flow itself. You must provide the purchase flow using native code or a Flutter plugin like in_app_purchase.
    • iOS Compatibility: Requires CocoaPods. It is not compatible with Swift Package Manager (SPM). If you use SPM, calling Purchase Connector APIs will throw a MissingPluginException.
  6. Understand event buffering and delivery constraints

    master

    The plugin uses in-memory event buffering for SDK events. Developers should be aware of the following constraints to avoid data loss:

    Buffering Limits

    • Capacity: Both Android and iOS cap the buffer at 64 events. When the limit is reached, the oldest events are dropped, and a warning is logged.
    • Persistence: Buffering is in-memory only; events do not survive process death.
    • Scope: The buffer is process-scoped on Android and engine-scoped on iOS.

    Delivery Mechanism

    • Subscription: The Dart side attaches its single af-events subscription upon the first register*Listener call.
    • Replay: Attaching the subscription flushes the entire native buffer at once. The plugin uses _AppsFlyerListenerRegistry to hold replayed events (up to 64) for listeners that have not yet registered.
    • Dropped Events: An event arriving after its listener has been registered and then subsequently unregistered will be logged and dropped.

    Android Specifics

    • On Android, createEventSink returns DELIVERED on success or RETRY_LATER if the engine is detached or if an unexpected RuntimeException occurs.
  7. How native events are delivered to Dart

    master

    The plugin uses a reverse path (Native SDK → RPC → Dart) to deliver events. Native RPC event notifiers emit JSON envelopes which are forwarded through an af-events channel.

    Key behaviors:

    • Buffering & Replay: Events emitted before the Dart layer attaches an EventChannel listener are buffered by the platform plugin (up to 64 events) and replayed when the listener is established.
    • Android vs iOS Buffering: On Android, the buffer is process-scoped (AppsFlyerEventBus), allowing events to survive engine teardown. On iOS, the buffer is engine-scoped (pendingEvents).
    • Lazy Subscription: The af-events subscription is established lazily upon the first call to a register*Listener method, not in the constructor. This ensures the native buffer is flushed only when the application is ready to receive events.
    • Event Normalization: The _AppsFlyerEvent.fromNative method normalizes the RPC JSON string. The event field becomes _AppsFlyerEvent.name (must be a non-empty string), and the data field is converted to a Map<String, dynamic> (or {} if null).
    // Internal mechanism for ensuring the event stream is active
    void _ensureEventsSubscribed() {
      _ensureIsolateCanReachPlatform();
      _eventSubscription ??= _eventChannel.receiveBroadcastStream().listen(
        _handleNativeEvent,
        onError: (Object error, StackTrace stackTrace) { /* log only */ },
        onDone: () => _eventSubscription = null,
      );
    }
  8. How Deep-Link URL Resolution Allow-list works

    master

    The setResolveDeepLinkURLs mechanism allows an app to expand the scope of domains the AppsFlyer SDK monitors for deep-link resolution.

    The Problem

    By default, the native SDK only resolves known AppsFlyer domains. If a user clicks a link on a third-party domain that wraps an AppsFlyer OneLink, the SDK might ignore it, preventing the deep-link payload from surfacing.

    The Solution

    By calling setResolveDeepLinkURLs, you register an 'allow-list' of domains. When the app receives a link from one of these registered domains, the SDK is triggered to resolve it, extracting the underlying OneLink attribution and deep-link data.

    Implementation Flow

    1. The Dart app calls AppsFlyerSdk.setResolveDeepLinkURLs(urls).
    2. The request is sent via an RPC (Remote Procedure Call) through the af-api MethodChannel.
    3. Android: The AppsFlyerRpcHandler receives the request and calls AppsFlyerLib.getInstance().setResolveDeepLinkURLs().
    4. iOS: The AppsFlyerRPCBridge receives the request and sets sdk.resolveDeepLinkURLs.
    5. The Future<void> completes once the native SDK has successfully updated its internal allow-list.
  9. Handle AppsFlyerException

    master

    Errors returned by the plugin or native layer.

    • Numeric Codes: Native failures often use HTTP-style codes (e.g., 400, 404, 422, 500).
    • Code is null: Occurs when the platform supplies a non-numeric code or during plugin transport failures like SERIALIZATION_ERROR (common on iOS when encoding non-finite doubles).
    • Method Not Found: The plugin returns 404 if a platform-only API is called on the wrong platform.
    • Invalid Parameters: The plugin returns 422 if a method exists but parameters are rejected.

    Best Practice: Match on the platform-only marker in the documentation rather than relying solely on the numeric code.

  10. Technical limitations of SPM and PurchaseConnector

    master

    A key finding in the SPM research was that Swift Package Manager Traits do NOT unblock PurchaseConnector.

    The Blocker: Even if SwiftPM supports traits (SE-0450), Flutter's own plugin build tooling currently lacks support for conditional compilation of plugin code. This means that features like PurchaseConnector, which rely on compile flags (e.g., #ifdef ENABLE_PURCHASE_CONNECTOR) to be gated, cannot be conditionally included via SPM through the Flutter plugin system.

    In the CocoaPods implementation, this is handled via $AppsFlyerPurchaseConnector. In the SPM implementation, the PurchaseConnector is omitted from the SPM target, and the existing #ifdef guards in the code ensure it is compiled out of targets where the macro is not defined.