Skip to content

Skaletek KYC Flutter Plugin

A comprehensive Flutter plugin for Know Your Customer (KYC) verification services, featuring document scanning, face liveness detection, and identity verification powered by AWS Amplify.

✨ Features

  • πŸ†” Document Verification: Passport, National ID, Driver's License, and more
  • πŸ‘€ Face Liveness Detection: Real-time biometric verification using AWS Amplify
  • πŸ“Έ Camera Integration: Live document capture with auto-detection
  • 🎨 Customizable UI: Branded verification experience
  • πŸ”’ Secure: Enterprise-grade security with AWS infrastructure
  • πŸ“± Cross-platform: iOS and Android support

πŸš€ Quick Start

1. Installation

dependencies:
  skaletek_kyc: ^0.0.24
flutter pub get

2. Platform Setup

πŸ“± Android Setup

Requires Kotlin 2.2.0.

Step 1: Update Project Build Configuration

1.1. Kotlin Version (Project Level)

The face liveness UI requires Kotlin 2.2.0 or higher. Where you set it depends on how the project was created β€” check android/settings.gradle(.kts) for a plugins { } block containing version numbers:

  • Present β€” set the Kotlin version there. Projects created with recent Flutter versions already declare 2.3.20 and need no change.
  • Absent β€” add the buildscript block below to android/build.gradle.
Groovy β€” android/build.gradle
buildscript {
    ext.kotlin_version = '2.2.0'

    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:8.9.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
Kotlin DSL β€” android/build.gradle.kts
buildscript {
    extra.apply {
        set("kotlin_version", "2.2.0")
    }

    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath("com.android.tools.build:gradle:8.9.1")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0")
    }
}

1.2. Update android/app/build.gradle (App Level)

Groovy β€” android/app/build.gradle
// Skaletek KYC setup
def userHome = System.getProperty('user.home')
def localAppData = System.getenv('LOCALAPPDATA') ?: "$userHome/AppData/Local"
def pubCacheCandidates = [
    "$userHome/.pub-cache/hosted/pub.dev",
    "$localAppData/Pub/Cache/hosted/pub.dev"
]

def setupScript = null
pubCacheCandidates.each { path ->
    if (setupScript != null) return
    def dir = file(path)
    if (!dir.exists()) return
    def pluginDir = dir.listFiles()?.find { it.name.startsWith('skaletek_kyc-') }
    if (pluginDir != null) {
        def candidate = file("${pluginDir.absolutePath}/android/skaletek_kyc.gradle")
        if (candidate.exists()) {
            setupScript = candidate
        }
    }
}

if (setupScript != null) {
    apply from: setupScript
}
Kotlin DSL β€” android/app/build.gradle.kts
apply {
    val userHome = System.getProperty("user.home")
    val localAppData = System.getenv("LOCALAPPDATA") ?: "$userHome/AppData/Local"
    val pubCacheDirs = listOf("$userHome/.pub-cache/hosted/pub.dev", "$localAppData/Pub/Cache/hosted/pub.dev")

    pubCacheDirs.map { file(it) }.find { it.exists() }?.listFiles()
        ?.find { it.name.startsWith("skaletek_kyc-") }
        ?.let { file("${it.absolutePath}/android/skaletek_kyc.gradle") }
        ?.takeIf { it.exists() }?.let { from(it) }
}

Step 2: Permissions (Automatic)

INTERNET, CAMERA and NFC are declared in the SDK's manifest and merge into your app. Nothing to add unless you customise manifest merging.

Step 3: Update MainActivity

Ensure your MainActivity extends FlutterFragmentActivity:

// android/app/src/main/kotlin/com/yourpackage/yourapp/MainActivity.kt
package com.yourpackage.yourapp

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()

🍎 iOS Setup

Step 1: Deployment Target

The face liveness SDK requires iOS 14.0. In Xcode, select the Runner target β†’ General β†’ Minimum Deployments β†’ iOS 14.0. Builds below this fail with requires minimum platform version 14.0.

Step 2: Permissions

Add to ios/Runner/Info.plist. iOS terminates the app without them:

<key>NSCameraUsageDescription</key>
<string>This app needs camera access for document scanning and face verification.</string>
<key>NFCReaderUsageDescription</key>
<string>This app uses NFC to read your passport chip for identity verification.</string>

Step 3: NFC Setup

Pass enableNfc: false on KYCCustomization to use document upload only. When true (default), Passport and National ID flows show the NFC option on supported devices.

iOS (NFC)

NFCReaderUsageDescription is added in Step 2. The capability must be enabled once in Xcode:

  1. Open ios/Runner.xcworkspace in Xcode.
  2. Select the Runner target β†’ Signing & Capabilities tab.
  3. Click + Capability and add Near Field Communication Tag Reading.

This automatically creates (or updates) ios/Runner/Runner.entitlements:

<key>com.apple.developer.nfc.readersession.formats</key>
<array>
    <string>TAG</string>
</array>

Note: NFC capability requires an Apple Developer account and a real device β€” NFC is not available in the iOS Simulator.

3. Add ISO 7816 application identifiers (required for e-passport reading)

This step is critical β€” without it the NFC session can start but immediately time out without detecting the chip.

Add the following to ios/Runner/Info.plist (not the entitlements file):

<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
    <string>A0000002471001</string>
    <string>A0000002472001</string>
    <string>00000000000000</string>
</array>

These are the standard ICAO 9303 Application Identifiers used by e-passports. Place them alongside NFCReaderUsageDescription in Info.plist.

NFC troubleshooting

  • "Session timeout" / chip not detected: Ensure com.apple.developer.nfc.readersession.iso7816.select-identifiers with the three AIDs is in Info.plist (not the entitlements file). This is the most common cause of NFC sessions opening but immediately failing.
  • "Failed to connect to NFC chip": Confirm Near Field Communication Tag Reading capability is added in Xcode under Signing & Capabilities, and that Runner.entitlements contains com.apple.developer.nfc.readersession.formats = [TAG].
  • Authentication failed after the chip is detected: Check that the document number, date of birth, and expiry date match the MRZ exactly. Wrong MRZ key fields can feel like NFC detection failure because the chip rejects BAC/PACE authentication.
  • Android detection is inconsistent: Remove thick or metal cases, place the document on a flat surface, and keep the phone still for up to 25 seconds while the reader finds the chip antenna.
  • NFC only works on physical devices β€” not supported in the iOS Simulator.

πŸ“– API Reference

KYCUserInfo

final userInfo = KYCUserInfo(
  firstName: "John",
  lastName: "Doe",
  documentType: DocumentType.passport.value,
  issuingCountry: "USA",
);

KYCCustomization

final customization = KYCCustomization(
  docSrc: DocumentSource.camera.value,
  partnerName: "Your Company",
  logoUrl: "https://example.com/logo.png", // optional
  primaryColor: Colors.blue, // optional
  enableNfc: true, // optional; false = hide NFC, upload flow only
);

Document Types

Type Description
DocumentType.passport International passport
DocumentType.nationalId National ID card
DocumentType.driverLicense Driver's license
DocumentType.residencePermit Residence permit
DocumentType.healthCard Health/medical card

Document Sources

Source Description
DocumentSource.camera Live camera capture with auto-detection
DocumentSource.file File upload from device gallery

🌐 Environment Configuration

You can now specify the environment for the KYC verification process. This controls which backend endpoints are used for the session.

Supported Environments

  • SkaletekEnvironment.dev
  • SkaletekEnvironment.prod
  • SkaletekEnvironment.sandbox

Usage

SkaletekKYC.instance.startVerification(
  context: context,
  token: "your-token-here",
  userInfo: userInfo,
  customization: customization,
  environment: SkaletekEnvironment.prod, // or .dev, .sandbox
  onResult: (result) {
    // Handle result
  },
);
  • If you do not specify the environment parameter, it defaults to SkaletekEnvironment.dev.

Note:

  • The environment parameter is available in the KYCConfig and is passed through the SDK automatically.
  • The correct API endpoints are selected internally based on the environment you choose.

Verification result (onResult)

The callback receives a typed KYCResult: success (bool), status (KYCStatus?), message and errorCode.

KYCStatus values: success, failure, awaitReview (manual review pending β€” handle separately from failure), cancelled, inProgress, pending, completed, reject.

onComplete, which receives a Map<String, dynamic>, still works but is deprecated and will be removed in 1.0.0.


Complete Example

import 'package:flutter/material.dart';
import 'package:skaletek_kyc/skaletek_kyc.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});
  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

enum _VerificationOutcome { none, success, failure, awaitReview }

class _HomeScreenState extends State<HomeScreen> {
  bool _isVerifying = false;
  String _status = '';
  bool _hasVerificationResult = false;
  _VerificationOutcome _outcome = _VerificationOutcome.none;

  void _startVerification() async {
    setState(() {
      _isVerifying = true;
      _status = 'Starting verification...';
      _hasVerificationResult = false;
      _outcome = _VerificationOutcome.none;
    });

    final userInfo = KYCUserInfo(
      firstName: "Whyte",
      lastName: "Peter",
      documentType: DocumentType.passport.value,
      issuingCountry: "USA",
    );
    final customization = KYCCustomization(
      docSrc: DocumentSource.file.value,
      logoUrl: null,
      partnerName: "Your Company",
      primaryColor: null,
    );

    SkaletekKYC.instance.startVerification(
      context: context,
      token: "your-token-here",
      userInfo: userInfo,
      customization: customization,
      environment: SkaletekEnvironment.dev,
      onResult: (result) {
        setState(() {
          _isVerifying = false;
          _hasVerificationResult = true;
          final statusStr = result.status?.value;
          final message = result.message;
          final errorCode = result.errorCode;

          if (result.success) {
            _outcome = _VerificationOutcome.success;
            _status = 'Verification completed successfully!';
          } else if (statusStr == KYCStatus.awaitReview.value) {
            _outcome = _VerificationOutcome.awaitReview;
            final b = StringBuffer(
              'Under review (status: ${KYCStatus.awaitReview.value})',
            );
            if (errorCode != null && errorCode.isNotEmpty) {
              b.write('\nError code: $errorCode');
            }
            if (message != null && message.isNotEmpty) b.write('\n$message');
            _status = b.toString();
          } else {
            _outcome = _VerificationOutcome.failure;
            final b = StringBuffer(
              'Verification failed (status: ${statusStr ?? 'Unknown'})',
            );
            if (errorCode != null && errorCode.isNotEmpty) {
              b.write('\nError code: $errorCode');
            }
            if (message != null && message.isNotEmpty) {
              b.write('\n$message');
            } else if (errorCode == null || errorCode.isEmpty) {
              b.write('\nNo additional details.');
            }
            _status = b.toString();
          }
        });
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Skaletek KYC'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.verified_user, size: 80, color: Color(0xFF1261C1)),
            const SizedBox(height: 24),
            const Text(
              'Skaletek KYC SDK Demo',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            const Text(
              'This demo shows how to integrate the Skaletek KYC Flutter SDK for identity verification.',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 16, color: Colors.grey),
            ),
            const SizedBox(height: 32),
            if (_isVerifying)
              const Column(
                children: [
                  CircularProgressIndicator(),
                  SizedBox(height: 16),
                  Text('Verification in progress...'),
                ],
              )
            else
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: _startVerification,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFF1261C1),
                    foregroundColor: Colors.white,
                    padding: const EdgeInsets.symmetric(vertical: 16),
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(8),
                    ),
                  ),
                  child: const Text('Start Identity Verification'),
                ),
              ),

            const SizedBox(height: 20),
            if (_hasVerificationResult && _status.isNotEmpty)
              Container(
                width: double.infinity,
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: switch (_outcome) {
                    _VerificationOutcome.success => Colors.green[50],
                    _VerificationOutcome.awaitReview => Colors.amber[50],
                    _ => Colors.red[50],
                  },
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(
                    color: switch (_outcome) {
                      _VerificationOutcome.success => Colors.green,
                      _VerificationOutcome.awaitReview => Colors.amber.shade700,
                      _ => Colors.red,
                    },
                  ),
                ),
                child: Text(
                  _status,
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    color: switch (_outcome) {
                      _VerificationOutcome.success => Colors.green[700],
                      _VerificationOutcome.awaitReview => Colors.amber.shade900,
                      _ => Colors.red[700],
                    },
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

πŸ”§ Troubleshooting

Common Issues

Android build errors:

Error Cause Fix
This version of the Compose Compiler requires Kotlin version … Kotlin version differs from the one the liveness plugin pins Set kotlin_version to 2.2.0
The Kotlin Gradle plugin was loaded multiple times… Same cause, reported as a warning Align Kotlin versions across settings.gradle and build.gradle
Plugin request for plugin already on the classpath must not include a version Versions declared in both settings.gradle and a buildscript block Declare them in one place β€” see Step 1.1
⚠️ Skaletek KYC: amplifyconfiguration.json not found Setup script ran before flutter pub get Run flutter pub get, then rebuild

iOS build errors:

  • Verify the iOS deployment target is 14.0 or higher

Plugin "SmithyCodeGeneratorPlugin" from package "smithy-swift" must be enabled before it can be used means the Amplify Swift packages resolved above the pinned versions. In Xcode, File β†’ Packages β†’ Reset Package Caches, then rebuild.

Face liveness:

  • Verify camera permissions are granted
  • Check network connectivity for AWS services

NFC: see Step 3 for entitlements and provisioning.