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.

Libraries

datra_engage_sdk