> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zixflow.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Push Notification Tracking

> Push Notification Tracking — Zixflow Flutter SDK integration guide.

### Overview

Zixflow automatically tracks push notification delivery and engagement metrics to help you measure campaign performance. The SDK tracks three key lifecycle events:

1. **Delivery Confirmed** - When the notification arrives on the device
2. **Notification Opened** - When the user taps the notification
3. **Action Button Clicked** - When the user taps an action button

### How Push Tracking Works

When Zixflow sends a push notification, it includes special tracking fields in the data payload:

| Field                    | Purpose                                                |
| ------------------------ | ------------------------------------------------------ |
| `Zixflow-Delivery-ID`    | Unique ID for this delivery (links to campaign record) |
| `Zixflow-Delivery-Token` | The FCM/APNs token the notification was sent to        |

**Example push payload:**

```json theme={null}
{
  "Zixflow-Delivery-ID": "626533406292836846",
  "Zixflow-Delivery-Token": "dcFRlDhiRbehM1vg...",
  "title": "Flash Sale! 70% OFF",
  "body": "Limited time only — grab your deal now!",
  "deeplink_url": "https://yourapp.com/sale",
  "image_url": "https://cdn.yourapp.com/banner.png",
  "action_buttons": "[{\"name\":\"Shop Now\",\"deeplink\":\"https://yourapp.com/sale\"},{\"name\":\"Remind Me\",\"deeplink\":\"\"}]"
}
```

> **Note:** The SDK automatically handles most tracking for you when using the default push implementation. This section covers advanced customization scenarios.

***

### Tracking Events

#### 1. Delivery Confirmed

**When to track:** The moment the push payload arrives on the device

**SDK method:** `Zixflow.instance.trackMetric(deliveryID: ..., deviceToken: ..., event: MetricEvent.delivered)`

**Implementation:**

```dart theme={null}
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:zixflow/zixflow.dart';

// Listen for foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  final deliveryId    = message.data['Zixflow-Delivery-ID'] ?? '';
  final deliveryToken = message.data['Zixflow-Delivery-Token'] ?? cachedFcmToken ?? '';

  if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
    Zixflow.instance.trackMetric(
      deliveryID:  deliveryId,
      deviceToken: deliveryToken,
      event:       MetricEvent.delivered,
    );
  }
});
```

> **Important:** The Zixflow SDK automatically tracks delivery. Manual tracking is only needed for custom implementations.

***

#### 2. Notification Opened

**When to track:** When the user taps the notification banner (body or action button)

**SDK method:** `Zixflow.instance.trackMetric(deliveryID: ..., deviceToken: ..., event: MetricEvent.opened)`

**Implementation:**

```dart theme={null}
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:zixflow/zixflow.dart';

// Background tap — app was backgrounded
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  _trackOpened(message.data, cachedFcmToken);
  _handleDeeplink(message.data['deeplink_url']);
});

// Terminated tap — app was closed when notification was tapped
final initial = await FirebaseMessaging.instance.getInitialMessage();
if (initial != null) {
  _trackOpened(initial.data, cachedFcmToken);
  _handleDeeplink(initial.data['deeplink_url']);
}

void _trackOpened(Map<String, String> data, String? fallbackToken) {
  final deliveryId    = data['Zixflow-Delivery-ID'] ?? '';
  final deliveryToken = data['Zixflow-Delivery-Token'] ?? fallbackToken ?? '';

  if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
    Zixflow.instance.trackMetric(
      deliveryID:  deliveryId,
      deviceToken: deliveryToken,
      event:       MetricEvent.opened,
    );
  }
}
```

> **Note:** The SDK automatically tracks opens when using the default push implementation.

***

#### 3. Action Button Clicked

**When to track:** When the user taps a named action button

**SDK method:** `Zixflow.instance.track(name: "Push Notification Action Clicked", properties: {...})`

**Implementation (using flutter\_local\_notifications):**

```dart theme={null}
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:zixflow/zixflow.dart';
import 'dart:convert';

// In FlutterLocalNotificationsPlugin.initialize()
onDidReceiveNotificationResponse: (NotificationResponse response) {
  final payload = jsonDecode(response.payload ?? '{}') as Map<String, dynamic>;

  if (response.actionId == null) {
    // Body tap → opened
    _trackOpened(payload, null);
  } else {
    // Action button tap → opened + clicked
    _trackOpened(payload, null);

    final actionIndex = int.tryParse(
        response.actionId!.replaceAll(RegExp(r'\D'), '')) ?? -1;
    final buttons = _parseButtons(payload['action_buttons']);
    final buttonName = (actionIndex >= 0 && actionIndex < buttons.length)
        ? buttons[actionIndex]['name'] ?? 'Action ${actionIndex + 1}'
        : 'Action ${actionIndex + 1}';

    Zixflow.instance.track(
      name: 'Push Notification Action Clicked',
      properties: {
        'Zixflow-Delivery-ID':    payload['Zixflow-Delivery-ID'] ?? '',
        'Zixflow-Delivery-Token': payload['Zixflow-Delivery-Token'] ?? '',
        'notification_id':        payload['Zixflow-Delivery-ID'] ?? '',
        'title':                  payload['title'] ?? '',
        'action_id':              response.actionId!,
        'action_index':           actionIndex,
        'action_name':            buttonName,
        'action_deeplink':        _resolveButtonDeeplink(buttons, actionIndex),
        'source':                 'local_notification',
      },
    );
  }
},

List<Map<String, dynamic>> _parseButtons(dynamic raw) {
  if (raw == null) return [];
  try {
    final decoded = raw is String ? jsonDecode(raw) : raw;
    return List<Map<String, dynamic>>.from(decoded as List);
  } catch (_) {
    return [];
  }
}
```

**Action button properties:**

| Property                 | Type    | Required    | Description                      |
| ------------------------ | ------- | ----------- | -------------------------------- |
| `Zixflow-Delivery-ID`    | string  | ✅           | Links event to campaign delivery |
| `action_index`           | integer | ✅           | 0-based index of button tapped   |
| `action_name`            | string  | ✅           | Button label (e.g., "Shop Now")  |
| `Zixflow-Delivery-Token` | string  | Recommended | FCM/APNs token                   |
| `action_deeplink`        | string  | Recommended | Button's URL                     |

***

### Action Buttons Format

The `action_buttons` field is a JSON-encoded string containing button definitions:

**Raw payload value:**

```
"[{\"name\":\"Shop Now\",\"deeplink\":\"https://yourapp.com/sale\"},{\"name\":\"Remind Me\",\"deeplink\":\"\"}]"
```

**Parsed structure:**

```json theme={null}
[
  { "name": "Shop Now",  "deeplink": "https://yourapp.com/sale" },
  { "name": "Remind Me", "deeplink": "" }
]
```

**Parsing example:**

```dart theme={null}
import 'dart:convert';

List<Map<String, dynamic>> parseActionButtons(dynamic raw) {
  if (raw == null) return [];

  try {
    final decoded = raw is String ? jsonDecode(raw) : raw;
    return List<Map<String, dynamic>>.from(decoded as List);
  } catch (e) {
    print('Error parsing action buttons: $e');
    return [];
  }
}

// Usage
final buttons = parseActionButtons(message.data['action_buttons']);
for (var i = 0; i < buttons.length; i++) {
  print('Button ${i}: ${buttons[i]['name']} -> ${buttons[i]['deeplink']}');
}
```

**Rules:**

* Maximum 2 buttons per notification (recommended for consistency across platforms)
* `deeplink` may be empty — handle gracefully
* Button index is 0-based (first button = index 0)

***

### Complete Push Tracking Implementation

Here's a complete example integrating all tracking events:

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:zixflow/zixflow.dart';
import 'dart:convert';

// Background message handler (top-level function)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();

  // Track delivery
  final deliveryId = message.data['Zixflow-Delivery-ID'] ?? '';
  final deliveryToken = message.data['Zixflow-Delivery-Token'] ?? '';

  if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
    Zixflow.instance.trackMetric(
      deliveryID: deliveryId,
      deviceToken: deliveryToken,
      event: MetricEvent.delivered,
    );
  }
}

class PushNotificationHandler {
  final FlutterLocalNotificationsPlugin _localNotifications =
      FlutterLocalNotificationsPlugin();
  String? _fcmToken;

  Future<void> initialize() async {
    // Register background handler
    FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

    // Get and cache FCM token
    _fcmToken = await FirebaseMessaging.instance.getToken();
    if (_fcmToken != null) {
      Zixflow.instance.registerDeviceToken(deviceToken: _fcmToken!);
    }

    // Listen for token refresh
    FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
      _fcmToken = newToken;
      Zixflow.instance.registerDeviceToken(deviceToken: newToken);
    });

    // Initialize local notifications
    await _initializeLocalNotifications();

    // Set up message listeners
    _setupMessageListeners();
  }

  Future<void> _initializeLocalNotifications() async {
    const androidSettings = AndroidInitializationSettings('@drawable/ic_notification');
    const iosSettings = DarwinInitializationSettings();

    await _localNotifications.initialize(
      InitializationSettings(android: androidSettings, iOS: iosSettings),
      onDidReceiveNotificationResponse: _handleNotificationResponse,
    );
  }

  void _setupMessageListeners() {
    // Foreground messages
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      _trackDelivery(message);
      _showLocalNotification(message);
    });

    // Background tap
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      _trackOpened(message.data);
      _handleDeeplink(message.data['deeplink_url']);
    });

    // Terminated tap
    FirebaseMessaging.instance.getInitialMessage().then((message) {
      if (message != null) {
        _trackOpened(message.data);
        _handleDeeplink(message.data['deeplink_url']);
      }
    });
  }

  void _trackDelivery(RemoteMessage message) {
    final deliveryId = message.data['Zixflow-Delivery-ID'] ?? '';
    final deliveryToken = message.data['Zixflow-Delivery-Token'] ?? _fcmToken ?? '';

    if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
      Zixflow.instance.trackMetric(
        deliveryID: deliveryId,
        deviceToken: deliveryToken,
        event: MetricEvent.delivered,
      );
    }
  }

  void _trackOpened(Map<String, dynamic> data) {
    final deliveryId = data['Zixflow-Delivery-ID'] ?? '';
    final deliveryToken = data['Zixflow-Delivery-Token'] ?? _fcmToken ?? '';

    if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
      Zixflow.instance.trackMetric(
        deliveryID: deliveryId,
        deviceToken: deliveryToken,
        event: MetricEvent.opened,
      );
    }
  }

  void _handleNotificationResponse(NotificationResponse response) {
    if (response.payload == null) return;

    final payload = jsonDecode(response.payload!) as Map<String, dynamic>;

    // Track opened
    _trackOpened(payload);

    // If action button was tapped
    if (response.actionId != null) {
      _trackActionClick(payload, response.actionId!);
    }

    // Handle navigation
    _handleDeeplink(payload['deeplink_url']);
  }

  void _trackActionClick(Map<String, dynamic> payload, String actionId) {
    final actionIndex = int.tryParse(actionId.replaceAll(RegExp(r'\D'), '')) ?? -1;
    final buttons = _parseButtons(payload['action_buttons']);
    final actionName = (actionIndex >= 0 && actionIndex < buttons.length)
        ? buttons[actionIndex]['name'] ?? 'Action ${actionIndex + 1}'
        : 'Action ${actionIndex + 1}';
    final actionDeeplink = (actionIndex >= 0 && actionIndex < buttons.length)
        ? buttons[actionIndex]['deeplink'] ?? ''
        : '';

    Zixflow.instance.track(
      name: 'Push Notification Action Clicked',
      properties: {
        'Zixflow-Delivery-ID': payload['Zixflow-Delivery-ID'] ?? '',
        'Zixflow-Delivery-Token': payload['Zixflow-Delivery-Token'] ?? '',
        'action_index': actionIndex,
        'action_name': actionName,
        'action_deeplink': actionDeeplink,
      },
    );
  }

  List<Map<String, dynamic>> _parseButtons(dynamic raw) {
    if (raw == null) return [];
    try {
      final decoded = raw is String ? jsonDecode(raw) : raw;
      return List<Map<String, dynamic>>.from(decoded as List);
    } catch (_) {
      return [];
    }
  }

  Future<void> _showLocalNotification(RemoteMessage message) async {
    // Implementation for showing local notification
  }

  void _handleDeeplink(String? deeplink) {
    if (deeplink == null || deeplink.isEmpty) return;
    // Implement your deeplink navigation logic
    print('Navigating to: $deeplink');
  }
}
```

***

### Tracking Decision Flowchart

```
Notification received?
├── YES → trackMetric(delivered)        [automatic with SDK]
│
User interacted?
├── Tapped notification body
│   └── trackMetric(opened)             [automatic with SDK]
│       └── Navigate to deeplink_url
│
└── Tapped action button
    ├── trackMetric(opened)             [automatic with SDK]
    └── track("Push Notification Action Clicked")
        └── Navigate to button's deeplink
```

***

### Fallback for Non-Zixflow Push

If a push notification doesn't contain `Zixflow-Delivery-ID` (sent from another source), the SDK automatically skips tracking.

For custom analytics on non-Zixflow pushes, use a different event name:

```dart theme={null}
// Don't use reserved push event names for non-Zixflow pushes
Zixflow.instance.track(
  name: 'External Push Received',
  properties: {
    'notification_id': message.messageId ?? '',
    'title': message.notification?.title ?? message.data['title'] ?? '',
    'body': message.notification?.body ?? message.data['body'] ?? '',
    'source': 'external',
  },
);
```

> **Important:** Push notification event names (`Push Notification Delivered`, `Push Notification Opened`, `Push Notification Action Clicked`) are reserved for Zixflow's delivery pipeline.

***

### Testing Push Tracking

#### Verify Tracking Events

Enable debug logging to see tracking events:

```dart theme={null}
final config = ZixflowConfig(
  apiKey: 'YOUR_API_KEY',
  logLevel: LogLevel.debug,
);
```

Look for these log messages:

* `[Zixflow] Push Notification Delivered tracked`
* `[Zixflow] Push Notification Opened tracked`
* `[Zixflow] Push Notification Action Clicked tracked`

#### Check Campaign Analytics

1. Log into Zixflow dashboard
2. Navigate to **Messaging** → **Push Notifications**
3. View campaign metrics:
   * **Sent** - Total notifications sent
   * **Delivered** - Arrived on device
   * **Opened** - User tapped notification
   * **Clicked** - User tapped action button

***

***
