Datra Engage SDK for Flutter
datra_engage_sdk resolves Datra in-app messages and sends delivery events from Flutter applications. Version 0.1.1 aligns resolve reliability with the React Native SDK and preserves a shared request when a single widget is cancelled.
The SDK does not depend on Riverpod or go_router, does not render marketing UI, and does not control navigation. Your app owns presentation and action handling.
Installation
During local development:
dependencies:
datra_engage_sdk:
path: ../datra-engage-flutter-sdk
After the first public release:
dependencies:
datra_engage_sdk: ^0.1.1
Then import the package:
import 'package:datra_engage_sdk/datra_engage_sdk.dart';
Configure the client
Create one client and keep it for the application lifetime. A Datra public SDK key may be embedded in a mobile app. Never place an integration token, admin token, private API key, or other secret in the app.
final engage = DatraEngage(
const DatraEngageConfig(
baseUrl: 'https://api.example.com',
publicKey: 'pk_live_your_public_key',
customerExternalId: 'customer_123',
platform: 'flutter',
appVersion: '1.0.0',
locale: 'uz',
defaultContext: {
'branchExternalId': 'branch_1',
},
),
);
Update identity after login or logout:
engage.identify(
const DatraEngageIdentity(
customerExternalId: 'customer_456',
context: {'segment': 'regular'},
),
);
engage.setCustomerExternalId(); // clear customer after logout
Changing the customer cancels obsolete in-flight resolve requests.
Flutter provider and banner placement
Wrap the relevant subtree. flushEventQueueOnResume is optional.
DatraEngageProvider(
client: engage,
flushEventQueueOnResume: true,
child: const MyApp(),
)
Resolve a banner for a placement and render it with your own design:
DatraEngageBuilder(
screen: 'home',
placement: 'home_top_banner',
enabled: customerId != null,
customerExternalId: customerId,
engageContext: {
'cartTotal': cart.total,
'productIds': cart.productIds,
},
trackShown: true,
builder: (context, state) {
if (state.isLoading && state.messages.isEmpty) {
return const SizedBox.shrink();
}
if (state.messages.isEmpty) {
return const SizedBox.shrink();
}
final message = state.messages.first;
return OfferBanner(
title: message.title,
body: message.body,
imageUrl: message.imageUrl,
onTap: () async {
await engage.messageClickedSafe(message.decisionId);
await handleEngageAction(message.action, message);
},
onDismiss: () => engage.messageDismissedSafe(message.decisionId),
);
},
)
DatraEngageMessagesState exposes messages, response, isLoading, error, and refresh. Request failures stay inside the state and never throw from the widget lifecycle.
Resolve messages without widgets
final response = await engage.getMessages(
const ResolveMessagesInput(
placement: 'cart_product_reward',
screen: 'cart',
context: {
'cartProductIds': ['fries', 'burger'],
'loyaltyPoints': 20,
},
),
);
Use getMessagesSafe or screenViewedSafe when Engage must fail open. They return an empty message list on network, timeout, cancellation, or parsing errors.
Tracking
await engage.messageShownSafe(message.decisionId);
await engage.messageClickedSafe(message.decisionId);
await engage.messageDismissedSafe(message.decisionId);
await engage.messageConvertedSafe(
message.decisionId,
const TrackEventOptions(metadata: {'orderExternalId': 'order_123'}),
);
Strict variants (messageShown, messageClicked, messageDismissed, messageConverted, and trackEvent) throw a DatraEngageError. Safe variants return SafeTrackEventResult and do not interrupt application flows.
Default idempotency keys match the React Native SDK:
datra:shown:<decisionId>
datra:clicked:<decisionId>
datra:dismissed:<decisionId>
datra:converted:<decisionId>
Action handling
The SDK only parses and dispatches actions. The application decides how to navigate.
final handleEngageAction = createDatraEngageActionHandler(
DatraEngageActionHandlers(
onOpenScreen: (screen, action, message) {
router.goNamed(screen);
},
onOpenPromotion: (promotionId, action, message) {
router.push('/promotions/$promotionId');
},
onOpenProduct: (productId, action, message) {
router.push('/products/$productId');
},
onOpenCart: (action, message) {
router.go('/cart');
},
onOpenUrl: (url, action, message) {
openExternalUrl(url);
},
onUnhandledAction: (action, message) {
diagnostics.log('Unsupported Engage action: ${action.type}');
},
),
);
Typed handlers are available for OPEN_SCREEN, OPEN_PROMOTION, OPEN_COUPON, OPEN_PRODUCT, OPEN_CART, OPEN_URL, OPEN_STORY, and OPEN_MECHANIC. Unknown or incomplete actions go to onUnhandledAction.
Offline event queue
The queue is opt-in. It stores retryable event failures only, keeps at most 100 events by default, and flushes with concurrency 3.
final storage = MemoryDatraEngageStorage();
final engage = DatraEngage(
DatraEngageConfig(
baseUrl: 'https://api.example.com',
publicKey: 'pk_live_your_public_key',
eventQueue: DatraEngageEventQueueConfig(
enabled: true,
storage: storage,
),
),
);
await engage.flushEventQueue();
Memory storage is the safe default but does not survive process restarts. To persist the queue and dedupe keys, implement DatraEngageStorage in the app with shared_preferences:
class SharedPreferencesEngageStorage implements DatraEngageStorage {
SharedPreferencesEngageStorage(this.preferences, {this.prefix = ''});
final SharedPreferences preferences;
final String prefix;
@override
Future<String?> getItem(String key) async =>
preferences.getString('$prefix$key');
@override
Future<void> setItem(String key, String value) async {
await preferences.setString('$prefix$key', value);
}
@override
Future<void> removeItem(String key) async {
await preferences.remove('$prefix$key');
}
}
The SDK intentionally does not require shared_preferences; the host app chooses its persistence implementation.
Runtime defaults
| Operation | Timeout | Retries |
|---|---|---|
| Message resolve | 2000 ms | 0 |
| Event tracking | 3000 ms | 1 |
Retry statuses are 408, 429, 500, 502, 503, and 504. Event dedupe is enabled by default with a 24-hour TTL and a maximum of 500 keys. Identical resolves use single-flight; a changed request for the same customer, placement, and screen cancels the stale request.
Discounts, rewards, and loyalty points
Datra Engage tells the app which offer to show and carries the action/offer metadata. The SDK does not calculate or apply a discount, validate promotion eligibility, reserve a reward product, or debit loyalty points. Those operations must be confirmed by the Datra backend/order calculation endpoint. Never grant a discount or spend points based only on client-side message data.
Development checks
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test
dart doc
dart pub publish --dry-run
Publishing to pub.dev is a separate manual release step.