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
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
buildscriptblock below toandroid/build.gradle.
Groovy β android/build.gradle
Kotlin DSL β android/build.gradle.kts
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:
- Open
ios/Runner.xcworkspacein Xcode. - Select the Runner target β Signing & Capabilities tab.
- Click + Capability and add Near Field Communication Tag Reading.
This automatically creates (or updates) ios/Runner/Runner.entitlements:
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-identifierswith the three AIDs is inInfo.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.entitlementscontainscom.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.devSkaletekEnvironment.prodSkaletekEnvironment.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
environmentparameter, it defaults toSkaletekEnvironment.dev.
Note:
- The environment parameter is available in the
KYCConfigand 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.