Mobile Development

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

Your Backendv2Client(Generate ID)
Your BackendFlutter App(Pass verification_id)
Flutter Appv2Client(WebView or Native APIs)
v2ClientYour Backend(Webhook results)

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

1

Backend Setup

Generate verification IDs and handle webhooks

2

Flutter Integration

WebView or native API flow with v2Client

3

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.