Flutter v1.0.0.3

Specifications

SpecValue
Flutter>= 3.3.0
Dart SDK>= 3.5.0 < 4.0.0
Android Min SDK24 (Android 7.0)
iOS Min13.0
Method Channelflutter_tv_behavioral_plugin
Bundled Android nativebehaviors 1.0.0.3
Bundled iOS nativeBehaviorSDK 1.0.3

The Flutter plugin wraps the native Android and iOS SDKs via platform method channels.

Installation

1. Add the SDK dependency

Add the plugin to your pubspec.yaml:

YAML
dependencies:
  flutter_tv_behavioral_plugin:
    path: ./flutter_tv_behavioral_plugin # or your local path

Then run:

Shell
flutter pub get

2. Android setup

In your app-level build.gradle:

Groovy
android {
    compileSdk 36
    defaultConfig {
        minSdk 24
    }
}

Add the local Maven repository for the native SDK in settings.gradle.kts:

Kotlin
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("${rootProject.projectDir}/repo")
        }
    }
}

3. iOS setup

In your Podfile:

Ruby
platform :ios, '13.0'

Then run:

Shell
cd ios && pod install

4. Permissions (iOS only)

Add to Info.plist if you enable Bluetooth or Location collection:

XML
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Used for device behavioral analysis</string>

<key>NSLocationWhenInUseUsageDescription</key>
<string>Used for device behavioral analysis</string>

Configuration

Dart
import 'package:flutter_tv_behavioral_plugin/flutter_tv_behavioral_plugin.dart';
import 'package:flutter_tv_behavioral_plugin/data/ts_behavioral_config.dart';

final config = TSBehavioralConfig(
  appId: 'your-app-id',                   // Your app's package name or bundle identifier
  baseUrl: 'https://your-base-url/api',   // Provided by TrustVision
  metadataConfig: const MetaDataCollectionConfig(
    intervalMillis: 2000,
    canAskPermission: false,
    enabled: true,
  ),
  uploadBlockSize: 10,
  sensorConfig: const SensorConfig(
    samplingIntervalMs: 20,
    sensorTypes: [
      SensorType.gyroscope,
      SensorType.magnetometer,
      SensorType.accelerometer,
      SensorType.light,
    ],
    enabled: true,
  ),
  keyboardConfig: const KeyboardConfig(
    trackRawText: false,
    enabled: true,
  ),
  touchConfig: const TouchConfig(
    enabled: true,
  ),
  screenTransitionBlockSize: 1,
  appStateBlockSize: 1,
  retryConfig: RetryConfig(
    enabled: true,
    maxRetries: 2,
    retryDelayMs: 1000,
    maxDelayMs: 5000,
    maxRetryAttempts: 3,
    maxRetryStorageItems: 50,
    finalFlushTimeoutSeconds: 10.0,
    retryItemDelaySeconds: 0.5,
  ),
);

TSBehavioralConfig

FieldTypeRequiredDefaultDescription
appIdStringYes—Your app's package name or bundle identifier
baseUrlStringYes—Base URL for TrustVision API
metadataConfigMetaDataCollectionConfigYes—Device metadata collection settings
uploadBlockSizeintNo10Number of batch entries before auto-upload
sensorConfigSensorConfigNoSensorConfig()Sensor data collection settings
keyboardConfigKeyboardConfigNoKeyboardConfig()Keyboard tracking settings
touchConfigTouchConfigNoTouchConfig()Touch tracking settings
screenTransitionBlockSizeintNo1Screen transition batch size
appStateBlockSizeintNo1Number of app-state events per upload batch
batchExtraMap<String, String>?NonullCustom key-value metadata in every batch upload
retryConfigRetryConfig?NoRetryConfig()Upload retry queue settings
sslCertificatesList<TSCertificate>?NonullCertificates to pin. See SSL Pinning

SSL Pinning

Optional certificate pinning. Pass pinned certificates via sslCertificates; when the list is non-empty the SDK trusts only servers presenting one of these certificates on every API call. null or an empty list disables pinning and falls back to the system trust store. Each TSCertificate references a certificate file bundled with the native platform (Android raw resource / iOS bundle resource) by name and type:

Dart
final config = TSBehavioralConfig(
  appId: 'com.example.app',
  baseUrl: 'https://api.trustingsocial.com',
  metadataConfig: const MetaDataCollectionConfig(intervalMillis: 2000),
  sslCertificates: const [
    TSCertificate(name: 'my_server_cert', type: 'der'),
  ],
);

MetaDataCollectionConfig

FieldTypeRequiredDefaultDescription
intervalMillisintYes—Metadata collection interval in milliseconds
canAskPermissionboolNofalseWhether SDK may request system permissions — iOS only (Bluetooth, Location)
enabledboolNotrueEnable/disable metadata collection

SensorConfig

FieldTypeRequiredDefaultDescription
samplingIntervalMsintNo20 (50Hz)Sensor sampling interval in milliseconds
sensorTypesList<SensorType>NoAll 4 typesSensors to collect: gyroscope, accelerometer, magnetometer, light
enabledboolNotrueEnable/disable sensor collection

KeyboardConfig

FieldTypeRequiredDefaultDescription
trackRawTextboolNofalseWhether to collect raw typed text. When false, only character type is recorded
enabledboolNotrueEnable/disable keyboard tracking
blockSizeintNo1Number of keyboard events per upload batch

TouchConfig

FieldTypeRequiredDefaultDescription
enabledboolNotrueEnable/disable touch tracking

RetryConfig

FieldTypeRequiredDefaultDescription
enabledboolNotrueEnable/disable the retry queue
maxRetriesintNo2Max immediate retries per request (0–10). Total calls = value + 1
retryDelayMsintNo1000Base delay before first retry in milliseconds (> 0)
maxDelayMsintNo5000Maximum delay cap in milliseconds (≥ retryDelayMs)
maxRetryAttemptsintNo3Maximum retry-queue attempts before dropping a failed batch (≥ 1)
maxRetryStorageItemsintNo50Maximum items in retry storage before FIFO eviction (≥ 1)
finalFlushTimeoutSecondsdoubleNo10.0Timeout in seconds for flush at session end (≥ 1)
retryItemDelaySecondsdoubleNo0.5Delay between retry uploads in seconds (≥ 0)

SDK Functions

FlutterTvBehavioralPlugin is the main plugin class. Create an instance and call methods on it. All methods return Future and should be awaited.

initialize

Initializes the SDK. Must be called first before any other method. Generates device identity on the native platform and returns device information needed for registration.

Signature:

Dart
Future<TSDeviceInitInfo> initialize(TSBehavioralConfig config)

Parameters:

ParameterTypeRequiredDescription
configTSBehavioralConfigYesSDK configuration

Returns: TSDeviceInitInfo

FieldTypeDescription
deviceIdStringUnique device identifier (UUID)
bootstrapKeyIdStringKey pair ID for encryption
isRegisteredboolWhether device was previously registered
encryptedDevicePublicKeyPayloadStringAES-GCM sealed device public key (base64). Pass to device_public_key in register API
encryptedSecretKeyStringRSA-OAEP encrypted AES secret key (base64). Pass to encrypted_secret_key in register API

Throws: PlatformException with code TSSessionError

Example:

Dart
final plugin = FlutterTvBehavioralPlugin();

try {
  final initInfo = await plugin.initialize(config);

  debugPrint('Device ID: ${initInfo.deviceId}');
  debugPrint('Already registered: ${initInfo.isRegistered}');
} on PlatformException catch (e) {
  debugPrint('Init error [${e.code}]: ${e.message}');
}

markDeviceRegistered

Marks the device as registered after your backend has completed server-side registration with TrustVision. Must be called after initialize() and before startCollect().

Registration status is persisted locally. On subsequent launches, initInfo.isRegistered will return true and you can skip the registration step.

Signature:

Dart
Future<bool> markDeviceRegistered({
  required bool isRegistered,
  required String customerUserDeviceId,
})

Parameters:

ParameterTypeRequiredDescription
isRegisteredboolYesRegistration status (true to mark as registered)
customerUserDeviceIdStringYesUnique user-device identifier returned by your server

Returns: Future<bool> — true on success, false on failure (errors are logged, not thrown)

Example:

Dart
final initInfo = await plugin.initialize(config);

if (!initInfo.isRegistered) {
  // Your backend calls TrustVision API: POST /v1/register_device
  // with: device_id, key_pair_id, device_public_key, encrypted_secret_key, cus_user_id, protocol
  final userDeviceId = await yourBackend.registerDevice(
    deviceId: initInfo.deviceId,
    keyPairId: initInfo.bootstrapKeyId,
    encryptedDevicePublicKeyPayload: initInfo.encryptedDevicePublicKeyPayload,
    encryptedSecretKey: initInfo.encryptedSecretKey,
    cusUserId: 'your-user-id',
  );

  await plugin.markDeviceRegistered(
    isRegistered: true,
    customerUserDeviceId: userDeviceId,
  );
}

startCollect

Creates a challenge with the TrustVision server and starts all data collectors (sensors, metadata, touch, keyboard). Requires device to be registered.

Signature:

Dart
Future<bool> startCollect()

Returns: Future<bool> — true on success, false on failure (errors are logged, not thrown)

Example:

Dart
try {
  await plugin.startCollect();
  debugPrint('Collection started');
} on PlatformException catch (e) {
  debugPrint('Start error [${e.code}]: ${e.message}');
}

updateScreenTransition (Required)

Important: You must call updateScreenTransition() immediately after startCollect() and whenever the user navigates to a new screen. This is required for behavioral data to be correctly associated with screen context.

Records a screen transition event. Silently no-ops if SDK is not initialized or session is not started.

Signature:

Dart
Future<void> updateScreenTransition(String screenName)

Parameters:

ParameterTypeRequiredDescription
screenNameStringYesName of the screen being navigated to

Example — call immediately after startCollect:

Dart
await plugin.startCollect();
await plugin.updateScreenTransition('main_screen');

// Then call again on every screen navigation
await plugin.updateScreenTransition('payment_screen');

Example — with Navigator observer:

Dart
class BehavioralNavigatorObserver extends NavigatorObserver {
  final FlutterTvBehavioralPlugin plugin;

  BehavioralNavigatorObserver(this.plugin);

  @override
  void didPush(Route route, Route? previousRoute) {
    super.didPush(route, previousRoute);
    final name = route.settings.name;
    if (name != null) {
      plugin.updateScreenTransition(name);
    }
  }
}

// In your MaterialApp:
MaterialApp(
  navigatorObservers: [
    BehavioralNavigatorObserver(plugin),
  ],
  // ...
)

TvBehavioralTouchTrackingWidget

A widget that wraps any widget to automatically capture and send touch events to the SDK. Works on both Android and iOS.

Wraps the child widget with a Listener that captures PointerDownEvent, PointerMoveEvent, and PointerUpEvent. Tracks active pointers for correct multi-touch handling. You can wrap it around your entire app or around specific widgets you want to track.

Parameters:

ParameterTypeRequiredDefaultDescription
childWidgetYes—Widget tree to track
enableboolNotrueEnable/disable touch tracking

Touch events captured:

EventAction NameAction Masked
First touch downaction_down0
Additional touch (multi-touch)action_pointer_down5
Pointer movementaction_move2
Last touch upaction_up1
Intermediate touch up (multi-touch)action_pointer_up6

Example — Wrap entire app:

Dart
import 'package:flutter_tv_behavioral_plugin/behavioral_widget/tv_behavioral_touch_tracking_widget.dart';

void main() {
  runApp(
    TvBehavioralTouchTrackingWidget(
      child: const MyApp(),
    ),
  );
}

Example — Wrap a specific widget:

Dart
TvBehavioralTouchTrackingWidget(
  child: Column(
    children: [
      TextField(decoration: InputDecoration(labelText: 'Username')),
      TextField(decoration: InputDecoration(labelText: 'Password')),
      ElevatedButton(onPressed: _login, child: Text('Login')),
    ],
  ),
)

BehavioralTouchTracker (Android only)

A helper class that forwards MotionEvents from Activity.dispatchTouchEvent to the SDK without requiring a direct dependency on the native behavioral AAR.

Example — FlutterActivity:

Kotlin
class MyFlutterActivity : FlutterActivity() {
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        BehavioralTouchTracker.track(this, ev)
        return super.dispatchTouchEvent(ev)
    }
}

Example — FlutterFragmentActivity:

Kotlin
class MyFlutterFragmentActivity : FlutterFragmentActivity() {
    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        BehavioralTouchTracker.track(this, ev)
        return super.dispatchTouchEvent(ev)
    }
}

FlutterKeyboardTracker

Utility class that tracks keyboard input on TextEditingController instances and automatically sends data to the native SDK. Detects keystroke type by analyzing text changes.

Methods:

attachController

Starts tracking a TextEditingController. Attaches a listener that detects insertions, deletions, and cursor movements.

Dart
void attachController({
  required TextEditingController controller,
  required String viewId,
})
ParameterTypeRequiredDescription
controllerTextEditingControllerYesThe controller to track
viewIdStringYesUnique identifier for this text field

detachController

Stops tracking a specific controller.

Dart
void detachController(TextEditingController controller)

detachAll

Stops tracking all attached controllers.

Dart
void detachAll()

Keystroke type detection:

TypeDetection
numberDigit 0-9
lowerTextLowercase a-z
upperTextUppercase A-Z
spacingSpace character
specialCharactersNon-alphanumeric characters
deleteText shortened (backspace)
cursorMovingSame text length, different cursor position

Example:

Dart
final keyboardTracker = FlutterKeyboardTracker();
final emailController = TextEditingController();
final passwordController = TextEditingController();

@override
void initState() {
  super.initState();
  keyboardTracker.attachController(
    controller: emailController,
    viewId: 'email_field',
  );
  keyboardTracker.attachController(
    controller: passwordController,
    viewId: 'password_field',
  );
}

@override
void dispose() {
  keyboardTracker.detachAll();
  emailController.dispose();
  passwordController.dispose();
  super.dispose();
}

addKeyboardTrackingData

Sends keyboard tracking data manually. Used internally by FlutterKeyboardTracker. Use this directly only for custom input handling where FlutterKeyboardTracker is not suitable.

Signature:

Dart
Future<bool> addKeyboardTrackingData({
  String? data,
  String? type,
  String? viewId,
  int? textLength,
})

Parameters:

ParameterTypeRequiredDescription
dataString?NoCharacter value (only if trackRawText is true)
typeString?NoKeystroke type: lowerText, upperText, number, spacing, specialCharacters, delete, cursorMoving, mixed, unknown
viewIdString?NoIdentifier for the input field
textLengthint?NoCurrent text field length

Returns: Future<bool> — true on success, false on failure (errors are logged, not thrown)

Example:

Dart
await plugin.addKeyboardTrackingData(
  data: null,
  type: 'lowerText',
  viewId: 'email_field',
  textLength: 5,
);

trackTouchEvent

Sends touch event data to the native SDK. Used internally by TvBehavioralTouchTrackingWidget. Use this directly only for custom touch handling.

Signature:

Dart
Future<void> trackTouchEvent(Map<String, dynamic> eventData)

Parameters:

ParameterTypeRequiredDescription
eventDataMap<String, dynamic>YesTouch event data

Event data fields:

KeyTypeDescription
actionNameStringaction_down, action_pointer_down, action_move, action_up, action_pointer_up
pointerIdintPointer identifier
globalXdoubleScreen X coordinate
globalYdoubleScreen Y coordinate
actionMaskedintAction constant (0=DOWN, 1=UP, 2=MOVE, 5=POINTER_DOWN, 6=POINTER_UP)

Example:

Dart
await plugin.trackTouchEvent({
  'actionName': 'action_down',
  'pointerId': 0,
  'globalX': 150.0,
  'globalY': 300.0,
  'actionMasked': 0,
});

finalizeCollect

Stops all collectors, flushes remaining data, and waits for the upload queue to drain completely.

Pass the returned challengeId to your backend to trigger behavior score computation via POST /v1/behavior/score.

Signature:

Dart
Future<TSFinalizeResult> finalizeCollect()

Returns: TSFinalizeResult

FieldTypeDescription
nonceString?Session nonce
challengeIdString?Challenge ID — pass to backend for scoring
lastSeqintLast uploaded batch sequence number
batchIdString?Last batch identifier
droppedBatchCountintNumber of event batches dropped without reaching the server (data loss). Defaults to 0; 0 on a clean finalize
lastUploadErrorString?Description of the last upload error that caused a drop, or null when none

Observability (v1.0.0.3): droppedBatchCount and lastUploadError are parsed from the native result via fromMap and are back-compatible — when the native platform omits these keys, they default to 0 and null respectively. A non-zero droppedBatchCount means some collected batches never reached the server; inspect lastUploadError for the cause.

Throws: PlatformException with code TSSessionError

Example:

Dart
try {
  final result = await plugin.finalizeCollect();

  debugPrint('Challenge ID: ${result.challengeId}');
  debugPrint('Last seq: ${result.lastSeq}');

  if (result.droppedBatchCount > 0) {
    debugPrint(
      'Dropped ${result.droppedBatchCount} batches. Last error: ${result.lastUploadError}',
    );
  }

  // Send challengeId to your backend for scoring
  await yourBackend.computeBehaviorScore(
    challengeId: result.challengeId!,
  );
} on PlatformException catch (e) {
  debugPrint('Finalize error [${e.code}]: ${e.message}');
}

stopCollect

Stops all collectors immediately without flushing data or draining the upload queue. This is the fast shutdown — use when you need to stop quickly (e.g., app backgrounding). May result in partial data loss.

Signature:

Dart
Future<TSFinalizeResult> stopCollect()

Returns: TSFinalizeResult (same structure as finalizeCollect, including the droppedBatchCount and lastUploadError observability fields). Because stopCollect() skips the flush and upload-queue drain, a non-zero droppedBatchCount is expected here.

Throws: PlatformException with code TSSessionError

finalizeCollect()stopCollect()
AsyncFutureFuture
Flush dataYesNo
Drain upload queueYes (waits)No
Data loss riskNonePossible
Use caseNormal completionApp backgrounding

Example:

Dart
try {
  final result = await plugin.stopCollect();
  debugPrint('Stopped. Last seq: ${result.lastSeq}');
} on PlatformException catch (e) {
  debugPrint('Stop error [${e.code}]: ${e.message}');
}

clearDeviceIdentity

Clears all stored device identity (device ID, registration status, key pair). You must call initialize() again after this to generate a new device identity.

Use when the user logs out or you need to reset the SDK completely.

Signature:

Dart
Future<bool> clearDeviceIdentity()

Returns: Future<bool> — true on success, false on failure (errors are logged, not thrown)

Example:

Dart
try {
  await plugin.clearDeviceIdentity();
  debugPrint('Device identity cleared');
  // Must call initialize() again before using SDK
} on PlatformException catch (e) {
  debugPrint('Clear error [${e.code}]: ${e.message}');
}

Error Handling

All SDK errors are thrown as PlatformException from the method channel. The code property maps directly to the native SDK's TSSessionError.name — both Android and iOS use the same error code strings.

Native SDK Error Mapping

The Flutter plugin catches native TSSessionError and converts it to a PlatformException whose code is error.name and whose message is the platform's human-readable description (Android error.message, iOS error.errorDescription).

PlatformException.codeAndroid (TSSessionError)iOS (TSSessionError)Description
notInitializedNotInitialized.notInitializedSDK not initialized. Call initialize() first
deviceNotRegisteredDeviceNotRegistered.deviceNotRegisteredDevice not registered. Call markDeviceRegistered() before startCollect()
challengeExpiredChallengeExpired.challengeExpiredServer challenge expired. Retry startCollect()
invalidChallengeResponseInvalidChallengeResponse.invalidChallengeResponseInvalid or empty challenge from server
notStartedNotStarted.notStartedNo active collection session. Call startCollect() first
sslPinningErrorSslPinningError(cause).sslPinningError(Error)TLS/SSL pinning validation failed — server certificate did not match a pinned certificate
keychainErrorKeystoreError(cause).keychainError(Error)Keychain/Keystore operation failed
cryptoErrorCryptoError(cause).cryptoError(Error)Encryption or signing operation failed
networkErrorNetworkError(code, message) / NetworkError(cause).networkError(Error)HTTP request or network connectivity failure. An HTTP-level failure carries a details map (see below); a pure connectivity failure has null details
serializationErrorSerializationError(cause).serializationError(Error)Failed to encode/decode a request or response payload
bootstrapKeyNotConfigured—.bootstrapKeyNotConfiguredBootstrap RSA public key not configured (iOS only)
unknownErrorUnknownError(cause).unknownError(Error)An unclassified error occurred
managerStartErrorManagerStartError(cause)—Failed to start data collection managers (Android only)

Error details (PlatformException.details)

Every SDK error surfaces as a PlatformException where:

  • e.code is the native error name (e.g. 'networkError', 'sslPinningError', 'deviceNotRegistered').
  • e.message is the human-readable message. Its exact wording is platform-specific — do not parse it programmatically.
  • e.details is populated only for an HTTP-level network error (the server responded with a status code), as a Map; for every other error kind — including a pure connectivity failure — it is null.

e.details for an HTTP-level networkError:

KeyTypeDescription
httpCodeintHTTP status code of the failed response
serverCodeString?The server's semantic error code (errors[].code)

These two structured fields are identical across Android and iOS. See the API reference for the meaning of serverCode. For the server's human-readable message, read e.message.

Bridge-Specific Errors

These errors originate from the Flutter plugin bridge layer, not from the native SDKs.

PlatformException.codePlatformDescription
activity_not_foundAndroid onlyAndroid Activity not available for the plugin
touch_event_data_not_foundBothTouch event data missing or invalid in trackTouchEvent()
screen_name_not_foundBothScreen name parameter missing in updateScreenTransition()
exceptionBothUnexpected internal error (catch-all for untyped exceptions)

Example

Dart
try {
  await plugin.startCollect();
} on PlatformException catch (e) {
  switch (e.code) {
    case 'deviceNotRegistered':
      debugPrint('Register device first via your backend');
      break;
    case 'notInitialized':
      debugPrint('Call initialize() before startCollect()');
      break;
    case 'sslPinningError':
      debugPrint('SSL pinning failed: ${e.message}');
      break;
    case 'networkError':
      // For a network error, e.details is a Map with server context
      final details = e.details as Map?;
      final httpCode = details?['httpCode'] as int?;
      final serverCode = details?['serverCode'] as String?;
      debugPrint('Network issue: HTTP $httpCode / $serverCode — ${e.message}');
      break;
    case 'challengeExpired':
      debugPrint('Challenge expired, retry startCollect()');
      break;
    default:
      debugPrint('SDK error [${e.code}]: ${e.message}');
  }
}

Complete Example

A full Flutter app integrating all SDK functions: touch tracking widget, keyboard tracking, initialization, device registration, data collection, screen transitions, and finalization.

Dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_tv_behavioral_plugin/flutter_tv_behavioral_plugin.dart';
import 'package:flutter_tv_behavioral_plugin/data/ts_behavioral_config.dart';
import 'package:flutter_tv_behavioral_plugin/behavioral_widget/tv_behavioral_touch_tracking_widget.dart';
import 'package:flutter_tv_behavioral_plugin/behavioral_widget/tv_behavioral_keyboard_tracking.dart';

// 1. Wrap your app (or any widget) with touch tracking widget
void main() {
  runApp(
    TvBehavioralTouchTrackingWidget(
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _plugin = FlutterTvBehavioralPlugin();
  final _keyboardTracker = FlutterKeyboardTracker();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _phoneController = TextEditingController();

  String _status = 'Not initialized';

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

    // 2. Attach keyboard tracking to all text fields
    _keyboardTracker.attachController(
      controller: _emailController,
      viewId: 'email_field',
    );
    _keyboardTracker.attachController(
      controller: _passwordController,
      viewId: 'password_field',
    );
    _keyboardTracker.attachController(
      controller: _phoneController,
      viewId: 'phone_field',
    );

    _initBehavioralSDK();
  }

  Future<void> _initBehavioralSDK() async {
    try {
      // 3. Configure SDK
      final config = TSBehavioralConfig(
        appId: 'your-app-id',
        baseUrl: 'https://your-base-url/api',
        metadataConfig: const MetaDataCollectionConfig(
          intervalMillis: 2000,
        ),
        sensorConfig: const SensorConfig(
          samplingIntervalMs: 20,
          sensorTypes: [
            SensorType.gyroscope,
            SensorType.magnetometer,
            SensorType.accelerometer,
            SensorType.light,
          ],
        ),
        keyboardConfig: const KeyboardConfig(
          trackRawText: false,
        ),
        touchConfig: const TouchConfig(
          enabled: true,
        ),
      );

      // 4. Initialize SDK
      final initInfo = await _plugin.initialize(config);
      setState(() => _status = 'Initialized');

      // 5. Register device (skip if already registered)
      if (!initInfo.isRegistered) {
        // Your backend calls:
        //   POST /v1/register_device
        //   Body: {
        //     "device_id": initInfo.deviceId,
        //     "key_pair_id": initInfo.bootstrapKeyId,
        //     "device_public_key": initInfo.encryptedDevicePublicKeyPayload,
        //     "encrypted_secret_key": initInfo.encryptedSecretKey,
        //     "cus_user_id": "your-user-id",
        //     "protocol": "encrypted"
        //   }
        //   Response: { "data": { "customer_user_device_id": "..." } }
        final userDeviceId = await yourBackend.registerDevice(
          deviceId: initInfo.deviceId,
          keyPairId: initInfo.bootstrapKeyId,
          encryptedDevicePublicKeyPayload: initInfo.encryptedDevicePublicKeyPayload,
          encryptedSecretKey: initInfo.encryptedSecretKey,
          cusUserId: 'your-user-id',
        );

        await _plugin.markDeviceRegistered(
          isRegistered: true,
          customerUserDeviceId: userDeviceId,
        );
      }
      setState(() => _status = 'Registered');

      // 6. Start data collection
      await _plugin.startCollect();
      setState(() => _status = 'Collecting...');

      // 7. Track initial screen (required, call immediately after startCollect)
      await _plugin.updateScreenTransition('login_screen');

    } on PlatformException catch (e) {
      setState(() => _status = 'Error: ${e.code}');
      debugPrint('SDK error [${e.code}]: ${e.message}');
    }
  }

  // 8. Track screen transitions on navigation
  void _navigateToPayment() {
    _plugin.updateScreenTransition('payment_screen');
    // ... navigate to payment screen
  }

  // 9. Finalize when the flow is complete
  Future<void> _onSubmit() async {
    try {
      final result = await _plugin.finalizeCollect();
      setState(() => _status = 'Finalized');

      // Send challengeId to your backend to compute behavior score:
      //   POST /v1/behavior/score
      //   Body: { "challenge_id": result.challengeId }
      await yourBackend.computeBehaviorScore(
        challengeId: result.challengeId!,
      );

    } on PlatformException catch (e) {
      debugPrint('Finalize error [${e.code}]: ${e.message}');
    }
  }

  // 10. Clear identity on logout
  Future<void> _onLogout() async {
    await _plugin.clearDeviceIdentity();
    setState(() => _status = 'Cleared');
    // Must call initialize() again before next use
  }

  @override
  void dispose() {
    _keyboardTracker.detachAll();
    _emailController.dispose();
    _passwordController.dispose();
    _phoneController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Behavioral SDK — $_status')),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              TextField(
                controller: _emailController,
                decoration: const InputDecoration(labelText: 'Email'),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _passwordController,
                obscureText: true,
                decoration: const InputDecoration(labelText: 'Password'),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _phoneController,
                keyboardType: TextInputType.phone,
                decoration: const InputDecoration(labelText: 'Phone'),
              ),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: _onSubmit,
                child: const Text('Submit'),
              ),
              const SizedBox(height: 12),
              TextButton(
                onPressed: _onLogout,
                child: const Text('Logout'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Server-Side Device Registration

After calling initialize(), your app must register the device with TrustVision backend before calling startCollect(). This is a server-side call — typically done from your app via an authenticated API client.

API Endpoint

POST {baseUrl}/v1/register_device
Content-Type: application/json
Authorization: TV {accessKeyId}:{signature}
X-TV-Timestamp: {ISO8601 UTC timestamp}

Request Body

FieldTypeSourceDescription
device_idStringinitInfo.deviceIdSDK-generated device identifier
key_pair_idStringinitInfo.bootstrapKeyIdBootstrap key pair identifier
device_public_keyStringinitInfo.encryptedDevicePublicKeyPayloadAES-GCM sealed device public key (base64)
encrypted_secret_keyStringinitInfo.encryptedSecretKeyRSA-OAEP encrypted AES secret key (base64)
cus_user_idStringYour appCustomer user identifier from your system
protocolString—Always "encrypted"
JSON
{
  "device_id": "from initInfo.deviceId",
  "key_pair_id": "from initInfo.bootstrapKeyId",
  "device_public_key": "from initInfo.encryptedDevicePublicKeyPayload",
  "encrypted_secret_key": "from initInfo.encryptedSecretKey",
  "cus_user_id": "your-user-identifier",
  "protocol": "encrypted"
}

Response

JSON
{
  "data": {
    "customer_user_device_id": "server-assigned-id"
  }
}

HMAC-SHA256 Authentication

All API requests require HMAC-SHA256 signature in the Authorization header.

String to sign:

{METHOD}\n{PATH}\n{TIMESTAMP}
  • METHOD — HTTP method (e.g., POST)
  • PATH — API path without base URL (e.g., /v1/register_device)
  • TIMESTAMP — ISO 8601 UTC timestamp (e.g., 2026-03-31T12:00:00.000+0000)

Signature: Base64(HMAC-SHA256(accessKeySecret, stringToSign))

Headers:

HeaderValue
AuthorizationTV {accessKeyId}:{signature}
X-TV-TimestampISO 8601 UTC timestamp

Dart Example

Dart
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';

class TVApiClient {
  final Dio _dio;

  TVApiClient({
    required String baseUrl,
    required String accessKeyId,
    required String accessKeySecret,
  }) : _dio = Dio(BaseOptions(baseUrl: baseUrl))
    ..interceptors.add(_AuthInterceptor(
      accessKeyId: accessKeyId,
      accessKeySecret: accessKeySecret,
    ));

  Future<String> registerDevice({
    required String keyPairId,
    required String encryptedDevicePublicKeyPayload,
    required String encryptedSecretKey,
    required String cusUserId,
    required String deviceId,
  }) async {
    final response = await _dio.post('/v1/register_device', data: {
      'key_pair_id': keyPairId,
      'device_public_key': encryptedDevicePublicKeyPayload,
      'encrypted_secret_key': encryptedSecretKey,
      'cus_user_id': cusUserId,
      'device_id': deviceId,
      'protocol': 'encrypted',
    });

    final responseData = response.data;
    if (responseData is! Map<String, dynamic>) {
      throw Exception('Unexpected response format: $responseData');
    }
    final data = responseData['data'] as Map<String, dynamic>?;
    return data?['customer_user_device_id'] ?? '';
  }
}

// --- HMAC-SHA256 Auth Interceptor ---

class _AuthInterceptor extends Interceptor {
  final String accessKeyId;
  final String accessKeySecret;

  _AuthInterceptor({
    required this.accessKeyId,
    required this.accessKeySecret,
  });

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    final method = options.method;
    final path = options.path;
    final timestamp = DateTime.now().toUtc().toIso8601String();

    final stringToSign = '$method\n$path\n$timestamp';
    final hmacSha256 = Hmac(sha256, utf8.encode(accessKeySecret));
    final digest = hmacSha256.convert(utf8.encode(stringToSign));
    final signature = base64.encode(digest.bytes);

    options.headers['Authorization'] = 'TV $accessKeyId:$signature';
    options.headers['X-TV-Timestamp'] = timestamp;

    super.onRequest(options, handler);
  }
}

Full Registration Flow

Dart
// 1. Initialize SDK
final initInfo = await plugin.initialize(config);

// 2. Register device via your backend
final client = TVApiClient(
  baseUrl: 'https://your-trustvision-endpoint.com/api',
  accessKeyId: 'your-access-key-id',
  accessKeySecret: 'your-access-key-secret',
);
final customerUserDeviceId = await client.registerDevice(
  keyPairId: initInfo.bootstrapKeyId,
  encryptedDevicePublicKeyPayload: initInfo.encryptedDevicePublicKeyPayload,
  encryptedSecretKey: initInfo.encryptedSecretKey,
  cusUserId: 'your-user-id',
  deviceId: initInfo.deviceId,
);

// 3. Mark device as registered in SDK
await plugin.markDeviceRegistered(
  isRegistered: true,
  customerUserDeviceId: customerUserDeviceId,
);

// 4. Now you can start collecting
await plugin.startCollect();