okyc_flutter 0.1.0
okyc_flutter: ^0.1.0 copied to clipboard
OKYC identity verification for Flutter: the hosted KYC/KYB flow in a secure, full-screen WebView with native permissions, file picking and results.
example/lib/main.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:okyc_flutter/okyc_flutter.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'OKYC Flutter example',
theme: ThemeData(colorSchemeSeed: const Color(0xFF3B5BDB), useMaterial3: true),
home: const ExampleHome(),
);
}
}
/// The Android emulator reaches the host machine at 10.0.2.2; the iOS simulator shares the host network.
String get _devHost => Platform.isAndroid ? '10.0.2.2' : 'localhost';
class ExampleHome extends StatefulWidget {
const ExampleHome({super.key});
@override
State<ExampleHome> createState() => _ExampleHomeState();
}
class _ExampleHomeState extends State<ExampleHome> {
final _url = TextEditingController();
final _api = TextEditingController(text: 'http://$_devHost:4000');
final _log = <String>[];
bool _creating = false;
String? _result;
@override
void dispose() {
_url.dispose();
_api.dispose();
super.dispose();
}
Future<void> _createSandboxSession() async {
setState(() => _creating = true);
try {
final url = await SandboxSessions(Uri.parse(_api.text.trim())).create();
_url.text = url;
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not create a session: $e')));
} finally {
if (mounted) setState(() => _creating = false);
}
}
Future<void> _start() async {
setState(() {
_log.clear();
_result = null;
});
final result = await Okyc.start(
context,
OkycConfig(sessionUrl: _url.text.trim()),
onEvent: (e) => setState(() => _log.add('${e.type.name} ${e.payload.isEmpty ? '' : e.payload}')),
);
setState(() {
_result = switch (result) {
OkycCompleted(:final sessionId, :final status) => 'Completed · $status · $sessionId\n'
'Fetch the verified result on your server: GET /v1/sessions/$sessionId',
OkycCancelled(:final sessionId) => 'Cancelled${sessionId == null ? '' : ' · $sessionId'}',
OkycFailed(:final code, :final message) => 'Failed · $code · $message',
};
});
}
@override
Widget build(BuildContext context) {
final t = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('OKYC Flutter example')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(
key: const Key('session-url'),
controller: _url,
decoration: const InputDecoration(
labelText: 'Session URL',
hintText: 'https://verify.okyc.stream/s/ses_…#t=st_…',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.url,
autocorrect: false,
maxLines: 3,
minLines: 1,
),
const SizedBox(height: 12),
FilledButton.icon(
key: const Key('start'),
onPressed: _start,
icon: const Icon(Icons.verified_user_outlined),
label: const Text('Start verification'),
),
const SizedBox(height: 24),
Text('Local development', style: t.textTheme.titleSmall),
const SizedBox(height: 8),
TextField(
controller: _api,
decoration: const InputDecoration(labelText: 'Local API', border: OutlineInputBorder(), isDense: true),
keyboardType: TextInputType.url,
autocorrect: false,
),
const SizedBox(height: 8),
OutlinedButton.icon(
key: const Key('create-sandbox'),
onPressed: _creating ? null : _createSandboxSession,
icon: _creating
? const SizedBox.square(dimension: 16, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.science_outlined),
label: const Text('Create sandbox session'),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'Creates a throwaway sandbox account on the local API and a test session. '
'In production your server creates sessions with its secret key.',
style: t.textTheme.bodySmall?.copyWith(color: t.colorScheme.onSurfaceVariant),
),
),
if (_result != null) ...[
const SizedBox(height: 24),
Text('Result', style: t.textTheme.titleSmall),
const SizedBox(height: 4),
SelectableText(_result!, key: const Key('result')),
],
if (_log.isNotEmpty) ...[
const SizedBox(height: 16),
Text('Events', style: t.textTheme.titleSmall),
const SizedBox(height: 4),
for (final line in _log) Text(line, style: t.textTheme.bodySmall?.copyWith(fontFamily: 'monospace')),
],
],
),
);
}
}
/// Local-development helper: signs up a throwaway dashboard user, creates a tenant and a sandbox session.
/// Never ship this — production apps get the session URL from their own backend.
class SandboxSessions {
SandboxSessions(this.api);
final Uri api;
final _client = HttpClient()..connectionTimeout = const Duration(seconds: 10);
final _cookies = <String, String>{};
Future<String> create() async {
try {
await _post('/api/auth/sign-up/email', {
'email': 'flutter+${DateTime.now().millisecondsSinceEpoch}@example.test',
'password': 'sandbox-${DateTime.now().microsecondsSinceEpoch}',
'name': 'Flutter example',
});
final tenant = await _post('/dash/v1/tenants', {'name': 'Flutter example'});
final workflows = tenant['workflows'] as Map<String, dynamic>;
final session = await _post(
'/dash/v1/test-sessions',
{'workflow_id': workflows['kyc']},
headers: {'x-okyc-tenant': tenant['id'] as String, 'x-okyc-mode': 'test'},
);
var url = Uri.parse(session['url'] as String);
if (url.host == 'localhost' && _devHost != 'localhost') url = url.replace(host: _devHost);
return url.toString();
} finally {
_client.close();
}
}
Future<Map<String, dynamic>> _post(String path, Object body, {Map<String, String> headers = const {}}) async {
final req = await _client.postUrl(api.resolve(path));
req.headers
..contentType = ContentType.json
// The dashboard API only accepts calls from the dashboard origin.
..set('origin', 'http://localhost:5174');
if (_cookies.isNotEmpty) req.headers.set('cookie', _cookies.entries.map((e) => '${e.key}=${e.value}').join('; '));
headers.forEach(req.headers.set);
req.write(jsonEncode(body));
final res = await req.close();
for (final c in res.cookies) {
_cookies[c.name] = c.value;
}
final text = await res.transform(utf8.decoder).join();
if (res.statusCode >= 300) throw HttpException('$path → ${res.statusCode} $text');
return jsonDecode(text) as Map<String, dynamic>;
}
}