Flutter v1.0.0.3
Specifications
| Spec | Value |
|---|---|
| Flutter | >= 3.3.0 |
| Dart SDK | >= 3.5.0 < 4.0.0 |
| Android Min SDK | 24 (Android 7.0) |
| iOS Min | 13.0 |
| Method Channel | flutter_tv_behavioral_plugin |
| Bundled Android native | behaviors 1.0.0.3 |
| Bundled iOS native | BehaviorSDK 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:
dependencies:
flutter_tv_behavioral_plugin:
path: ./flutter_tv_behavioral_plugin # or your local path
Then run:
flutter pub get
2. Android setup
In your app-level build.gradle:
android {
compileSdk 36
defaultConfig {
minSdk 24
}
}
Add the local Maven repository for the native SDK in settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("${rootProject.projectDir}/repo")
}
}
}
3. iOS setup
In your Podfile:
platform :ios, '13.0'
Then run:
cd ios && pod install
4. Permissions (iOS only)
Add to Info.plist if you enable Bluetooth or Location collection:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Used for device behavioral analysis</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Used for device behavioral analysis</string>
Configuration
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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
appId | String | Yes | — | Your app's package name or bundle identifier |
baseUrl | String | Yes | — | Base URL for TrustVision API |
metadataConfig | MetaDataCollectionConfig | Yes | — | Device metadata collection settings |
uploadBlockSize | int | No | 10 | Number of batch entries before auto-upload |
sensorConfig | SensorConfig | No | SensorConfig() | Sensor data collection settings |
keyboardConfig | KeyboardConfig | No | KeyboardConfig() | Keyboard tracking settings |
touchConfig | TouchConfig | No | TouchConfig() | Touch tracking settings |
screenTransitionBlockSize | int | No | 1 | Screen transition batch size |
appStateBlockSize | int | No | 1 | Number of app-state events per upload batch |
batchExtra | Map<String, String>? | No | null | Custom key-value metadata in every batch upload |
retryConfig | RetryConfig? | No | RetryConfig() | Upload retry queue settings |
sslCertificates | List<TSCertificate>? | No | null | Certificates 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:
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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
intervalMillis | int | Yes | — | Metadata collection interval in milliseconds |
canAskPermission | bool | No | false | Whether SDK may request system permissions — iOS only (Bluetooth, Location) |
enabled | bool | No | true | Enable/disable metadata collection |
SensorConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
samplingIntervalMs | int | No | 20 (50Hz) | Sensor sampling interval in milliseconds |
sensorTypes | List<SensorType> | No | All 4 types | Sensors to collect: gyroscope, accelerometer, magnetometer, light |
enabled | bool | No | true | Enable/disable sensor collection |
KeyboardConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
trackRawText | bool | No | false | Whether to collect raw typed text. When false, only character type is recorded |
enabled | bool | No | true | Enable/disable keyboard tracking |
blockSize | int | No | 1 | Number of keyboard events per upload batch |
TouchConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | bool | No | true | Enable/disable touch tracking |
RetryConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | bool | No | true | Enable/disable the retry queue |
maxRetries | int | No | 2 | Max immediate retries per request (0–10). Total calls = value + 1 |
retryDelayMs | int | No | 1000 | Base delay before first retry in milliseconds (> 0) |
maxDelayMs | int | No | 5000 | Maximum delay cap in milliseconds (≥ retryDelayMs) |
maxRetryAttempts | int | No | 3 | Maximum retry-queue attempts before dropping a failed batch (≥ 1) |
maxRetryStorageItems | int | No | 50 | Maximum items in retry storage before FIFO eviction (≥ 1) |
finalFlushTimeoutSeconds | double | No | 10.0 | Timeout in seconds for flush at session end (≥ 1) |
retryItemDelaySeconds | double | No | 0.5 | Delay 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:
Future<TSDeviceInitInfo> initialize(TSBehavioralConfig config)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
config | TSBehavioralConfig | Yes | SDK configuration |
Returns: TSDeviceInitInfo
| Field | Type | Description |
|---|---|---|
deviceId | String | Unique device identifier (UUID) |
bootstrapKeyId | String | Key pair ID for encryption |
isRegistered | bool | Whether device was previously registered |
encryptedDevicePublicKeyPayload | String | AES-GCM sealed device public key (base64). Pass to device_public_key in register API |
encryptedSecretKey | String | RSA-OAEP encrypted AES secret key (base64). Pass to encrypted_secret_key in register API |
Throws: PlatformException with code TSSessionError
Example:
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:
Future<bool> markDeviceRegistered({
required bool isRegistered,
required String customerUserDeviceId,
})
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
isRegistered | bool | Yes | Registration status (true to mark as registered) |
customerUserDeviceId | String | Yes | Unique user-device identifier returned by your server |
Returns: Future<bool> — true on success, false on failure (errors are logged, not thrown)
Example:
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:
Future<bool> startCollect()
Returns: Future<bool> — true on success, false on failure (errors are logged, not thrown)
Example:
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 afterstartCollect()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:
Future<void> updateScreenTransition(String screenName)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
screenName | String | Yes | Name of the screen being navigated to |
Example — call immediately after startCollect:
await plugin.startCollect();
await plugin.updateScreenTransition('main_screen');
// Then call again on every screen navigation
await plugin.updateScreenTransition('payment_screen');
Example — with Navigator observer:
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:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
child | Widget | Yes | — | Widget tree to track |
enable | bool | No | true | Enable/disable touch tracking |
Touch events captured:
| Event | Action Name | Action Masked |
|---|---|---|
| First touch down | action_down | 0 |
| Additional touch (multi-touch) | action_pointer_down | 5 |
| Pointer movement | action_move | 2 |
| Last touch up | action_up | 1 |
| Intermediate touch up (multi-touch) | action_pointer_up | 6 |
Example — Wrap entire app:
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:
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:
class MyFlutterActivity : FlutterActivity() {
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
BehavioralTouchTracker.track(this, ev)
return super.dispatchTouchEvent(ev)
}
}
Example — FlutterFragmentActivity:
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.
void attachController({
required TextEditingController controller,
required String viewId,
})
| Parameter | Type | Required | Description |
|---|---|---|---|
controller | TextEditingController | Yes | The controller to track |
viewId | String | Yes | Unique identifier for this text field |
detachController
Stops tracking a specific controller.
void detachController(TextEditingController controller)
detachAll
Stops tracking all attached controllers.
void detachAll()
Keystroke type detection:
| Type | Detection |
|---|---|
number | Digit 0-9 |
lowerText | Lowercase a-z |
upperText | Uppercase A-Z |
spacing | Space character |
specialCharacters | Non-alphanumeric characters |
delete | Text shortened (backspace) |
cursorMoving | Same text length, different cursor position |
Example:
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:
Future<bool> addKeyboardTrackingData({
String? data,
String? type,
String? viewId,
int? textLength,
})
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
data | String? | No | Character value (only if trackRawText is true) |
type | String? | No | Keystroke type: lowerText, upperText, number, spacing, specialCharacters, delete, cursorMoving, mixed, unknown |
viewId | String? | No | Identifier for the input field |
textLength | int? | No | Current text field length |
Returns: Future<bool> — true on success, false on failure (errors are logged, not thrown)
Example:
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:
Future<void> trackTouchEvent(Map<String, dynamic> eventData)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
eventData | Map<String, dynamic> | Yes | Touch event data |
Event data fields:
| Key | Type | Description |
|---|---|---|
actionName | String | action_down, action_pointer_down, action_move, action_up, action_pointer_up |
pointerId | int | Pointer identifier |
globalX | double | Screen X coordinate |
globalY | double | Screen Y coordinate |
actionMasked | int | Action constant (0=DOWN, 1=UP, 2=MOVE, 5=POINTER_DOWN, 6=POINTER_UP) |
Example:
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:
Future<TSFinalizeResult> finalizeCollect()
Returns: TSFinalizeResult
| Field | Type | Description |
|---|---|---|
nonce | String? | Session nonce |
challengeId | String? | Challenge ID — pass to backend for scoring |
lastSeq | int | Last uploaded batch sequence number |
batchId | String? | Last batch identifier |
droppedBatchCount | int | Number of event batches dropped without reaching the server (data loss). Defaults to 0; 0 on a clean finalize |
lastUploadError | String? | Description of the last upload error that caused a drop, or null when none |
Observability (v1.0.0.3):
droppedBatchCountandlastUploadErrorare parsed from the native result viafromMapand are back-compatible — when the native platform omits these keys, they default to0andnullrespectively. A non-zerodroppedBatchCountmeans some collected batches never reached the server; inspectlastUploadErrorfor the cause.
Throws: PlatformException with code TSSessionError
Example:
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:
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() | |
|---|---|---|
| Async | Future | Future |
| Flush data | Yes | No |
| Drain upload queue | Yes (waits) | No |
| Data loss risk | None | Possible |
| Use case | Normal completion | App backgrounding |
Example:
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:
Future<bool> clearDeviceIdentity()
Returns: Future<bool> — true on success, false on failure (errors are logged, not thrown)
Example:
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.code | Android (TSSessionError) | iOS (TSSessionError) | Description |
|---|---|---|---|
notInitialized | NotInitialized | .notInitialized | SDK not initialized. Call initialize() first |
deviceNotRegistered | DeviceNotRegistered | .deviceNotRegistered | Device not registered. Call markDeviceRegistered() before startCollect() |
challengeExpired | ChallengeExpired | .challengeExpired | Server challenge expired. Retry startCollect() |
invalidChallengeResponse | InvalidChallengeResponse | .invalidChallengeResponse | Invalid or empty challenge from server |
notStarted | NotStarted | .notStarted | No active collection session. Call startCollect() first |
sslPinningError | SslPinningError(cause) | .sslPinningError(Error) | TLS/SSL pinning validation failed — server certificate did not match a pinned certificate |
keychainError | KeystoreError(cause) | .keychainError(Error) | Keychain/Keystore operation failed |
cryptoError | CryptoError(cause) | .cryptoError(Error) | Encryption or signing operation failed |
networkError | NetworkError(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 |
serializationError | SerializationError(cause) | .serializationError(Error) | Failed to encode/decode a request or response payload |
bootstrapKeyNotConfigured | — | .bootstrapKeyNotConfigured | Bootstrap RSA public key not configured (iOS only) |
unknownError | UnknownError(cause) | .unknownError(Error) | An unclassified error occurred |
managerStartError | ManagerStartError(cause) | — | Failed to start data collection managers (Android only) |
Error details (PlatformException.details)
Every SDK error surfaces as a PlatformException where:
e.codeis the native error name (e.g.'networkError','sslPinningError','deviceNotRegistered').e.messageis the human-readable message. Its exact wording is platform-specific — do not parse it programmatically.e.detailsis populated only for an HTTP-level network error (the server responded with a status code), as aMap; for every other error kind — including a pure connectivity failure — it isnull.
e.details for an HTTP-level networkError:
| Key | Type | Description |
|---|---|---|
httpCode | int | HTTP status code of the failed response |
serverCode | String? | 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.code | Platform | Description |
|---|---|---|
activity_not_found | Android only | Android Activity not available for the plugin |
touch_event_data_not_found | Both | Touch event data missing or invalid in trackTouchEvent() |
screen_name_not_found | Both | Screen name parameter missing in updateScreenTransition() |
exception | Both | Unexpected internal error (catch-all for untyped exceptions) |
Example
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.
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
| Field | Type | Source | Description |
|---|---|---|---|
device_id | String | initInfo.deviceId | SDK-generated device identifier |
key_pair_id | String | initInfo.bootstrapKeyId | Bootstrap key pair identifier |
device_public_key | String | initInfo.encryptedDevicePublicKeyPayload | AES-GCM sealed device public key (base64) |
encrypted_secret_key | String | initInfo.encryptedSecretKey | RSA-OAEP encrypted AES secret key (base64) |
cus_user_id | String | Your app | Customer user identifier from your system |
protocol | String | — | Always "encrypted" |
{
"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
{
"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:
| Header | Value |
|---|---|
Authorization | TV {accessKeyId}:{signature} |
X-TV-Timestamp | ISO 8601 UTC timestamp |
Dart Example
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
// 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();