datra_engage_sdk 0.1.1
datra_engage_sdk: ^0.1.1 copied to clipboard
Flutter SDK for resolving and tracking Datra Engage in-app messages.
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.