import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
class UrtenticVerificationPage extends StatefulWidget {
final String clientId;
final String flowId;
final String metadata;
final void Function(String) onFinished;
final void Function() onExited;
const UrtenticVerificationPage({
super.key,
required this.clientId,
required this.flowId,
required this.metadata,
required this.onFinished,
required this.onExited,
});
@override
State<UrtenticVerificationPage> createState() =>
_UrtenticVerificationPageState();
}
class _UrtenticVerificationPageState extends State<UrtenticVerificationPage> {
late WebViewController controller;
int loadingPercentage = 0;
static const _urtenticUrl = 'https://verify.urtentic.com';
Future<void> _requestPermissions() async {
await [Permission.camera, Permission.microphone].request();
}
@override
void initState() {
super.initState();
_requestPermissions();
_initializeController();
}
void _initializeController() {
late final PlatformWebViewControllerCreationParams params;
// iOS: enable inline media playback without requiring user gesture
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
controller = WebViewController.fromPlatformCreationParams(params)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
// Register a JS bridge so the hosted page can send events to Flutter
..addJavaScriptChannel(
'UrtenticBridge',
onMessageReceived: (JavaScriptMessage message) {
_handleMessage(message.message);
},
)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (progress) =>
setState(() => loadingPercentage = progress),
onPageStarted: (_) => setState(() => loadingPercentage = 0),
onPageFinished: (_) {
_injectBridgeListener();
setState(() => loadingPercentage = 100);
},
),
);
// Android: grant camera/microphone permissions from within the WebView
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
final androidController =
controller.platform as AndroidWebViewController;
androidController.setMediaPlaybackRequiresUserGesture(false);
androidController.setOnPlatformPermissionRequest((request) {
request.grant();
});
}
// Build the verification URL with required query parameters
final uri = Uri.parse(_urtenticUrl).replace(
queryParameters: {
'clientId': widget.clientId,
'flowId': widget.flowId,
'metadata': widget.metadata,
},
);
controller.loadRequest(uri);
}
/// Inject a postMessage listener after the page loads.
/// This forwards verification events from the hosted page to the Flutter bridge.
void _injectBridgeListener() {
const jsScript = '''
window.addEventListener('message', function(event) {
const expectedOrigin = new URL("$_urtenticUrl").origin;
if (event.origin !== expectedOrigin) return;
const data = event.data;
if (data && (
data.type === 'verificationComplete' ||
data.type === 'verificationStarted' ||
data.type === 'verificationExited'
)) {
if (window.UrtenticBridge) {
window.UrtenticBridge.postMessage(JSON.stringify(data));
}
}
}, false);
''';
controller.runJavaScript(jsScript);
}
/// Handle events sent from the hosted verification page.
void _handleMessage(String jsonString) {
try {
final data = jsonDecode(jsonString) as Map<String, dynamic>;
final type = data['type'] as String?;
switch (type) {
case 'verificationStarted':
// Verification flow has begun — update UI if needed
break;
case 'verificationComplete':
Navigator.of(context).pop();
widget.onFinished(type!);
break;
case 'verificationExited':
Navigator.of(context).pop();
widget.onExited();
break;
}
} catch (e) {
debugPrint('UrtenticBridge error: $e');
}
}
@override
void setState(VoidCallback fn) {
if (mounted) super.setState(fn);
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false, // Prevent back-swipe from closing mid-verification
child: Scaffold(
appBar: AppBar(title: const Text('Verify Identity')),
body: Stack(
children: [
WebViewWidget(controller: controller),
if (loadingPercentage < 80)
const Center(child: CircularProgressIndicator()),
],
),
),
);
}
}