entrust_idvsdk_flutter 100.10.0
entrust_idvsdk_flutter: ^100.10.0 copied to clipboard
Entrust Identity SDK Flutter plugin for document and biometric capture.
example/lib/main.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:entrust_idvsdk_flutter/entrust_idvsdk_flutter.dart';
import 'smoke_test_page.dart';
const String devtoolVersion = 'main';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Resolved before the first frame so DevToolsPage knows, synchronously, not
// to build its WebView on an automated run.
final sdkToken = await readLaunchSdkToken();
runApp(MyApp(sdkToken: sdkToken));
}
class MyApp extends StatelessWidget {
const MyApp({super.key, this.sdkToken});
final String? sdkToken;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'IDV SDK Flutter Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: DevToolsPage(sdkToken: sdkToken),
);
}
}
class DevToolsPage extends StatefulWidget {
const DevToolsPage({super.key, this.sdkToken});
/// Set only when Maestro launched the app with a token, i.e. on an E2E run.
final String? sdkToken;
@override
State<DevToolsPage> createState() => _DevToolsPageState();
}
class _DevToolsPageState extends State<DevToolsPage> {
late final WebViewController _webViewController;
late EntrustIdv _idv;
bool _isLoading = true;
/// Mirrors the native demo apps, which only pass `onMedia` through when
/// DevTools asked for it. Registering it costs a full serialisation of every
/// captured image across the bridge, so it stays off until requested.
bool _mediaCallbacksEnabled = false;
/// The DevTools WebView pulls https://sdk.onfido.com and runs a Chromium
/// renderer in its own process. On a loaded CI emulator that renderer gets
/// killed, which takes the whole app down with it. The smoke test needs
/// neither the network nor a WebView, so it never builds one.
bool get _isAutomatedRun => widget.sdkToken?.isNotEmpty ?? false;
@override
void initState() {
super.initState();
_idv = _createIdv();
if (!_isAutomatedRun) {
_initializeWebView();
}
}
EntrustIdv _createIdv() {
return EntrustIdv(
callbacks: Callbacks(
onComplete: (result) {
debugPrint('SDK completed: $result');
SmokeEvents.publish(const SmokeEvent(SmokeEventKind.complete));
_injectJavaScript(
'devtools.callbacks.onComplete(${jsonEncode(_captureResultsToJson(result))})',
);
},
onError: (error) {
debugPrint('SDK error: ${error.message}');
SmokeEvents.publish(
SmokeEvent(SmokeEventKind.error, message: error.message),
);
_injectJavaScript(
'devtools.callbacks.onError(${jsonEncode(error.toJson())})',
);
},
onUserExit: (userAction) {
debugPrint('User exited: ${userAction.value}');
SmokeEvents.publish(const SmokeEvent(SmokeEventKind.exit));
_injectJavaScript(
'devtools.callbacks.onExit(${jsonEncode(userAction.value)})',
);
},
onMedia: _mediaCallbacksEnabled
? (media) {
debugPrint('Media received: ${media.key}');
_injectJavaScript(
'devtools.callbacks.onMedia(${jsonEncode(media.toJson())})',
);
}
: null,
onAnalytics: (analytics) {
debugPrint('Analytics event: ${analytics.identifier}');
SmokeEvents.publish(const SmokeEvent(SmokeEventKind.analytics));
_injectJavaScript(
'devtools.callbacks.onAnalytics(${jsonEncode(analytics.toJson())})',
);
},
biometricsTokenHandler: BiometricsTokenHandler(
onTokenGenerated: (customerUserHash, encryptedBiometricToken) {
debugPrint('Biometric token generated for: $customerUserHash');
_injectJavaScript(
"devtools.callbacks.biometricsTokenHandler.onTokenGenerated('$customerUserHash', '$encryptedBiometricToken')",
);
},
onTokenRequested: (customerHash) {
debugPrint('Biometric token requested for: $customerHash');
_injectJavaScript(
"devtools.callbacks.biometricsTokenHandler.onTokenRequested('$customerHash')",
);
// Return a placeholder token - in real app this would come from storage
return 'new wrong token';
},
),
),
);
}
/// Applies the DevTools `enableMediaCallbacks` switch.
///
/// The callback set is fixed when [EntrustIdv] is constructed and is handed
/// to the native SDK there, so the instance is replaced rather than mutated.
/// Disposing first matters: both instances share one method channel, and the
/// old one clears its handler on the way out.
void _applyMediaCallbacks({required bool enabled}) {
if (enabled == _mediaCallbacksEnabled) {
return;
}
_mediaCallbacksEnabled = enabled;
_idv.dispose();
_idv = _createIdv();
}
void _initializeWebView() {
_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (String url) {
setState(() {
_isLoading = false;
});
},
onWebResourceError: (WebResourceError error) {
debugPrint('WebView error: ${error.description}');
},
),
)
..addJavaScriptChannel(
'_devtools',
onMessageReceived: (JavaScriptMessage message) {
_handleWebViewMessage(message.message);
},
)
..loadRequest(
Uri.parse(
'https://sdk.onfido.com/capture/devtools/$devtoolVersion?mode=webview',
),
);
}
void _handleWebViewMessage(String message) {
try {
final devtoolsConfig = jsonDecode(message) as Map<String, dynamic>;
debugPrint('Flutter webview received config: $devtoolsConfig');
if (devtoolsConfig.containsKey('parameter')) {
final configuration = devtoolsConfig['configuration'];
if (configuration is Map) {
_applyMediaCallbacks(
enabled: configuration['enableMediaCallbacks'] == true,
);
}
final parameter = devtoolsConfig['parameter'] as Map<String, dynamic>;
final sdkParams = sdkParametersFromJson(parameter);
_idv.start(sdkParams);
}
} catch (e) {
debugPrint('Error parsing webview message: $e');
}
}
void _injectJavaScript(String script) {
if (_isAutomatedRun) {
return;
}
_webViewController.runJavaScript(script);
}
Map<String, dynamic> _captureResultsToJson(
Map<String, CaptureResult> results,
) {
return results.map((key, value) => MapEntry(key, value.toJson()));
}
@override
void dispose() {
_idv.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('IDV SDK DevTools'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
TextButton(
key: const Key('open-smoke-test'),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => SmokeTestPage(idv: _idv),
),
),
child: const Text('Smoke test'),
),
],
),
body: _isAutomatedRun
? const Center(child: Text('DevTools disabled for the automated run'))
: Stack(
children: [
WebViewWidget(controller: _webViewController),
if (_isLoading)
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading DevTools...'),
],
),
),
],
),
);
}
}