> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter SDK

> Flutter Dart SDK for KYC, KYB, biometric liveness, and AML. Native iOS and Android, NFC e-passport reading. Pay-per-call from $0.30, 500 free/month.

export const AgentPromptAccordion = ({prompt, title = "AI Agent Integration Prompt"}) => {
  const [copied, setCopied] = React.useState(false);
  const handleCopy = e => {
    e.stopPropagation();
    if (!prompt) return;
    navigator.clipboard.writeText(prompt.trim()).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };
  const agents = ["Claude Code", "Codex", "Cursor", "Devin", "Windsurf", "GitHub Copilot"];
  return <div className="didit-agent-card">
      {}
      <div className="didit-agent-titlebar">
        <div className="didit-agent-dots" aria-hidden="true">
          <span className="didit-agent-dot didit-agent-dot-red"></span>
          <span className="didit-agent-dot didit-agent-dot-yellow"></span>
          <span className="didit-agent-dot didit-agent-dot-green"></span>
        </div>
        <span className="didit-agent-filename">{title}</span>
        <button type="button" className={`didit-agent-copy ${copied ? "didit-agent-copy-copied" : ""}`} onClick={handleCopy} title="Copy prompt to clipboard" aria-label={copied ? "Copied!" : "Copy prompt to clipboard"}>
          {copied ? <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <path d="M3 8.5l3.5 3.5L13 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
              </svg>
              <span>Copied</span>
            </> : <>
              <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
                <rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
                <path d="M11 5V3.5A1.5 1.5 0 0 0 9.5 2h-6A1.5 1.5 0 0 0 2 3.5v6A1.5 1.5 0 0 0 3.5 11H5" stroke="currentColor" strokeWidth="1.5" />
              </svg>
              <span>Copy</span>
            </>}
        </button>
      </div>

      {}
      <pre className="didit-agent-body"><code>{prompt.trim()}</code></pre>

      {}
      <div className="didit-agent-footer">
        <span className="didit-agent-footer-label">Paste into</span>
        <div className="didit-agent-chips">
          {agents.map(name => <span key={name} className="didit-agent-chip">{name}</span>)}
        </div>
      </div>
    </div>;
};

<AgentPromptAccordion
  title="Flutter SDK Integration Prompt"
  prompt={`Integrate Didit identity verification into my Flutter app using the native SDK.

## Requirements
- Flutter 3.3+, Dart 3.11+
- iOS 13.0+ (NFC requires iOS 15+)
- Android API 23+ (Android 6.0)

## Installation
flutter pub add didit_sdk

iOS — Podfile uses an env var to toggle NFC. Inside the target block:
pod 'DiditSDK', :podspec => 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/main/DiditSDK.podspec'
NFC-enabled (default) requires platform :ios, '15.0'. Use 'DiditSDK/Core' for no-NFC (iOS 13.0+).
Then: cd ios && pod install   (or DIDIT_SDK_IOS_NFC_ENABLED=false pod install)
Info.plist must include NSCameraUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryUsageDescription (required) and NFCReaderUsageDescription if using NFC.

Android — for no-NFC builds add to android/gradle.properties:
diditSdkAndroidNfcEnabled=false
For NFC-enabled (default), add to android/app/build.gradle.kts inside android block:
packaging {
  resources {
      pickFirsts += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
  }
}

## Backend: Create Session (your server)
POST https://verification.didit.me/v3/session/
Headers: { "x-api-key": DIDIT_API_KEY, "Content-Type": "application/json" }
Body: { "workflow_id": DIDIT_WORKFLOW_ID, "vendor_data": "user-id" }
Response: { "session_id": "uuid", "session_token": "short-token", "url": "https://verify.didit.me/session/short-token", "status": "Not Started" }
Send session_token to the Flutter app (never expose DIDIT_API_KEY to the device).

## Integration
import 'package:didit_sdk/sdk_flutter.dart';

// Option A: With session token from your backend (recommended)
final result = await DiditSdk.startVerification('session-token-from-backend');

// Option B: With workflow ID (SDK creates session internally)
final result = await DiditSdk.startVerificationWithWorkflow('your-workflow-id',
vendorData: 'user-123',
);

// Handle result
switch (result) {
case VerificationCompleted(:final session):
if (session.status == VerificationStatus.approved) { /* grant access */ }
case VerificationCancelled():
// User dismissed the verification flow
case VerificationFailed(:final error):
// SDK error — show error.message
}

## Result Statuses (VerificationStatus enum)
- VerificationStatus.approved — user identity verified, grant access
- VerificationStatus.declined — verification failed, show retry or contact support
- VerificationStatus.pending  — needs manual review by your compliance team

## Error Types (VerificationErrorType enum)
sessionExpired, networkError, cameraAccessDenied, notInitialized, apiError, retryBlocked, unknown

## Advanced Session Parameters
contact_details, expected_details, and metadata are only available via the Session Token method (your backend creates the session). The Dart startVerificationWithWorkflow API only accepts workflowId, vendorData, and config.

## Full Verification Results
The SDK only returns the status. Full decision data (document fields, face match scores, AML hits, etc.) arrives via webhook to your backend.
Set up webhooks: https://docs.didit.me/integration/webhooks

## Rate Limits (session creation)
- 600 session-create requests / minute per x-api-key (see https://docs.didit.me/integration/rate-limiting)
- On 429, retry honouring Retry-After / X-RateLimit-* headers

## Environment Variables (backend only — never expose in the app)
- DIDIT_API_KEY — from Didit Console > API & Webhooks
- DIDIT_WORKFLOW_ID — from Didit Console > Workflows

## Docs
- Flutter SDK: https://docs.didit.me/integration/native-sdks/flutter-sdk
- GitHub: https://github.com/didit-protocol/sdk-flutter
- pub.dev: https://pub.dev/packages/didit_sdk
- Webhooks: https://docs.didit.me/integration/webhooks
- API Reference: https://docs.didit.me/api-reference/overview`}
/>

A cross-platform Flutter plugin that wraps the native iOS and Android SDKs, providing a unified Dart API for identity verification.

<Card title="GitHub Repository" icon="github" href="https://github.com/didit-protocol/sdk-flutter">
  View source code and examples on GitHub
</Card>

<Card title="pub.dev Package" icon="box" href="https://pub.dev/packages/didit_sdk">
  didit\_sdk
</Card>

<Note>
  ### **Native SDK**: This is the recommended approach for Flutter apps. The SDK wraps the native iOS and Android SDKs via platform channels for the best user experience, optimized camera handling, and full NFC support.
</Note>

***

## Requirements

| Requirement | Minimum Version |
| ----------- | --------------- |
| Flutter     | 3.3+            |
| Dart        | 3.11+           |

### Platform Requirements

| Platform | Minimum Version | Notes                                   |
| -------- | --------------- | --------------------------------------- |
| iOS      | 13.0+           | NFC passport reading requires iOS 15.0+ |
| Android  | API 23+ (6.0)   | Java 17+                                |

***

## Installation

```bash theme={null}
flutter pub add didit_sdk
```

Or add it manually to your `pubspec.yaml`:

```yaml theme={null}
dependencies:
  didit_sdk: ^3.7.1
```

Then run:

```bash theme={null}
flutter pub get
```

### iOS Setup

The plugin selects the native iOS SDK variant from the `DIDIT_SDK_IOS_NFC_ENABLED` environment variable. Configure your `ios/Podfile` (the DiditSDK pod is not on CocoaPods trunk):

```ruby theme={null}
didit_sdk_ios_nfc_enabled = ENV.fetch('DIDIT_SDK_IOS_NFC_ENABLED', 'true').downcase != 'false'
platform :ios, didit_sdk_ios_nfc_enabled ? '15.0' : '13.0'

didit_sdk_ios_pod = didit_sdk_ios_nfc_enabled ? 'DiditSDK' : 'DiditSDK/Core'
didit_sdk_ios_podspec = 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/main/DiditSDK.podspec'

target 'Runner' do
  use_frameworks!

  pod didit_sdk_ios_pod, :podspec => didit_sdk_ios_podspec

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    target.build_configurations.each do |config|
      config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = didit_sdk_ios_nfc_enabled ? '15.0' : '13.0'
    end
  end
end
```

Install the full SDK with NFC (default):

```bash theme={null}
cd ios
pod install
```

Install the core SDK without NFC (removes `NFCPassportReader`, CoreNFC-linked code, and OpenSSL):

```bash theme={null}
cd ios
DIDIT_SDK_IOS_NFC_ENABLED=false pod install
```

<Note>
  NFC-enabled iOS builds require a deployment target of iOS 15.0+; core-only builds can target iOS 13.0. When switching variants, clean CocoaPods first (`rm -rf Pods Podfile.lock`) so the previous SDK variant is not reused.
</Note>

### Android Setup

By default the plugin depends on the full Android SDK including NFC. To build without NFC, add this to `android/gradle.properties`:

```properties theme={null}
diditSdkAndroidNfcEnabled=false
```

This switches the dependency from `me.didit:didit-sdk` to `me.didit:didit-sdk-core`, removing the NFC reader module and its JMRTD/SCUBA/BouncyCastle dependencies.

When NFC is enabled (default), add this packaging rule to `android/app/build.gradle.kts` to resolve a duplicate metadata file from BouncyCastle:

```kotlin theme={null}
android {
    packaging {
        resources {
            pickFirsts += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
        }
    }
}
```

Without this rule the build fails with a `mergeDebugJavaResource` error. The rule is not needed when `diditSdkAndroidNfcEnabled=false`.

<Note>
  The Android native SDK resolves from a remote GitHub Maven repository (since plugin 3.4.0) — no manual Maven configuration is needed in your app.
</Note>

***

## Permissions

### iOS

Add the following keys to your app's `Info.plist`:

| Permission    | Info.plist Key                        | Description                             | Required     |
| ------------- | ------------------------------------- | --------------------------------------- | ------------ |
| Camera        | `NSCameraUsageDescription`            | Document scanning and face verification | Yes          |
| Microphone    | `NSMicrophoneUsageDescription`        | Video recording for liveness checks     | Yes          |
| Photo Library | `NSPhotoLibraryUsageDescription`      | Upload documents from device gallery    | Yes          |
| NFC           | `NFCReaderUsageDescription`           | Read NFC chips in passports/ID cards    | If using NFC |
| Location      | `NSLocationWhenInUseUsageDescription` | Geolocation for fraud prevention        | Optional     |

<Warning>
  If any required iOS privacy key is missing, iOS terminates the app as soon as the SDK tries to access that protected resource. For example, missing `NSCameraUsageDescription` causes a crash when the user taps the document camera's take photo button.
</Warning>

#### NFC Configuration

To enable NFC reading for passports and ID cards with chips:

1. **Add NFC Capability** in Xcode:
   * Select your target > **Signing & Capabilities** > **+ Capability** > **Near Field Communication Tag Reading**

2. **Add ISO7816 Identifiers** to `Info.plist`:

```xml theme={null}
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
    <string>D23300000045737445494420763335</string>
    <string>A0000002471001</string>
    <string>A0000002472001</string>
    <string>00000000000000</string>
</array>
```

3. **Add an entitlements file** with NFC tag reading enabled:

```xml theme={null}
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
    <string>TAG</string>
</array>
```

Make sure the app's provisioning profile includes the NFC Tag Reading capability. If the bundle ID is not configured for NFC in your Apple Developer account, Xcode will fail signing with a missing `com.apple.developer.nfc.readersession.formats` entitlement.

This NFC configuration is not needed when `DIDIT_SDK_IOS_NFC_ENABLED=false`.

### Android

The following permissions are declared in the SDK's `AndroidManifest.xml` and merged automatically:

| Permission             | Description                             | Required     |
| ---------------------- | --------------------------------------- | ------------ |
| `INTERNET`             | Network access for API communication    | Yes          |
| `ACCESS_NETWORK_STATE` | Detect network availability             | Yes          |
| `CAMERA`               | Document scanning and face verification | Yes          |
| `NFC`                  | Read NFC chips in passports/ID cards    | If using NFC |

Camera and NFC hardware features are declared as optional (`android:required="false"`), so your app can be installed on devices without these features. When `diditSdkAndroidNfcEnabled=false`, the Android NFC permission and feature are not added by the SDK.

#### Runtime Permissions

The SDK handles Android runtime permission requests automatically. When the user reaches a step that requires camera access:

1. The SDK prompts for camera permission if not already granted
2. If the user **denies** the permission, an error message is displayed with a **Try Again** button
3. If the user **grants** the permission, the verification flow continues

You do not need to request camera permission in your app code before calling `startVerification()` — the SDK manages this internally.

***

## Quick Start

```dart theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

// Start verification with a session token from your backend
final result = await DiditSdk.startVerification('your-session-token');

switch (result) {
  case VerificationCompleted(:final session):
    if (session.status == VerificationStatus.approved) {
      print('Identity verified!');
    }
  case VerificationCancelled():
    print('User cancelled');
  case VerificationFailed(:final error):
    print('Error: ${error.message}');
}
```

***

## Integration Methods

The SDK supports two integration methods:

### Method 1: Session Token (Recommended for Production)

Create a session on your backend using the [Create Verification Session API](/sessions-api/create-session), then pass the token to the SDK:

```dart theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

// Your backend creates a session and returns the token
final sessionToken = await yourBackend.createVerificationSession(userId);

// Pass the token to the SDK
final result = await DiditSdk.startVerification(sessionToken);
```

This approach gives you full control over:

* Associating sessions with your users (`vendor_data`)
* Setting custom metadata
* Configuring callbacks per session

### Method 2: Workflow ID (Simpler Integration)

For simpler integrations, the SDK can create sessions directly using your workflow ID — no backend needed:

```dart theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

final result = await DiditSdk.startVerificationWithWorkflow(
  'your-workflow-id',
  vendorData: 'user-123',
  config: const DiditConfig(loggingEnabled: true),
);
```

<Note>
  Advanced session parameters (`contact_details`, `expected_details`, `metadata`) are **only** supported through the Session Token method, where your backend calls the [Create Session API](/sessions-api/create-session) with full parameter support. Pass the returned `session_token` to `DiditSdk.startVerification()`.
</Note>

***

## Configuration

Customize the SDK behavior by passing a `DiditConfig` object:

```dart theme={null}
final result = await DiditSdk.startVerification(
  'your-session-token',
  config: const DiditConfig(
    languageCode: 'es',       // Force Spanish language
    fontFamily: 'Avenir',     // Custom font
    loggingEnabled: true,     // Enable debug logging
  ),
);
```

For `startVerificationWithWorkflow`, pass `config` as a named parameter:

```dart theme={null}
final result = await DiditSdk.startVerificationWithWorkflow(
  'your-workflow-id',
  vendorData: 'user-123',
  config: const DiditConfig(
    languageCode: 'es',
    loggingEnabled: true,
  ),
);
```

### Configuration Options

| Property         | Type      | Default       | Description                                           |
| ---------------- | --------- | ------------- | ----------------------------------------------------- |
| `languageCode`   | `String?` | Device locale | ISO 639-1 language code (e.g. `"en"`, `"fr"`, `"ar"`) |
| `fontFamily`     | `String?` | System font   | Custom font family name (must be registered natively) |
| `loggingEnabled` | `bool`    | `false`       | Enable SDK debug logging                              |

<Info>
  **Theming & Colors**: Colors, backgrounds, and intro screen settings are configured through your [White Label settings](https://business.didit.me) in the Didit Console, not in the SDK configuration. This ensures consistent branding across all platforms.
</Info>

***

## Language Support

The SDK supports **53 languages**. If no language is specified, the SDK uses the device locale with English as fallback.

```dart theme={null}
// Use device locale (default)
await DiditSdk.startVerification(token);

// Force specific language
await DiditSdk.startVerification(token,
  config: const DiditConfig(languageCode: 'fr'),
);
```

**[View All Supported Languages →](/integration/supported-languages)**

***

## Advanced Session Parameters

Parameters like `contact_details`, `expected_details`, and `metadata` are **only** supported through the Session Token method. Your backend calls [`POST /v3/session/`](/sessions-api/create-session) with full parameter support, then passes the returned `session_token` to the SDK.

```dart theme={null}
// Your backend handles the full session creation:
// POST /v3/session/ with contact_details, expected_details, metadata, etc.
final sessionToken = await yourBackend.createSession(userId);

// The SDK only needs the token
final result = await DiditSdk.startVerification(sessionToken);
```

<Note>
  The Dart `startVerificationWithWorkflow` method only accepts `workflowId`, `vendorData`, and `config`. Pre-3.4.0 versions exposed `contactDetails`, `expectedDetails`, and `metadata` parameters, but these were removed because the Unilink endpoint does not honour them.
</Note>

***

## Handling Results

Both `startVerification` and `startVerificationWithWorkflow` return a `Future<VerificationResult>`. The result is a sealed class — use pattern matching to determine the outcome.

### Result Cases

| Case                    | Description                                                     |
| ----------------------- | --------------------------------------------------------------- |
| `VerificationCompleted` | Verification flow completed (check `session.status` for result) |
| `VerificationCancelled` | User cancelled the verification flow                            |
| `VerificationFailed`    | An error occurred during verification                           |

### SessionData Properties

| Property    | Type                 | Description                          |
| ----------- | -------------------- | ------------------------------------ |
| `sessionId` | `String`             | Unique session identifier            |
| `status`    | `VerificationStatus` | `approved`, `pending`, or `declined` |

### Error Types

The `VerificationErrorType` enum exposes the following cases:

| Error                | Description                                          |
| -------------------- | ---------------------------------------------------- |
| `sessionExpired`     | The session has expired                              |
| `networkError`       | Network connectivity issue                           |
| `cameraAccessDenied` | Camera permission not granted                        |
| `notInitialized`     | SDK not initialized (Android only)                   |
| `apiError`           | API request failed                                   |
| `retryBlocked`       | Maximum retry attempts exceeded for the current step |
| `unknown`            | Other error — inspect `error.message` for details    |

### Complete Result Handling Example

```dart theme={null}
import 'package:didit_sdk/sdk_flutter.dart';

Future<void> verify(String token) async {
  final result = await DiditSdk.startVerification(token);

  switch (result) {
    case VerificationCompleted(:final session):
      switch (session.status) {
        case VerificationStatus.approved:
          print('Approved! Session: ${session.sessionId}');
          // User is verified — grant access
        case VerificationStatus.pending:
          print('Under review. Session: ${session.sessionId}');
          // Show "verification in progress" UI
        case VerificationStatus.declined:
          print('Declined. Session: ${session.sessionId}');
          // Handle declined verification
      }

    case VerificationCancelled(:final session):
      print('User cancelled.');
      if (session != null) {
        print('Session: ${session.sessionId}');
      }
      // Maybe show retry option

    case VerificationFailed(:final error):
      print('Error [${error.type}]: ${error.message}');
      // Handle error — show retry or contact support
  }
}
```

***

## End-to-End Example (Backend Session → Flutter SDK → Webhook)

The production-ready integration: your backend creates the session, your Flutter app receives the `session_token`, and the SDK runs the flow. The final decision is delivered to your backend via webhook — never trust the client-side result alone.

### 1. Backend — create the session

```ts theme={null}
// Node.js / Express example. Run on your server. NEVER ship DIDIT_API_KEY to the device.
import express from "express";

const app = express();
app.use(express.json());

app.post("/api/verification/start", async (req, res) => {
  const response = await fetch("https://verification.didit.me/v3/session/", {
    method: "POST",
    headers: {
      "x-api-key": process.env.DIDIT_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      workflow_id: process.env.DIDIT_WORKFLOW_ID,
      vendor_data: req.body.userId, // your stable user id
    }),
  });

  if (!response.ok) {
    return res.status(500).json({ error: `Didit ${response.status}` });
  }

  const { session_id, session_token } = await response.json();
  // Persist session_id <-> userId so you can match the webhook later.
  res.json({ session_id, session_token });
});
```

### 2. Flutter — exchange and start the SDK

```dart theme={null}
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:didit_sdk/sdk_flutter.dart';

class VerifyButton extends StatefulWidget {
  const VerifyButton({super.key, required this.userId});
  final String userId;

  @override
  State<VerifyButton> createState() => _VerifyButtonState();
}

class _VerifyButtonState extends State<VerifyButton> {
  bool _loading = false;

  Future<void> _verify() async {
    setState(() => _loading = true);
    try {
      final resp = await http.post(
        Uri.parse('https://your-backend.example.com/api/verification/start'),
        headers: const {'Content-Type': 'application/json'},
        body: jsonEncode({'userId': widget.userId}),
      );
      final sessionToken = (jsonDecode(resp.body) as Map)['session_token'] as String;

      final result = await DiditSdk.startVerification(sessionToken);

      switch (result) {
        case VerificationCompleted(:final session):
          if (session.status == VerificationStatus.approved) {
            // Optimistic UI; rely on the webhook delivered to your backend as the source of truth.
          }
        case VerificationCancelled():
          // User dismissed the flow
          break;
        case VerificationFailed(:final error):
          // Show retry — error.type / error.message
          break;
      }
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _loading ? null : _verify,
      child: Text(_loading ? 'Starting...' : 'Verify identity'),
    );
  }
}
```

### 3. Backend — receive the final decision via webhook

```ts theme={null}
app.post("/api/webhooks/didit", express.raw({ type: "application/json" }), (req, res) => {
  // 1. Verify the X-Signature header (see Webhooks docs)
  // 2. Parse the body
  const event = JSON.parse(req.body.toString());
  if (event.webhook_type === "status.updated") {
    // event.session_id, event.status ("Approved" | "Declined" | "In Review" | ...)
    // Look up the user by session_id and update their verification state.
  }
  res.sendStatus(200);
});
```

See the [Webhooks guide](/integration/webhooks) for HMAC signature verification details.

***

## Complete Example

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:didit_sdk/sdk_flutter.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Didit SDK Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1A1A1A)),
        useMaterial3: true,
      ),
      home: const VerificationScreen(),
    );
  }
}

class VerificationScreen extends StatefulWidget {
  const VerificationScreen({super.key});

  @override
  State<VerificationScreen> createState() => _VerificationScreenState();
}

class _VerificationScreenState extends State<VerificationScreen> {
  final _tokenController = TextEditingController();
  bool _loading = false;

  @override
  void dispose() {
    _tokenController.dispose();
    super.dispose();
  }

  Future<void> _startVerification() async {
    final token = _tokenController.text.trim();
    if (token.isEmpty) {
      _showAlert('Error', 'Please enter a session token.');
      return;
    }

    setState(() => _loading = true);

    try {
      final result = await DiditSdk.startVerification(
        token,
        config: const DiditConfig(loggingEnabled: true),
      );

      switch (result) {
        case VerificationCompleted(:final session):
          _showAlert(
            'Verification Complete',
            'Status: ${session.status.name}\nSession: ${session.sessionId}',
          );
        case VerificationCancelled():
          _showAlert('Cancelled', 'The user cancelled the verification.');
        case VerificationFailed(:final error):
          _showAlert('Failed', '${error.type.name}: ${error.message}');
      }
    } catch (e) {
      _showAlert('Error', 'Unexpected error: $e');
    } finally {
      setState(() => _loading = false);
    }
  }

  void _showAlert(String title, String message) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text(title),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              TextField(
                controller: _tokenController,
                decoration: InputDecoration(
                  hintText: 'Enter session token...',
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(8),
                  ),
                ),
                autocorrect: false,
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: _loading ? null : _startVerification,
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF1A1A1A),
                  foregroundColor: Colors.white,
                  padding: const EdgeInsets.symmetric(vertical: 16),
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(8),
                  ),
                ),
                child: _loading
                    ? const SizedBox(
                        height: 20,
                        width: 20,
                        child: CircularProgressIndicator(
                          strokeWidth: 2,
                          color: Colors.white,
                        ),
                      )
                    : const Text(
                        'Start Verification',
                        style: TextStyle(fontWeight: FontWeight.w600),
                      ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

***
