Releva Flutter SDK

This package offers an easy way to integrate Releva's AI-powered e-commerce personalization platform into your mobile app built on Flutter.

Features

🎯 E-commerce Personalization

  • Product Recommendations: AI-powered product suggestions with real-time personalization
  • Dynamic Content: Personalized banners and content blocks based on user behavior
  • Advanced Filtering: Complex product filtering with nested AND/OR logic, price ranges, custom fields
  • Smart Search: Search tracking with result optimization and recommendation integration

πŸ“± Mobile Tracking & Analytics

  • Automatic Screen Tracking: NavigatorObserver for seamless route/screen tracking
  • E-commerce Events: Product views, cart changes, checkout tracking, search analytics
  • Custom Events: Flexible event system for business-specific tracking needs
  • Real-Time Analytics: ClickHouse integration for comprehensive engagement insights

πŸ”” Push Notifications

  • Firebase Integration: Complete FCM push notification system with data-only payloads
  • Rich Notifications: Images, action buttons, and deep linking support
  • Navigation: Automatic screen navigation from notification taps
  • Engagement Analytics: Delivered, opened, dismissed tracking to ClickHouse
  • Backward Compatible: Supports both new (data-only) and legacy (notification+data) payloads
  • Cross-Platform: Android, iOS, Huawei device support

βš™οΈ Flexible Configuration

  • Modular Setup: Enable only needed features
  • Production Ready: Robust error handling, offline support, automatic retries

Installation

Add the Releva Flutter SDK to your project by adding the following to your pubspec.yaml:

dependencies:
  releva_sdk: ^0.0.38

Then run:

flutter pub get

Firebase Setup (Required for Push Notifications)

1. Install Firebase dependencies

flutter pub add firebase_core firebase_messaging

2. Configure Firebase for your project

flutterfire configure

This will create firebase_options.dart with your project configuration.

3. Android Configuration

Add to android/app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:label="your_app_name"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            ...>

            <!-- ADDED for Releva -->
            <intent-filter>
                <action android:name="RELEVA_NOTIFICATION_CLICK" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        ...
    </application>
</manifest>

4. iOS Configuration

iOS notification categories are automatically configured by the Releva SDK! No manual AppDelegate modification is required.

The SDK automatically registers notification categories when your app starts, enabling:

  • Action buttons on notifications
  • Deep linking
  • Notification tap handling

Optional: Notification Service Extension (for dynamic button text on background/terminated notifications)

If you want to display dynamic button text from push notification payloads even when the app is in the background or terminated, you need to add a Notification Service Extension. This is required for showing custom button text on notifications that arrive while the app is not running.

Why is this needed? iOS requires notification action buttons to be registered BEFORE a notification is displayed. When the app is in the background, a Notification Service Extension allows you to modify the notification (including registering dynamic categories) right before it's shown to the user.

Setup Steps:

See the detailed setup guide in ios/SETUP_NOTIFICATION_SERVICE_EXTENSION.md

Quick Summary:

  1. In Xcode, go to File β†’ New β†’ Target β†’ Notification Service Extension
  2. Name it RelevaNotificationService
  3. Add Firebase/Messaging to the extension in your Podfile
  4. Replace the generated NotificationService.swift with the implementation from releva_sdk/ios/Templates/NotificationService.swift
  5. Ensure your notification payload includes mutable-content: 1 in the aps section

Without the extension:

  • Foreground notifications: βœ… Dynamic button text works
  • Background notifications: ❌ Shows "Open" button
  • Terminated app notifications: ❌ Shows "Open" button

With the extension:

  • Foreground notifications: βœ… Dynamic button text works
  • Background notifications: βœ… Dynamic button text works
  • Terminated app notifications: βœ… Dynamic button text works

Initialize the Releva Client

import 'package:releva_sdk/client.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late RelevaClient client;

  @override
  void initState() {
    super.initState();

    // Initialize Releva Client
    // realm - use '' unless instructed otherwise by your account manager
    // accessToken - use the access token provided by your account manager
    client = RelevaClient(
      '',                  // realm
      '<yourAccessToken>', // access token
    );

    _initializeSDK();
  }

  Future<void> _initializeSDK() async {
    // Set device ID based on your current logic for device tracking
    await client.setDeviceId('<deviceId>');

    // If a user has registered or logged in, provide a profileId
    // This must be consistent across channels and integrations
    await client.setProfileId('<profileId>');

    // Enable app push notification engagement metrics collection
    // IMPORTANT: ensure that you have set the profileId and deviceId first!
    await client.enablePushEngagementTracking();

    // Register FCM token
    _registerPushToken();
  }

  Future<void> _registerPushToken() async {
    final messaging = FirebaseMessaging.instance;

    // Request notification permissions
    final settings = await messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      // Get FCM token
      String? token = await messaging.getToken();
      if (token != null) {
        DeviceType deviceType = Platform.isIOS
            ? DeviceType.ios
            : DeviceType.android;
        await client.registerPushToken(deviceType, token);
      }

      // Handle token refresh
      messaging.onTokenRefresh.listen((newToken) async {
        DeviceType deviceType = Platform.isIOS
            ? DeviceType.ios
            : DeviceType.android;
        await client.registerPushToken(deviceType, newToken);
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // Add automatic screen tracking
      navigatorObservers: [client.createScreenTrackingService()],
      // Add navigation key for deep linking
      navigatorKey: NavigationService.instance.navigatorKey,
      home: HomeScreen(client: client),
    );
  }
}

Push Notification Engagement Tracking

IMPORTANT: Only do this if you DO NOT have ANY of the following anywhere in your app OR YOUR OTHER LIBRARIES:

  1. FirebaseMessaging.instance.onMessage.listen((message) => ...);
  2. FirebaseMessaging.instance.onMessageOpenedApp.listen((message) => ...);
  3. FirebaseMessaging.instance.getInitialMessage().then((message) => ...);
await client.enablePushEngagementTracking();

The SDK will automatically track engagement when notifications are opened.

Custom Notification Tap Callback

If you need to handle navigation yourself when a notification is tapped, you can provide an optional callback. When you provide a callback, the SDK will NOT handle navigation automatically - you have full control:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

await client.enablePushEngagementTracking(
  onNotificationTapped: (NotificationResponse response) {
    print('Notification tapped!');
    print('Response type: ${response.notificationResponseType}');
    print('Action ID: ${response.actionId}'); // null if notification body was tapped, non-null if button was tapped

    // Parse the payload to access notification data
    final data = Uri.splitQueryString(response.payload ?? '');

    // For Releva messages, check: data['click_action'] == 'RELEVA_NOTIFICATION_CLICK'
    if (data['click_action'] == 'RELEVA_NOTIFICATION_CLICK') {
      final deeplink = data['navigate_to_url'];
      final screen = data['navigate_to_screen'];

      // Handle navigation yourself
      if (deeplink != null && deeplink.isNotEmpty) {
        ActionHandler.instance.handleJump(ActionModel.from(deeplink));
      } else if (screen != null) {
        Navigator.pushNamed(context, screen);
      }
    }
  },
);

NotificationResponse Properties:

  • notificationResponseType - Type of response (e.g., NotificationResponseType.selectedNotification)
  • actionId - ID of the tapped button (null if notification body was tapped)
  • payload - URL-encoded string containing notification data (parse with Uri.splitQueryString())

Available Fields in Parsed Payload (data):

  • click_action - Always "RELEVA_NOTIFICATION_CLICK" for Releva messages
  • navigate_to_url - Deep link URL (if target is "url")
  • navigate_to_screen - Screen name (if target is "screen")
  • navigate_to_parameters - JSON string with navigation parameters
  • target - Navigation type: "screen" or "url"
  • title - Notification title
  • body - Notification body text
  • imageUrl - Image URL (if present)
  • button - Action button text (if present)
  • callbackUrl - Engagement tracking URL

Navigation Behavior:

  • βœ… With callback: SDK skips automatic navigation, you handle it in your callback
  • βœ… Without callback: SDK handles navigation automatically based on target, navigate_to_url, and navigate_to_screen fields
  • βœ… Engagement tracking: Always happens automatically regardless of callback presence

When Your Callback Runs:

  • Works in all app states: foreground, background, and terminated
  • Called when notification body OR action button is tapped
  • Runs AFTER SDK's internal engagement tracking completes

OPTION 2: Manually invoke Releva's engagement tracking in your hooks

If you already have Firebase messaging hooks in your app, place the following code in your event hooks:

// When app is in foreground
FirebaseMessaging.instance.onMessage.listen((message) async {
  // Your existing logic...
  await client.trackEngagement(message);
});

// When user taps notification while app is in background
FirebaseMessaging.instance.onMessageOpenedApp.listen((message) async {
  // Your existing logic...
  await client.trackEngagement(message);
});

// When app is opened from terminated state via notification
FirebaseMessaging.instance.getInitialMessage().then((message) async {
  if (message != null) {
    // Your existing logic...
    await client.trackEngagement(message);
  }
});

Send Push Requests

Set Wishlist (if applicable)

import 'package:releva_sdk/types/wishlist/wishlist_product.dart';
import 'package:releva_sdk/types/custom_field/custom_fields.dart';

// If your app supports a wishlist feature, set the active wishlist
// If the user has no wishlist right now, set it to an empty list
WishlistProduct product = WishlistProduct('<productId>', CustomFields.empty());
await client.setWishlist([product]);

Set Cart (if applicable)

import 'package:releva_sdk/types/cart/cart.dart';
import 'package:releva_sdk/types/cart/cart_product.dart';
import 'package:releva_sdk/types/custom_field/custom_field.dart';
import 'package:releva_sdk/types/custom_field/custom_fields.dart';

// Create custom fields to pass to your cart product
CustomField<String> string = CustomField('size', ['S']);
CustomField<double> numeric = CustomField('size_code', [1, 2]);
CustomField<DateTime> date = CustomField('in_promo_after', [
  DateTime.utc(2025, 1, 1, 0, 1, 2, 5)
]);
CustomFields custom = CustomFields([string], [numeric], [date]);

// If your app supports a cart feature, set the active cart
// If the user has no cart right now, set it to Cart.active with an empty list
CartProduct product = CartProduct('<id>', 29.99, 1, custom);
await client.setCart(Cart.active([product]));

Initialize and Send a Request - full low-level example

import 'package:releva_sdk/types/push_request.dart';
import 'package:releva_sdk/types/view/viewed_product.dart';
import 'package:releva_sdk/types/event/custom_event.dart';
import 'package:releva_sdk/types/event/custom_event_product.dart';
import 'package:releva_sdk/types/filter/nested_filter.dart';

// Initialize a request
PushRequest request = PushRequest()
  // If a user is viewing a screen with non-default language
  .locale('en')
  // If a user is viewing a screen with non-default currency
  .currency('EUR')
  // If the screen the user is viewing contains a list of items with a filter applied
  .pageFilter(NestedFilter.and([]))
  // If a user is viewing a specific screen, send the screen (a.k.a. page) token
  .screenView('<pageToken>')
  // If a user is viewing a product, send the viewed product
  .productView(Viewedproduct('<productId>', CustomFields.empty()))
  // If a user has performed custom events (e.g., filling out a form)
  .customEvents([
    CustomEvent(
      'fubar',
      [CustomEventProduct('<productId>', 2)],
      ['foo_tag'],
      CustomFields.empty(),
    )
  ]);

// Send the request
RelevaResponse response = await client.push(request);

// Handle recommendations
if (response.hasRecommenders) {
  for (final recommender in response.recommenders) {
    print('${recommender.name}: ${recommender.response.length} products');
    // Display products to user
  }
}

HIgh-level Tracking Methods

Product View Tracking

final response = await client.trackProductView(
  screenToken: 'product_detail',
  productId: 'product-123',
  categories: ['electronics', 'phones'],
  locale: 'en',
  currency: 'USD',
);

Search Tracking

import 'package:releva_sdk/types/filter/simple_filter.dart';
import 'package:releva_sdk/types/filter/nested_filter.dart';

final response = await client.trackSearchView(
  screenToken: 'search_results',
  query: 'red running shoes',
  resultProductIds: ['prod1', 'prod2', 'prod3'],
  filter: NestedFilter.and([
    SimpleFilter.priceRange(minPrice: 50, maxPrice: 200),
    SimpleFilter.brand(brand: 'Nike'),
    SimpleFilter.color(color: 'red'),
  ]),
  locale: 'en',
  currency: 'USD',
);

Checkout Success Tracking

final response = await client.trackCheckoutSuccess(
  screenToken: 'checkout_success',
  orderedCart: Cart.active([...]),
  userEmail: 'user@example.com',
  userPhoneNumber: '+1234567890',
  userFirstName: 'John',
  userLastName: 'Doe',
  locale: 'en',
  currency: 'USD',
);

Screen View Tracking

final response = await client.trackScreenView(
  screenToken: 'home_screen',
  productIds: ['prod1', 'prod2', 'prod3'],
  categories: ['electronics', 'phones'],
  locale: 'en',
  currency: 'USD',
);

Advanced Filtering

Build complex product filters with nested AND/OR logic:

final complexFilter = NestedFilter.and([
  // Price range
  SimpleFilter.priceRange(minPrice: 10, maxPrice: 100),

  // Multiple brands (OR)
  NestedFilter.or([
    SimpleFilter.brand(brand: 'Nike'),
    SimpleFilter.brand(brand: 'Adidas'),
  ]),

  // Size AND color
  NestedFilter.and([
    NestedFilter.or([
      SimpleFilter.size(size: '42'),
      SimpleFilter.size(size: '43'),
    ]),
    SimpleFilter.color(color: 'red'),
  ]),
]);

final response = await client.trackSearchView(
  screenToken: 'search_results',
  query: 'shoes',
  filter: complexFilter,
);

Banners

Banners are dynamic content overlays (popup modals or bars) that can be displayed based on user behavior and configured triggers. The SDK automatically handles banner display, positioning, and tracking.

Using BannerDisplayWidget

Wrap your screen content with BannerDisplayWidget to enable banner display on that screen:

import 'package:releva_sdk/widgets/banner_display_widget.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final ScrollController _scrollController = ScrollController();

  @override
  Widget build(BuildContext context) {
    final client = RelevaManager.instance.client;

    return BannerDisplayWidget(
      key: const ValueKey('home-banner-display'),  // Important: Use a unique key
      targetSelector: "#home-content",
      client: client,
      child: Scaffold(
        body: ListView(
          controller: _scrollController,
          children: [
            // Your screen content
          ],
        ),
      ),
    );
  }
}

Important: Banners are reset when you navigate back to a screen

When you call trackScreenView() on a screen (which happens automatically with RouteAware or when you manually track the screen), banners for that screen are reset and will be shown again based on their trigger conditions. This ensures users see relevant banners each time they visit a screen.

Example flow:

  1. User views Home screen β†’ Banner shows (trigger: immediately)
  2. User navigates to Product screen
  3. User returns to Home screen β†’ Banner shows again (reset on navigation back)

Why this matters:

  • Banners will re-display when users navigate back to a screen
  • Each screen visit is treated as a fresh opportunity to show banners
  • This behavior is intentional and matches web SDK behavior

Banners can be configured with different triggers in the Releva dashboard:

  • immediately: Shows as soon as the screen loads
  • delaySeconds: Shows after a specified delay (e.g., 5 seconds)
  • scrollPercentage: Shows when user scrolls to a certain percentage (requires ScrollController)
  • cartChanged: Shows when cart is modified
  • wishlistChanged: Shows when wishlist is modified
  • leaveIntent: Not supported on mobile (web-only feature)

Scroll-based Triggers

For scroll-based banner triggers, you need to provide a ScrollController:

class _HomeScreenState extends State<HomeScreen> {
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    // Set scroll controller for banner positioning
    RelevaManager.instance.setScrollController(_scrollController);
  }

  @override
  Widget build(BuildContext context) {
    return BannerDisplayWidget(
      // ... same as above
      child: ListView(
        controller: _scrollController,  // Pass to your scrollable widget
        children: [...],
      ),
    );
  }
}

The SDK supports two banner display types:

  1. Popup Banners: Modal overlays with backdrop, close button, and customizable styling
  2. Bar Banners: Fixed position bars (top/bottom) with configurable height and styling

All banner styling, positioning, and content are configured in the Releva dashboard.

Best Practices

  1. Use unique ValueKey: Always provide a unique ValueKey to each BannerDisplayWidget to maintain proper state
  2. One BannerDisplayWidget per screen: Each screen should have its own BannerDisplayWidget wrapper
  3. Track screen views: Ensure you call trackScreenView() when the screen is displayed (use RouteAware pattern)
  4. Provide ScrollController: If using scroll-triggered banners, ensure you pass the ScrollController to both the widget and RelevaManager

Automatic Tracking

The SDK automatically tracks:

  • Banner impressions (when banner is displayed)
  • Banner taps (when user clicks on banner)
  • Banner dismissals (when user closes banner or taps outside)

All tracking happens transparently - you don't need to implement any tracking logic.

Expected Push Notification Payload

Releva sends push notifications with the following data-only payload structure:

{
  "data": {
    "click_action": "RELEVA_NOTIFICATION_CLICK",
    "title": "Special Offer!",
    "body": "Get 20% off your next purchase",
    "imageUrl": "https://example.com/image.jpg",
    "button": "Shop Now",
    "target": "screen",
    "navigate_to_screen": "/product/123",
    "callbackUrl": "https://api.releva.ai/track/..."
  }
}

The SDK automatically:

  • Displays rich notifications with images and action buttons
  • Navigates to the specified screen when tapped
  • Tracks engagement metrics (delivered, opened, dismissed)

Configuration Options

// Full functionality (default)
RelevaClient('', 'token');

// Custom configuration
RelevaClient('', 'token', config: RelevaConfig(
  enableTracking: true,
  enableScreenTracking: true,     // Automatic screen tracking
  enablePushNotifications: true,
));

API Reference

RelevaClient Methods

Configuration

  • Future<void> setDeviceId(String deviceId) - Set unique device identifier
  • Future<void> setProfileId(String profileId) - Set user profile identifier
  • Future<void> setCart(Cart cart) - Update user's cart
  • Future<void> setWishlist(List<WishlistProduct> products) - Update user's wishlist
  • Future<void> enablePushEngagementTracking({Function(NotificationResponse)? onNotificationTapped}) - Enable push notification tracking with optional tap callback
  • Future<void> registerPushToken(DeviceType type, String token) - Register FCM token
  • ScreenTrackingService createScreenTrackingService() - Create screen tracking observer

Tracking

  • Future<RelevaResponse> push(PushRequest request) - Send custom tracking request
  • Future<RelevaResponse> trackScreenView({required String screenToken, ...}) - Track screen views
  • Future<RelevaResponse> trackProductView({required String screenToken, required String productId, ...}) - Track product views
  • Future<RelevaResponse> trackSearchView({required String screenToken, ...}) - Track search queries
  • Future<RelevaResponse> trackCheckoutSuccess({required String screenToken, required Cart orderedCart, ...}) - Track successful purchases

Push Notifications

  • Future<void> trackEngagement(RemoteMessage message) - Track notification engagement
  • bool isRelevaMessage(RemoteMessage message) - Check if notification is from Releva

Response Models

RelevaResponse

class RelevaResponse {
  final List<RecommenderResponse> recommenders;
  final List<BannerResponse> banners;

  bool get hasRecommenders;
  bool get hasBanners;
  RecommenderResponse? getRecommenderByToken(String token);
}

ProductRecommendation

class ProductRecommendation {
  final String id;
  final String name;
  final double price;
  final bool available;
  final String? imageUrl;
  final double? discountPrice;
  final List<String>? categories;
}

Additional Information

For additional information, please visit https://releva.ai or contact tech-support@releva.ai