Flutter Integration Guide
Complete guide for integrating v2Client verification into your Flutter application using Dart and secure backend patterns.
Architecture Overview
Flutter App
Your mobile application with verification UI
Your Backend
Generates verification IDs with your API key
v2Client
okID proxy with verification endpoints
Data Flow
Security Note
API keys are never stored in the mobile app. Your backend generates verification IDs securely, passes them to the mobile app, and receives completion notifications via webhooks from v2Client.
Step 1: Backend Setup (Verification ID Generation & Webhook Handling)
Create backend endpoints to generate verification IDs with webhook URLs and handle completion notifications from v2Client. Your backend manages the secure communication while the Flutter app handles the user interface.
Backend Dependencies (Node.js/Express Example)
// package.json
{
"dependencies": {
"express": "^4.18.2",
"axios": "^1.6.0",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5"
}
}
// .env
V2CLIENT_BASE_URL=https://verify.test.okid.io
V2CLIENT_API_KEY=your_api_key_here
WEBHOOK_URL=https://your-backend.com/api/webhooks/verification-complete
JWT_SECRET=your_jwt_secret_here
Backend API Controllers
const express = require('express');
const axios = require('axios');
const jwt = require('jsonwebtoken');
const router = express.Router();
// Mobile API for verification generation
router.post('/mobile/generate-verification', authenticateToken, async (req, res) => {
try {
const userId = req.user.id;
const requestBody = {
modules: ['document', 'liveness'],
webhook_url: process.env.WEBHOOK_URL,
settings: {
redirect_url: 'https://your-app.com/verification-complete'
}
};
const response = await axios.post(
`${process.env.V2CLIENT_BASE_URL}/api/generate-verification`,
requestBody,
{
headers: {
'Content-Type': 'application/json',
'X-SDK-Key': process.env.V2CLIENT_API_KEY
}
}
);
// Store verification ownership in your database
await db.verifications.create({
verificationId: response.data.verificationId,
userId: userId,
status: 'pending',
createdAt: new Date()
});
res.json({
verificationId: response.data.verificationId,
expiresAt: response.data.expiresAt,
webviewUrl: `https://verify.test.okid.io/verify?verification_id=${response.data.verificationId}`,
modules: response.data.modules,
flow: response.data.flow
});
} catch (error) {
console.error('Verification generation failed:', error);
res.status(500).json({ error: 'Failed to generate verification' });
}
});
router.get('/mobile/verification-status/:verificationId', authenticateToken, async (req, res) => {
try {
const userId = req.user.id;
const { verificationId } = req.params;
// Verify user owns this verification
const verification = await db.verifications.findOne({
where: { verificationId, userId }
});
if (!verification) {
return res.status(404).json({ error: 'Verification not found' });
}
res.json({
verificationId: verification.verificationId,
status: verification.status,
updatedAt: verification.updatedAt,
results: verification.results
});
} catch (error) {
console.error('Status check failed:', error);
res.status(500).json({ error: 'Failed to get verification status' });
}
});
// Webhook endpoint for v2Client notifications
router.post('/webhooks/verification-complete', async (req, res) => {
try {
const { verificationId, status, results } = req.body;
// Update verification status in database
await db.verifications.update(
{
status: status,
results: results,
completedAt: new Date()
},
{
where: { verificationId }
}
);
// Optional: Fetch images from v2Client
if (status === 'verified') {
await fetchAndStoreVerificationImages(verificationId);
}
// Send push notification to mobile app
await sendPushNotificationToUser(verificationId, status);
res.json({ message: 'Webhook processed successfully' });
} catch (error) {
console.error('Webhook processing failed:', error);
res.status(500).json({ error: 'Webhook processing failed' });
}
});
async function fetchAndStoreVerificationImages(verificationId) {
try {
const response = await axios.get(
`${process.env.V2CLIENT_BASE_URL}/api/get-verification-image/${verificationId}`,
{
headers: { 'X-SDK-Key': process.env.V2CLIENT_API_KEY },
responseType: 'arraybuffer'
}
);
// Store image (implement your storage logic)
await imageStorageService.storeVerificationImage(verificationId, 'document', response.data);
} catch (error) {
console.warn(`Failed to fetch verification images for ${verificationId}`, error);
}
}
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.sendStatus(401);
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
module.exports = router;
Step 2: Flutter App Dependencies
Set up your Flutter project with the necessary dependencies for network requests, WebView support, image handling, and camera access.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
# Networking
http: ^1.1.0
dio: ^5.3.2
# State Management
provider: ^6.1.1
# Or use your preferred state management solution
# bloc: ^8.1.2
# riverpod: ^2.4.9
# WebView
webview_flutter: ^4.4.2
webview_flutter_android: ^3.12.1
webview_flutter_wkwebview: ^3.9.4
# Camera and Image
camera: ^0.10.5+5
image_picker: ^1.0.4
path_provider: ^2.1.1
# Permissions
permission_handler: ^11.0.1
# File handling
path: ^1.8.3
# JSON serialization
json_annotation: ^4.8.1
dev_dependencies:
flutter_test:
sdk: flutter
# JSON serialization
build_runner: ^2.4.7
json_serializable: ^6.7.1
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
Android Permissions (android/app/src/main/AndroidManifest.xml)
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<application
android:usesCleartextTraffic="false">
<!-- File Provider for camera -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
iOS Permissions (ios/Runner/Info.plist)
<key>NSCameraUsageDescription</key>
<string>This app needs camera access for document and identity verification</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access for document upload</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for liveness verification</string>
Step 3: Network Layer (Backend Communication)
Create the network layer to communicate with your backend for verification management and status checking. The mobile app will use WebView or native v2Client APIs only for the verification flow itself.
Data Models
import 'package:json_annotation/json_annotation.dart';
part 'verification_models.g.dart';
// Backend API Response Models
()
class VerificationResponse {
final String verificationId;
final String expiresAt;
final String webviewUrl;
final List<String> modules;
final List<String> flow;
final String? error;
VerificationResponse({
required this.verificationId,
required this.expiresAt,
required this.webviewUrl,
required this.modules,
required this.flow,
this.error,
});
factory VerificationResponse.fromJson(Map<String, dynamic> json) =>
_$VerificationResponseFromJson(json);
Map<String, dynamic> toJson() => _$VerificationResponseToJson(this);
}
()
class VerificationStatusResponse {
final String verificationId;
final String status; // "pending", "in_progress", "verified", "rejected", "expired"
final String updatedAt;
final VerificationResults? results;
final String? error;
VerificationStatusResponse({
required this.verificationId,
required this.status,
required this.updatedAt,
this.results,
this.error,
});
factory VerificationStatusResponse.fromJson(Map<String, dynamic> json) =>
_$VerificationStatusResponseFromJson(json);
Map<String, dynamic> toJson() => _$VerificationStatusResponseToJson(this);
}
()
class VerificationResults {
final DocumentResult? document;
final LivenessResult? liveness;
final FormDataResult? formData;
VerificationResults({
this.document,
this.liveness,
this.formData,
});
factory VerificationResults.fromJson(Map<String, dynamic> json) =>
_$VerificationResultsFromJson(json);
Map<String, dynamic> toJson() => _$VerificationResultsToJson(this);
}
()
class DocumentResult {
final String status;
final double confidence;
final Map<String, dynamic> extractedData;
DocumentResult({
required this.status,
required this.confidence,
required this.extractedData,
});
factory DocumentResult.fromJson(Map<String, dynamic> json) =>
_$DocumentResultFromJson(json);
Map<String, dynamic> toJson() => _$DocumentResultToJson(this);
}
()
class LivenessResult {
final String status;
final double confidence;
final Map<String, dynamic> metadata;
LivenessResult({
required this.status,
required this.confidence,
required this.metadata,
});
factory LivenessResult.fromJson(Map<String, dynamic> json) =>
_$LivenessResultFromJson(json);
Map<String, dynamic> toJson() => _$LivenessResultToJson(this);
}
()
class FormDataResult {
final String status;
final Map<String, dynamic> data;
FormDataResult({
required this.status,
required this.data,
});
factory FormDataResult.fromJson(Map<String, dynamic> json) =>
_$FormDataResultFromJson(json);
Map<String, dynamic> toJson() => _$FormDataResultToJson(this);
}
// Native v2Client API Models (for native implementation)
()
class StartVerificationRequest {
(name: 'verification_id')
final String verificationId;
StartVerificationRequest({required this.verificationId});
factory StartVerificationRequest.fromJson(Map<String, dynamic> json) =>
_$StartVerificationRequestFromJson(json);
Map<String, dynamic> toJson() => _$StartVerificationRequestToJson(this);
}
()
class FormDataRequest {
(name: 'verification_id')
final String verificationId;
(name: 'form_data')
final Map<String, dynamic> formData;
FormDataRequest({
required this.verificationId,
required this.formData,
});
factory FormDataRequest.fromJson(Map<String, dynamic> json) =>
_$FormDataRequestFromJson(json);
Map<String, dynamic> toJson() => _$FormDataRequestToJson(this);
}
API Services
import 'package:dio/dio.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';
class ApiService {
static const String _backendBaseUrl = 'https://your-backend.com/api';
static const String _v2clientBaseUrl = 'https://verify.test.okid.io/api';
final Dio _dio;
final String _authToken;
ApiService({required String authToken})
: _authToken = authToken,
_dio = Dio() {
_dio.options.baseUrl = _backendBaseUrl;
_dio.options.headers = {
'Authorization': 'Bearer $_authToken',
'Content-Type': 'application/json',
};
_dio.options.connectTimeout = const Duration(seconds: 30);
_dio.options.receiveTimeout = const Duration(seconds: 30);
// Add logging interceptor for debug
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (object) => print(object),
));
}
// Backend API calls (primary communication)
Future<VerificationResponse> generateVerification() async {
try {
final response = await _dio.post('/mobile/generate-verification');
return VerificationResponse.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
Future<VerificationStatusResponse> getVerificationStatus(String verificationId) async {
try {
final response = await _dio.get('/mobile/verification-status/$verificationId');
return VerificationStatusResponse.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
// Native v2Client API calls (for native implementation only)
Future<void> startNativeVerification(String verificationId) async {
try {
final request = StartVerificationRequest(verificationId: verificationId);
await _dio.post(
'$_v2clientBaseUrl/start-verification',
data: request.toJson(),
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
Future<void> uploadDocument(String verificationId, File documentFile) async {
try {
final formData = FormData.fromMap({
'verification_id': verificationId,
'document': await MultipartFile.fromFile(
documentFile.path,
filename: 'document.jpg',
),
});
await _dio.post(
'$_v2clientBaseUrl/upload-document',
data: formData,
options: Options(
headers: {'Content-Type': 'multipart/form-data'},
),
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
Future<void> uploadSelfie(String verificationId, File selfieFile, {Map<String, dynamic>? biometricData}) async {
try {
final metadata = {
'attempt_id': DateTime.now().millisecondsSinceEpoch.toString(),
'passive_mode': true,
'biometric_data': biometricData ?? {},
};
final formData = FormData.fromMap({
'verification_id': verificationId,
'selfie': await MultipartFile.fromFile(
selfieFile.path,
filename: 'selfie.jpg',
),
'metadata_json': jsonEncode(metadata),
});
await _dio.post(
'$_v2clientBaseUrl/upload-selfie',
data: formData,
options: Options(
headers: {'Content-Type': 'multipart/form-data'},
),
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
Future<void> submitFormData(String verificationId, Map<String, dynamic> formData) async {
try {
final request = FormDataRequest(
verificationId: verificationId,
formData: formData,
);
await _dio.post(
'$_v2clientBaseUrl/submit-form-data',
data: request.toJson(),
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
Future<void> validateVerification(String verificationId) async {
try {
await _dio.post(
'$_v2clientBaseUrl/validate-verification',
data: {'verification_id': verificationId},
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
Future<void> acceptTerms(String verificationId) async {
try {
await _dio.post(
'$_v2clientBaseUrl/accept-terms',
data: {'verification_id': verificationId},
);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
Exception _handleDioError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return Exception('Connection timeout. Please check your internet connection.');
case DioExceptionType.badResponse:
final statusCode = e.response?.statusCode;
final message = e.response?.data?['error'] ?? 'Server error occurred';
return Exception('Server error ($statusCode): $message');
case DioExceptionType.cancel:
return Exception('Request was cancelled');
case DioExceptionType.unknown:
default:
return Exception('Network error: ${e.message}');
}
}
}
Step 4: Repository Implementation
Implement the repository pattern to handle communication with your backend. Status updates come via webhooks, eliminating the need for polling.
Verification Repository
import 'dart:io';
import '../models/verification_models.dart';
import '../services/api_service.dart';
class VerificationRepository {
final ApiService _apiService;
VerificationRepository({required ApiService apiService}) : _apiService = apiService;
// Primary backend communication
Future<VerificationResponse> generateVerification() async {
try {
return await _apiService.generateVerification();
} catch (e) {
throw Exception('Failed to generate verification: $e');
}
}
Future<VerificationStatusResponse> getVerificationStatus(String verificationId) async {
try {
return await _apiService.getVerificationStatus(verificationId);
} catch (e) {
throw Exception('Failed to get verification status: $e');
}
}
// === Native Implementation Functions (Optional) ===
// Use these only if implementing native verification flow
Future<void> startNativeVerification(String verificationId) async {
try {
await _apiService.startNativeVerification(verificationId);
} catch (e) {
throw Exception('Failed to start verification: $e');
}
}
Future<void> uploadDocument(String verificationId, File documentFile) async {
try {
await _apiService.uploadDocument(verificationId, documentFile);
} catch (e) {
throw Exception('Failed to upload document: $e');
}
}
Future<void> uploadSelfie(String verificationId, File selfieFile, {Map<String, dynamic>? biometricData}) async {
try {
await _apiService.uploadSelfie(verificationId, selfieFile, biometricData: biometricData);
} catch (e) {
throw Exception('Failed to upload selfie: $e');
}
}
Future<void> submitFormData(String verificationId, Map<String, dynamic> formData) async {
try {
await _apiService.submitFormData(verificationId, formData);
} catch (e) {
throw Exception('Failed to submit form data: $e');
}
}
Future<void> validateVerification(String verificationId) async {
try {
await _apiService.validateVerification(verificationId);
} catch (e) {
throw Exception('Failed to validate verification: $e');
}
}
Future<void> acceptTerms(String verificationId) async {
try {
await _apiService.acceptTerms(verificationId);
} catch (e) {
throw Exception('Failed to accept terms: $e');
}
}
}
// Biometric data class
class BiometricData {
final int age;
final String gender;
final double genderProbability;
BiometricData({
required this.age,
required this.gender,
required this.genderProbability,
});
Map<String, dynamic> toJson() => {
'age': age,
'gender': gender,
'genderProbability': genderProbability,
};
}
Step 5: State Management (Provider Example)
Create state management to handle verification flow and UI state. Status updates come via backend webhooks, eliminating the need for polling.
Verification Provider
import 'package:flutter/foundation.dart';
import '../models/verification_models.dart';
import '../repositories/verification_repository.dart';
enum VerificationState {
initial,
loading,
success,
error,
}
class VerificationProvider extends ChangeNotifier {
final VerificationRepository _repository;
VerificationProvider({required VerificationRepository repository})
: _repository = repository;
VerificationState _state = VerificationState.initial;
VerificationResponse? _verificationResponse;
VerificationStatusResponse? _statusResponse;
String? _errorMessage;
// Getters
VerificationState get state => _state;
VerificationResponse? get verificationResponse => _verificationResponse;
VerificationStatusResponse? get statusResponse => _statusResponse;
String? get errorMessage => _errorMessage;
// Generate verification ID from backend
Future<void> generateVerification() async {
_setState(VerificationState.loading);
try {
_verificationResponse = await _repository.generateVerification();
_setState(VerificationState.success);
} catch (e) {
_setError('Failed to generate verification: $e');
}
}
// Check verification status from backend
Future<void> checkVerificationStatus(String verificationId) async {
try {
_statusResponse = await _repository.getVerificationStatus(verificationId);
notifyListeners();
} catch (e) {
_setError('Failed to get verification status: $e');
}
}
// Get WebView URL
String? getWebViewUrl() {
return _verificationResponse?.webviewUrl;
}
// Clear state
void clearState() {
_state = VerificationState.initial;
_verificationResponse = null;
_statusResponse = null;
_errorMessage = null;
notifyListeners();
}
void _setState(VerificationState newState) {
_state = newState;
_errorMessage = null;
notifyListeners();
}
void _setError(String error) {
_state = VerificationState.error;
_errorMessage = error;
notifyListeners();
}
}
// Native verification provider for complex flows
class NativeVerificationProvider extends ChangeNotifier {
final VerificationRepository _repository;
NativeVerificationProvider({required VerificationRepository repository})
: _repository = repository;
VerificationState _state = VerificationState.initial;
String? _verificationId;
String? _errorMessage;
int _currentStep = 0;
final List<String> _verificationFlow = ['document', 'liveness', 'form_data'];
// Getters
VerificationState get state => _state;
String? get verificationId => _verificationId;
String? get errorMessage => _errorMessage;
int get currentStep => _currentStep;
List<String> get verificationFlow => _verificationFlow;
String get currentStepName => _verificationFlow[_currentStep];
Future<void> generateAndStartVerification() async {
_setState(VerificationState.loading);
try {
final response = await _repository.generateVerification();
_verificationId = response.verificationId;
await _repository.startNativeVerification(_verificationId!);
_setState(VerificationState.success);
} catch (e) {
_setError('Failed to start verification: $e');
}
}
Future<void> uploadDocument(File documentFile) async {
if (_verificationId == null) {
_setError('No verification ID available');
return;
}
_setState(VerificationState.loading);
try {
await _repository.uploadDocument(_verificationId!, documentFile);
_nextStep();
_setState(VerificationState.success);
} catch (e) {
_setError('Failed to upload document: $e');
}
}
Future<void> uploadSelfie(File selfieFile, {BiometricData? biometricData}) async {
if (_verificationId == null) {
_setError('No verification ID available');
return;
}
_setState(VerificationState.loading);
try {
await _repository.uploadSelfie(
_verificationId!,
selfieFile,
biometricData: biometricData?.toJson(),
);
_nextStep();
_setState(VerificationState.success);
} catch (e) {
_setError('Failed to upload selfie: $e');
}
}
Future<void> submitFormData(Map<String, dynamic> formData) async {
if (_verificationId == null) {
_setError('No verification ID available');
return;
}
_setState(VerificationState.loading);
try {
await _repository.submitFormData(_verificationId!, formData);
_nextStep();
_setState(VerificationState.success);
} catch (e) {
_setError('Failed to submit form data: $e');
}
}
Future<void> validateVerification() async {
if (_verificationId == null) {
_setError('No verification ID available');
return;
}
_setState(VerificationState.loading);
try {
await _repository.validateVerification(_verificationId!);
_setState(VerificationState.success);
} catch (e) {
_setError('Failed to validate verification: $e');
}
}
Future<void> acceptTerms() async {
if (_verificationId == null) {
_setError('No verification ID available');
return;
}
_setState(VerificationState.loading);
try {
await _repository.acceptTerms(_verificationId!);
_setState(VerificationState.success);
} catch (e) {
_setError('Failed to accept terms: $e');
}
}
void _nextStep() {
if (_currentStep < _verificationFlow.length - 1) {
_currentStep++;
}
}
void _setState(VerificationState newState) {
_state = newState;
_errorMessage = null;
notifyListeners();
}
void _setError(String error) {
_state = VerificationState.error;
_errorMessage = error;
notifyListeners();
}
void reset() {
_state = VerificationState.initial;
_verificationId = null;
_errorMessage = null;
_currentStep = 0;
notifyListeners();
}
}
Step 6: UI Implementation (Two Approaches)
Choose between WebView integration (recommended for quick implementation) or native implementation following the v2Client API flow (for full control).
Option A: WebView Integration (Recommended)
✅ Quick to implement
✅ Consistent with web experience
✅ Automatic updates
✅ All modules supported
Option B: Native Implementation
✅ Full UI control
✅ Better performance
✅ Custom branding
✅ All modules supported via API
Complete API Support for Native Implementation
v2Client provides full API support for native mobile implementation across all verification modules.
Available APIs: Document upload, liveness/selfie upload, form data submission, status checking, image retrieval
Option A: WebView Integration Screen
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../providers/verification_provider.dart';
import '../screens/verification_results_screen.dart';
class VerificationWebViewScreen extends StatefulWidget {
const VerificationWebViewScreen({Key? key}) : super(key: key);
State<VerificationWebViewScreen> createState() => _VerificationWebViewScreenState();
}
class _VerificationWebViewScreenState extends State<VerificationWebViewScreen> {
late final WebViewController _controller;
bool _isLoading = true;
void initState() {
super.initState();
_initializeWebView();
// Generate verification when screen loads
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<VerificationProvider>().generateVerification();
});
}
void _initializeWebView() {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) {
setState(() {
_isLoading = true;
});
},
onPageFinished: (String url) {
setState(() {
_isLoading = false;
});
},
onWebResourceError: (WebResourceError error) {
_showError('WebView error: ${error.description}');
},
),
)
..addJavaScriptChannel(
'FlutterApp',
onMessageReceived: (JavaScriptMessage message) {
_handleJavaScriptMessage(message.message);
},
);
}
void _handleJavaScriptMessage(String message) {
// Handle messages from WebView
if (message.startsWith('verification_complete:')) {
final verificationId = message.split(':')[1];
_onVerificationComplete(verificationId);
} else if (message.startsWith('verification_error:')) {
final error = message.split(':')[1];
_showError('Verification error: $error');
}
}
void _onVerificationComplete(String verificationId) {
// Verification completed in WebView
// Backend will receive webhook notification
_showSuccess('Verification submitted for processing');
// Navigate to results screen
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => VerificationResultsScreen(verificationId: verificationId),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Identity Verification'),
backgroundColor: const Color(0xFF4ECDC4),
foregroundColor: Colors.white,
),
body: Consumer<VerificationProvider>(
builder: (context, provider, child) {
if (provider.state == VerificationState.loading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Color(0xFF4ECDC4),
),
SizedBox(height: 16),
Text('Preparing verification...'),
],
),
);
}
if (provider.state == VerificationState.error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
provider.errorMessage ?? 'An error occurred',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
provider.generateVerification();
},
child: const Text('Retry'),
),
],
),
);
}
if (provider.state == VerificationState.success && provider.getWebViewUrl() != null) {
return Stack(
children: [
WebViewWidget(
controller: _controller..loadRequest(
Uri.parse(provider.getWebViewUrl()!),
),
),
if (_isLoading)
const Center(
child: CircularProgressIndicator(
color: Color(0xFF4ECDC4),
),
),
],
);
}
return const Center(
child: Text('Initializing verification...'),
);
},
),
);
}
void _showSuccess(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green,
),
);
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
}
Option B: Native API Implementation Screen
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:camera/camera.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import '../providers/verification_provider.dart';
import '../screens/verification_results_screen.dart';
class NativeVerificationScreen extends StatefulWidget {
const NativeVerificationScreen({Key? key}) : super(key: key);
State<NativeVerificationScreen> createState() => _NativeVerificationScreenState();
}
class _NativeVerificationScreenState extends State<NativeVerificationScreen> {
final ImagePicker _picker = ImagePicker();
final _formKey = GlobalKey<FormState>();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _dobController = TextEditingController();
File? _documentImage;
File? _selfieImage;
void initState() {
super.initState();
// Generate and start verification when screen loads
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<NativeVerificationProvider>().generateAndStartVerification();
});
}
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_dobController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Identity Verification'),
backgroundColor: const Color(0xFF4ECDC4),
foregroundColor: Colors.white,
),
body: Consumer<NativeVerificationProvider>(
builder: (context, provider, child) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Status indicator
_buildStatusIndicator(provider),
const SizedBox(height: 24),
// Content based on current step
Expanded(
child: _buildStepContent(provider),
),
// Error display
if (provider.state == VerificationState.error)
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: Colors.red.shade50,
border: Border.all(color: Colors.red.shade200),
borderRadius: BorderRadius.circular(8),
),
child: Text(
provider.errorMessage ?? 'An error occurred',
style: TextStyle(color: Colors.red.shade800),
),
),
],
),
);
},
),
);
}
Widget _buildStatusIndicator(NativeVerificationProvider provider) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
if (provider.state == VerificationState.loading)
const LinearProgressIndicator(
color: Color(0xFF4ECDC4),
),
const SizedBox(height: 8),
Text(
_getStatusText(provider),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
String _getStatusText(NativeVerificationProvider provider) {
if (provider.state == VerificationState.loading) {
switch (provider.currentStep) {
case 0:
return 'Please capture your document';
case 1:
return 'Please capture your selfie';
case 2:
return 'Please fill the form';
default:
return 'Processing verification...';
}
}
switch (provider.currentStep) {
case 0:
return 'Step 1: Document Capture';
case 1:
return 'Step 2: Liveness Check';
case 2:
return 'Step 3: Form Data';
default:
return 'Verification Complete';
}
}
Widget _buildStepContent(NativeVerificationProvider provider) {
if (provider.state == VerificationState.initial ||
(provider.state == VerificationState.loading && provider.verificationId == null)) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Color(0xFF4ECDC4)),
SizedBox(height: 16),
Text('Initializing verification...'),
],
),
);
}
switch (provider.currentStep) {
case 0:
return _buildDocumentCaptureStep(provider);
case 1:
return _buildSelfieCaptureStep(provider);
case 2:
return _buildFormDataStep(provider);
default:
return _buildCompletionStep(provider);
}
}
Widget _buildDocumentCaptureStep(NativeVerificationProvider provider) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.credit_card,
size: 80,
color: Color(0xFF4ECDC4),
),
const SizedBox(height: 24),
const Text(
'Document Verification',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text(
'Please capture a clear photo of your government-issued ID',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 32),
if (_documentImage != null) ...[
Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Image.file(_documentImage!, fit: BoxFit.cover),
),
const SizedBox(height: 16),
],
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: provider.state == VerificationState.loading
? null
: () => _captureDocument(),
icon: const Icon(Icons.camera_alt),
label: Text(_documentImage == null ? 'Capture Document' : 'Retake Photo'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4ECDC4),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
if (_documentImage != null) ...[
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: provider.state == VerificationState.loading
? null
: () => provider.uploadDocument(_documentImage!),
child: provider.state == VerificationState.loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('Upload Document'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
],
],
);
}
Widget _buildSelfieCaptureStep(NativeVerificationProvider provider) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.face,
size: 80,
color: Color(0xFF4ECDC4),
),
const SizedBox(height: 24),
const Text(
'Liveness Verification',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text(
'Please capture a clear selfie for liveness verification',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 32),
if (_selfieImage != null) ...[
Container(
height: 200,
width: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(100),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(100),
child: Image.file(_selfieImage!, fit: BoxFit.cover),
),
),
const SizedBox(height: 16),
],
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: provider.state == VerificationState.loading
? null
: () => _captureSelfie(),
icon: const Icon(Icons.camera_alt),
label: Text(_selfieImage == null ? 'Capture Selfie' : 'Retake Photo'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4ECDC4),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
if (_selfieImage != null) ...[
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: provider.state == VerificationState.loading
? null
: () => provider.uploadSelfie(_selfieImage!),
child: provider.state == VerificationState.loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('Upload Selfie'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
],
],
);
}
Widget _buildFormDataStep(NativeVerificationProvider provider) {
return SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Personal Information',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text(
'Please verify your personal information',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 32),
TextFormField(
controller: _firstNameController,
decoration: const InputDecoration(
labelText: 'First Name',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your first name';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _lastNameController,
decoration: const InputDecoration(
labelText: 'Last Name',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your last name';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _dobController,
decoration: const InputDecoration(
labelText: 'Date of Birth (YYYY-MM-DD)',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your date of birth';
}
return null;
},
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: provider.state == VerificationState.loading
? null
: () => _submitFormData(provider),
child: provider.state == VerificationState.loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('Submit Information'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4ECDC4),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
],
),
),
);
}
Widget _buildCompletionStep(NativeVerificationProvider provider) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.check_circle_outline,
size: 80,
color: Colors.green,
),
const SizedBox(height: 24),
const Text(
'Verification Complete!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
const Text(
'Your verification has been submitted successfully. You will receive the results shortly.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => _completeVerification(provider),
child: const Text('View Results'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4ECDC4),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
],
);
}
Future<void> _captureDocument() async {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (image != null) {
setState(() {
_documentImage = File(image.path);
});
}
}
Future<void> _captureSelfie() async {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
preferredCameraDevice: CameraDevice.front,
imageQuality: 80,
);
if (image != null) {
setState(() {
_selfieImage = File(image.path);
});
}
}
void _submitFormData(NativeVerificationProvider provider) {
if (_formKey.currentState!.validate()) {
final formData = {
'first_name': _firstNameController.text,
'last_name': _lastNameController.text,
'date_of_birth': _dobController.text,
};
provider.submitFormData(formData).then((_) {
// After form submission, validate and accept terms
provider.validateVerification().then((_) {
provider.acceptTerms();
});
});
}
}
void _completeVerification(NativeVerificationProvider provider) {
if (provider.verificationId != null) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => VerificationResultsScreen(
verificationId: provider.verificationId!,
),
),
);
}
}
}
Step 7: Verification Results Display
Create a results screen to display verification status retrieved from your backend.
Results Screen
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/verification_provider.dart';
import '../models/verification_models.dart';
class VerificationResultsScreen extends StatefulWidget {
final String verificationId;
const VerificationResultsScreen({
Key? key,
required this.verificationId,
}) : super(key: key);
State<VerificationResultsScreen> createState() => _VerificationResultsScreenState();
}
class _VerificationResultsScreenState extends State<VerificationResultsScreen> {
void initState() {
super.initState();
// Load verification status when screen loads
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadVerificationData();
});
}
void _loadVerificationData() {
context.read<VerificationProvider>().checkVerificationStatus(widget.verificationId);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Verification Results'),
backgroundColor: const Color(0xFF4ECDC4),
foregroundColor: Colors.white,
actions: [
IconButton(
onPressed: _loadVerificationData,
icon: const Icon(Icons.refresh),
),
],
),
body: Consumer<VerificationProvider>(
builder: (context, provider, child) {
if (provider.statusResponse == null) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Color(0xFF4ECDC4)),
SizedBox(height: 16),
Text('Loading verification status...'),
],
),
);
}
return RefreshIndicator(
onRefresh: () async => _loadVerificationData(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStatusCard(provider.statusResponse!),
const SizedBox(height: 16),
if (provider.statusResponse!.results != null)
_buildResultsCard(provider.statusResponse!.results!),
const SizedBox(height: 16),
_buildActionButtons(provider.statusResponse!),
],
),
),
);
},
),
);
}
Widget _buildStatusCard(VerificationStatusResponse status) {
Color statusColor;
IconData statusIcon;
switch (status.status) {
case 'verified':
statusColor = Colors.green;
statusIcon = Icons.check_circle;
break;
case 'rejected':
statusColor = Colors.red;
statusIcon = Icons.error;
break;
case 'in_progress':
statusColor = Colors.orange;
statusIcon = Icons.hourglass_empty;
break;
case 'pending':
statusColor = Colors.blue;
statusIcon = Icons.pending;
break;
default:
statusColor = Colors.grey;
statusIcon = Icons.help;
}
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(statusIcon, color: statusColor, size: 32),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
status.status.toUpperCase(),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
Text(
'ID: ${status.verificationId}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
],
),
const SizedBox(height: 12),
Divider(color: Colors.grey[300]),
const SizedBox(height: 12),
Row(
children: [
Icon(Icons.access_time, size: 16, color: Colors.grey[600]),
const SizedBox(width: 8),
Text(
'Updated: ${_formatDate(status.updatedAt)}',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
const SizedBox(height: 8),
_buildStatusMessage(status.status),
],
),
),
);
}
Widget _buildStatusMessage(String status) {
String message;
Color messageColor;
switch (status) {
case 'verified':
message = 'Your identity has been successfully verified.';
messageColor = Colors.green;
break;
case 'rejected':
message = 'Verification was not successful. Please try again.';
messageColor = Colors.red;
break;
case 'in_progress':
message = 'Your verification is being processed. Please wait.';
messageColor = Colors.orange;
break;
case 'pending':
message = 'Verification is pending. Processing will begin shortly.';
messageColor = Colors.blue;
break;
default:
message = 'Unknown status. Please check back later.';
messageColor = Colors.grey;
}
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: messageColor.withOpacity(0.1),
border: Border.all(color: messageColor.withOpacity(0.3)),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.info_outline,
color: messageColor,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
message,
style: TextStyle(
color: messageColor.shade800,
fontSize: 14,
),
),
),
],
),
);
}
Widget _buildResultsCard(VerificationResults results) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Verification Details',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (results.document != null) ...[
_buildResultItem(
'Document Verification',
results.document!.status,
'Confidence: ${(results.document!.confidence * 100).toInt()}%',
),
const SizedBox(height: 12),
],
if (results.liveness != null) ...[
_buildResultItem(
'Liveness Check',
results.liveness!.status,
'Confidence: ${(results.liveness!.confidence * 100).toInt()}%',
),
const SizedBox(height: 12),
],
if (results.formData != null) ...[
_buildResultItem(
'Form Data',
results.formData!.status,
'Information verified',
),
],
],
),
),
);
}
Widget _buildResultItem(String title, String status, String subtitle) {
Color statusColor = status == 'completed' ? Colors.green :
status == 'failed' ? Colors.red : Colors.orange;
return Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Text(
subtitle,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
],
);
}
Widget _buildActionButtons(VerificationStatusResponse status) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (status.status == 'in_progress' || status.status == 'pending') ...[
ElevatedButton.icon(
onPressed: _loadVerificationData,
icon: const Icon(Icons.refresh),
label: const Text('Refresh Status'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4ECDC4),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
],
if (status.status == 'rejected') ...[
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
// Navigate back to start new verification
},
icon: const Icon(Icons.refresh),
label: const Text('Start New Verification'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
],
if (status.status == 'verified') ...[
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
// Navigate to main app
},
icon: const Icon(Icons.check),
label: const Text('Continue'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
],
],
);
}
String _formatDate(String dateString) {
try {
final date = DateTime.parse(dateString);
return '${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
} catch (e) {
return dateString;
}
}
}
Security Best Practices & Tips
✅ Security Do's
- Store API keys only on your backend server
- Use HTTPS for all network communications
- Validate user ownership of verifications
- Implement proper authentication on your backend
- Use certificate pinning for production
❌ Security Don'ts
- Never store API keys in the mobile app
- Don't trust verification IDs without validation
- Don't cache sensitive images permanently
- Don't skip certificate validation
- Don't expose sensitive data in logs
Flutter-Specific Tips
- • Use provider/bloc pattern for state management
- • Implement proper error handling with try-catch blocks
- • Use image compression for better performance
- • Handle platform permissions properly (iOS/Android)
- • Implement loading states and progress indicators
- • Use secure storage for sensitive local data
Quick Start Summary
Backend Setup
Generate verification IDs and handle webhooks
Flutter Integration
WebView or native API flow with v2Client
UI Implementation
Choose WebView or native (both fully supported)
Webhook-driven architecture: Your backend generates verification IDs and receives completion notifications via webhooks. WebView offers fastest implementation, while native provides complete control following the API flow: start-verification → upload-document → upload-selfie → validate-verification → accept-terms.