releva_sdk 0.0.48
releva_sdk: ^0.0.48 copied to clipboard
Releva.ai SDK for Flutter apps
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, stories, 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
📬 App Inbox #
- Persistent Messages: In-app message centre where messages survive until read, deleted, or expired
- Rich Content: Messages rendered with Unlayer design JSON, fully personalized per user
- Real-Time Sync: Silent push notifications trigger automatic inbox refresh
- Optimistic Updates: Mark read, mark all read, and delete with instant UI feedback
- Cursor Pagination: Efficient infinite scroll with server-side cursor pagination
- Local Caching: Messages cached locally for instant display on return visits
⚙️ 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.42
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:
- In Xcode, go to File → New → Target → Notification Service Extension
- Name it
RelevaNotificationService - Add Firebase/Messaging to the extension in your Podfile
- Replace the generated
NotificationService.swiftwith the implementation fromreleva_sdk/ios/Templates/NotificationService.swift - Ensure your notification payload includes
mutable-content: 1in theapssection
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),
);
}
}
Profile Management and User Logout #
Important: Handling User Logout #
When a user logs out of your application, it's critical to prevent merging the logged-in user's profile with the anonymous profile created for the logged-out state. To do this, use the skipMergeWithPreviousProfileId parameter when setting the new anonymous profile ID:
// When user logs out, generate a new anonymous profile ID
final newAnonymousProfileId = const Uuid().v4();
// Set the new profile ID with skipMergeWithPreviousProfileId set to true
await client.setProfileId(newAnonymousProfileId, true);
// Registering the firebase push token to the new profile id because we are skipping the merge with previous one and we need explicit call for registering it to the new profile id
RelevaClient rlv_client = RelevaClient(realm, accessToken);
DeviceType deviceType = Platform.isIOS
? DeviceType.ios
: DeviceType.android;
final messaging = FirebaseMessaging.instance;
// Now get FCM token (safe to call after APNs is ready on iOS)
String? firebaseToken = await messaging.getToken();
if (firebaseToken != null) {
await rlv_client!.registerPushToken(deviceType, firebaseToken);
}
Why is this important?
By default, when you change a profile ID, the SDK will merge the previous profile with the new one to maintain user behavior continuity. However, when a user logs out, you want to start fresh with a completely separate anonymous profile. Setting skipMergeWithPreviousProfileId: true ensures:
- The logged-in user's profile data remains separate
- The new anonymous profile starts with a clean slate
- No cross-contamination of user behavior data
Default Behavior (when logging in or switching users):
// Normal profile change - previous profile will be merged
await client.setProfileId(loggedInUserId);
This default behavior is ideal for:
- User login (merging anonymous behavior with logged-in profile)
- Account linking
- Profile migrations
Push Notification Engagement Tracking #
OPTION 1: Use the library's built-in callback registration (Recommended) #
IMPORTANT: Only do this if you DO NOT have ANY of the following anywhere in your app OR YOUR OTHER LIBRARIES:
FirebaseMessaging.instance.onMessage.listen((message) => ...);FirebaseMessaging.instance.onMessageOpenedApp.listen((message) => ...);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 withUri.splitQueryString())
Available Fields in Parsed Payload (data):
click_action- Always"RELEVA_NOTIFICATION_CLICK"for Releva messagesnavigate_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 parameterstarget- Navigation type:"screen"or"url"title- Notification titlebody- Notification body textimageUrl- 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, andnavigate_to_screenfields - ✅ 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
],
),
),
);
}
}
Banner Lifecycle and Behavior #
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:
- User views Home screen → Banner shows (trigger: immediately)
- User navigates to Product screen
- 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
Banner Triggers #
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: [...],
),
);
}
}
Banner Types #
The SDK supports two banner display types:
- Popup Banners: Modal overlays with backdrop, close button, and customizable styling
- 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 #
- Use unique ValueKey: Always provide a unique
ValueKeyto eachBannerDisplayWidgetto maintain proper state - One BannerDisplayWidget per screen: Each screen should have its own
BannerDisplayWidgetwrapper - Track screen views: Ensure you call
trackScreenView()when the screen is displayed (use RouteAware pattern) - 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.
Stories #
Stories are full-screen, multi-slide content experiences similar to Instagram or Facebook stories. They support auto-advance timers, progress indicators, tap/swipe navigation, and configurable end behavior (dismiss, loop, or stay on last slide). Like banners, stories are configured in the Releva dashboard and delivered as part of the push response.
Setup #
Stories require three things:
-
StoryDisplayWidget— wrap your screen content with this widget. It listens for story display events and opens the full-screen story viewer. -
StoryManagerService— processes stories from push responses and evaluates trigger conditions. You create an instance and callinitialize()after each push response. -
Push response wiring — after each push, pass
response.storiesto the story manager so triggers are evaluated.
import 'package:releva_sdk/widgets/story_display_widget.dart';
import 'package:releva_sdk/services/story_manager_service.dart';
// Create a StoryManagerService instance (e.g. in your app manager/service)
final storyManager = StoryManagerService();
// After each push response, initialize stories:
final response = await client.push(request);
if (response.hasStories) {
storyManager.initialize(response.stories, scrollController, client);
}
Then wrap your screen content with StoryDisplayWidget:
class HomeScreen extends StatelessWidget {
final RelevaClient client;
const HomeScreen({super.key, required this.client});
@override
Widget build(BuildContext context) {
return StoryDisplayWidget(
client: client,
onLinkTap: (url) {
// Handle link taps from within story slides
launchUrl(Uri.parse(url));
},
child: Scaffold(
body: ListView(
children: [
// Your screen content
],
),
),
);
}
}
How Stories Are Triggered #
Stories share the same trigger system as banners. When you call trackScreenView() (or any push method), the server response may include stories. Pass them to StoryManagerService.initialize() to evaluate triggers:
- immediately: Story opens as soon as the response is processed
- delaySeconds: Story opens after the configured delay
- scrollPercentage: Story opens when the user scrolls past the threshold (requires
ScrollController— see Banners section) - cartChanged / wishlistChanged: Story opens when cart or wishlist is modified
Story Viewer Behavior #
The story viewer is a full-screen overlay that displays slides sequentially:
- Progress indicators at the top show the current position and auto-advance timer
- Tap left half of the screen to go to the previous slide
- Tap right half to go to the next slide
- Swipe left/right to navigate between slides
- Close button (X) in the top-right corner dismisses the story
- Each slide's background color is read from the Unlayer design JSON
When multiple stories are triggered simultaneously, they are queued and shown one at a time.
End Behavior #
Each story has a configurable end behavior (set in the Releva dashboard):
- dismiss (default): Story closes automatically after the last slide
- loop: Story restarts from the first slide
- stayOnLast: Story stays on the last slide until the user closes it
Link Handling #
Story slides can contain interactive elements (buttons, links) created in the Unlayer editor. When a user taps one of these elements, the onLinkTap callback you provided to StoryDisplayWidget is called with the URL:
StoryDisplayWidget(
client: client,
onLinkTap: (url) {
// Navigate to a screen, open a browser, or handle deep links
launchUrl(Uri.parse(url));
},
child: ...,
);
Automatic Tracking #
The SDK automatically tracks all story engagement events:
- storyImpression — when the story viewer opens
- storySlideView — when each slide is displayed
- storySlideClick — when the user taps a link or button within a slide
- storyComplete — when the last slide is reached
- storyClose — when the user dismisses the story
All tracking happens transparently — no manual tracking code is needed.
App Inbox #
App Inbox is a persistent, in-app message centre. Unlike push notifications, inbox messages survive until the user reads or deletes them (or they expire). Messages are delivered server-side with content already personalized — the SDK receives ready-to-render data.
Initialize the Inbox #
Call initializeInbox() after setting the profile ID. The inbox is gated behind the enableInbox config flag (enabled by default in RelevaConfig.full()).
await client.setProfileId('<profileId>');
await client.initializeInbox();
Access Inbox State #
The InboxService is a ChangeNotifier. Listen to it for reactive UI updates:
final inbox = client.inbox;
// Current state
print(inbox.state.messages); // List<InboxMessage>
print(inbox.state.unreadCount); // int
print(inbox.state.isLoading); // bool
print(inbox.state.hasMore); // bool (more pages available)
// Listen for changes
inbox.addListener(() {
setState(() {}); // or use ListenableBuilder
});
Refresh and Pagination #
// Pull-to-refresh: fetch first page + unread count
await client.inbox.refresh();
// Infinite scroll: load next page
await client.inbox.loadMore();
// Refresh only if cache is stale (> 5 minutes)
await client.inbox.refreshIfStale();
Mark as Read and Delete #
All mutations use optimistic updates — the UI updates instantly, and reverts on API error.
// Mark a single message as read
await client.inbox.markAsRead(message.id);
// Mark all messages as read
await client.inbox.markAllAsRead();
// Delete a message
await client.inbox.deleteMessage(message.id);
Track Message Actions #
Call trackAction() when a user taps an interactive element inside a message (e.g. a button or link). This records an analytics event but does not mark the message as read.
await client.inbox.trackAction(message.id);
Render Message Content #
Use InboxMessageWidget to render the message body. It wraps the SDK's DesignRenderer and automatically tracks actions on link taps:
import 'package:releva_sdk/widgets/inbox_message_widget.dart';
InboxMessageWidget(
message: inboxMessage,
onLinkTap: (url) {
// Handle URL (e.g. open in browser or deep link)
launchUrl(Uri.parse(url));
},
);
Silent Push Sync #
When new messages are delivered, the backend sends a silent FCM push with magellan_notification_type: "inbox_sync". The SDK automatically handles this — it refreshes the inbox without showing a visible notification. No additional setup is required beyond enablePushEngagementTracking().
The inbox also refreshes automatically when the app returns to the foreground, as a reliable fallback for cases where silent push delivery is not guaranteed.
InboxMessage Data Model #
| Field | Type | Description |
|---|---|---|
id |
String (UUID) |
Unique ID of this delivery. Use in all read/delete/action calls. |
title |
String |
Resolved message title. |
design |
Map<String, dynamic> |
Unlayer design JSON, ready to render. |
read |
bool |
Whether the user has read this message. |
createdAt |
DateTime |
When the message was delivered. Messages are sorted newest-first. |
inboxMessageId |
int |
ID of the source message template. |
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,
enableInbox: true, // App Inbox
));
API Reference #
RelevaClient Methods #
Configuration
Future<void> setDeviceId(String deviceId)- Set unique device identifierFuture<void> setProfileId(String profileId, [bool skipMergeWithPreviousProfileId = false])- Set user profile identifier. Passtrueas second parameter when logging out to prevent merging logged-in profile with anonymous profileFuture<void> setCart(Cart cart)- Update user's cartFuture<void> setWishlist(List<WishlistProduct> products)- Update user's wishlistFuture<void> enablePushEngagementTracking({Function(NotificationResponse)? onNotificationTapped})- Enable push notification tracking with optional tap callbackFuture<void> registerPushToken(DeviceType type, String token)- Register FCM tokenScreenTrackingService createScreenTrackingService()- Create screen tracking observerInboxService get inbox- Access the inbox serviceFuture<void> initializeInbox()- Initialize inbox (call after setProfileId)
Tracking
Future<RelevaResponse> push(PushRequest request)- Send custom tracking requestFuture<RelevaResponse> trackScreenView({required String screenToken, ...})- Track screen viewsFuture<RelevaResponse> trackProductView({required String screenToken, required String productId, ...})- Track product viewsFuture<RelevaResponse> trackSearchView({required String screenToken, ...})- Track search queriesFuture<RelevaResponse> trackCheckoutSuccess({required String screenToken, required Cart orderedCart, ...})- Track successful purchases
Push Notifications
Future<void> trackEngagement(RemoteMessage message)- Track notification engagementbool 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;
}
InboxMessage
class InboxMessage {
final String id;
final String title;
final Map<String, dynamic> design;
bool read;
final DateTime createdAt;
final int inboxMessageId;
}
InboxService Methods
Future<void> refresh()- Fetch first page + unread countFuture<void> loadMore()- Fetch next page (cursor pagination)Future<void> markAsRead(String messageId)- Mark single message as readFuture<void> markAllAsRead()- Mark all messages as readFuture<void> deleteMessage(String messageId)- Delete a messageFuture<void> trackAction(String messageId)- Track tap/click on message contentFuture<void> refreshIfStale()- Refresh only if cache > 5 minutes oldInboxState get state- Current inbox state (messages, unreadCount, isLoading, hasMore)
Additional Information #
For additional information, please visit https://releva.ai or contact tech-support@releva.ai