apenia_verify 0.0.6
apenia_verify: ^0.0.6 copied to clipboard
Apenia Verify — KYC capture SDK. Native active-liveness selfie + document scan; matching runs server-side in Apenia Verify.
example/lib/main.dart
import 'package:apenia_verify/apenia_verify.dart';
import 'package:apenia_verify/apenia_verify_platform_interface.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'fake_platform.dart';
/// Real native camera capture on Android/iOS; fake capture on web/desktop.
final bool _realCapture = !kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS);
/// Flip to false to run against the real Apenia API instead of the mock.
const bool kUseMockBackend = true;
/// Which outcome the mock Apenia backend returns next (demo control).
String _scenario = 'approved';
http.Client _mockBackend() {
return MockClient((req) async {
// Stand-in for Apenia's ML validation endpoint — log what we'd send.
if (req.url.path == '/v1/ml/validate') {
debugPrint('ML PAYLOAD → ${req.body}');
return http.Response('{"ok":true}', 200);
}
if (req.method == 'PUT') return http.Response('', 200);
if (req.method == 'GET' &&
req.url.path.startsWith('/v1/onboarding/sessions/')) {
await Future<void>.delayed(const Duration(milliseconds: 500));
return http.Response(_scenarioBody(req.url.pathSegments.last), 200,
headers: {'content-type': 'application/json'});
}
return http.Response('{"error":"unexpected ${req.method} ${req.url}"}', 404);
});
}
const _sampleDoc =
'"document":{"type":"NATIONAL_ID","full_name":"Asha N. Mwangi",'
'"id_number":"31245678","date_of_birth":"1994-06-12","sex":"F",'
'"nationality":"Kenyan","expiry_date":"2031-06-11"}';
String _scenarioBody(String sessionId) {
switch (_scenario) {
case 'review':
return '{"session_id":"$sessionId","status":"review","subject_id":"sub_demo",'
'"result":{"face_match":{"score":0.72,"match":true},'
'"liveness":{"score":0.90,"live":true},"voice":{"score":0.70},'
'"identity_confidence":61,$_sampleDoc}}';
case 'failed':
return '{"session_id":"$sessionId","status":"failed","subject_id":null,'
'"result":{"face_match":{"score":0.30,"match":false},'
'"liveness":{"score":0.41,"live":false},"identity_confidence":18}}';
default:
return '{"session_id":"$sessionId","status":"completed","subject_id":"sub_demo",'
'"result":{"face_match":{"score":0.96,"match":true},'
'"liveness":{"score":0.98,"live":true},"voice":{"score":0.93},'
'"identity_confidence":88,$_sampleDoc}}';
}
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (!_realCapture) {
ApeniaVerifyPlatform.instance = FakeApeniaVerifyPlatform();
}
await ApeniaVerify.initialize(
kUseMockBackend ? _demoConfig() : _productionConfig(),
);
runApp(const ExampleApp());
}
/// ── Production wiring ───────────────────────────────────────────────────
/// The only Apenia-specific things the app holds are the base URL and the
/// **publishable** key. Your backend owns the secret (`sk_…`) and is the only
/// party that may call `POST /v1/onboarding/sessions`.
ApeniaConfig _productionConfig() => ApeniaConfig(
baseUrl: 'https://verify.apenia.com',
appKey: 'pk_YOUR_PUBLISHABLE_KEY',
documentCaptureMode: DocumentCaptureMode.auto,
// Optional: also push the on-device analysis to your ML service. It
// already travels inside the upload manifest either way.
analysisEndpoint: '/v1/ml/validate',
sessionProvider: (req) async {
// Call YOUR backend, which in turn calls Apenia with the secret key
// and returns { session_id, upload_url, client_token }.
//
// final json = await myBackend.createSession(
// flow: req.flow, // biometric_kyc | doc_verification | …
// metadata: req.params.toJson(), // user_id, job_id, customer_ref…
// );
// return ApeniaSession.fromJson(json);
throw UnimplementedError('Wire sessionProvider to your backend');
},
);
/// ── Demo wiring ─────────────────────────────────────────────────────────
/// Fakes the session and the API so the whole flow can be exercised with no
/// backend at all. Never ship `httpClient` in production.
ApeniaConfig _demoConfig() => ApeniaConfig(
baseUrl: 'https://mock.apenia',
appKey: 'pk_demo',
documentCaptureMode: DocumentCaptureMode.auto,
analysisEndpoint: '/v1/ml/validate',
sessionProvider: (req) async => const ApeniaSession(
sessionId: 'onb_demo',
uploadUrl: 'https://mock.apenia/uploads/onb_demo',
clientToken: 'cst_demo',
),
httpClient: _mockBackend(),
);
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Apenia Verify',
debugShowCheckedModeBanner: false,
theme: ApeniaTheme.light(),
home: const OnboardingHost(),
);
}
}
/// Hosts the single-entry [ApeniaOnboarding], restarting it after completion
/// so the demo can be run repeatedly.
class OnboardingHost extends StatefulWidget {
const OnboardingHost({super.key});
@override
State<OnboardingHost> createState() => _OnboardingHostState();
}
class _OnboardingHostState extends State<OnboardingHost> {
Key _runKey = UniqueKey();
void _restart() => setState(() => _runKey = UniqueKey());
@override
Widget build(BuildContext context) {
return ApeniaOnboarding(
key: _runKey,
badgeLabel: 'Sandbox',
params: const PartnerParams(userId: 'user-123', jobId: 'job-demo'),
onComplete: (verdict) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Result: ${verdict.status.name}')),
);
_restart();
},
// Demo-only: pick what the mock backend returns.
introActions: [
PopupMenuButton<String>(
tooltip: 'Demo outcome',
icon: const Icon(Icons.tune_rounded),
initialValue: _scenario,
onSelected: (v) => _scenario = v,
itemBuilder: (_) => const [
PopupMenuItem(value: 'approved', child: Text('Outcome: Approved')),
PopupMenuItem(value: 'review', child: Text('Outcome: Needs review')),
PopupMenuItem(value: 'failed', child: Text('Outcome: Failed')),
],
),
],
);
}
}