flutter_digital_credentials 0.1.0-dev.2
flutter_digital_credentials: ^0.1.0-dev.2 copied to clipboard
Android Credential Manager Digital Credentials verifier bridge for Flutter.
example/lib/main.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_digital_credentials/flutter_digital_credentials.dart';
import 'package:http/http.dart' as http;
const _verifierBaseUrl = String.fromEnvironment(
'VERIFIER_BASE_URL',
defaultValue: 'http://127.0.0.1:8787',
);
void main() => runApp(const VerifiedEmailExample());
class VerifiedEmailExample extends StatelessWidget {
const VerifiedEmailExample({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const VerifiedEmailScreen(),
);
}
}
class VerifiedEmailScreen extends StatefulWidget {
const VerifiedEmailScreen({super.key});
@override
State<VerifiedEmailScreen> createState() => _VerifiedEmailScreenState();
}
class _VerifiedEmailScreenState extends State<VerifiedEmailScreen> {
final _credentials = FlutterDigitalCredentials();
String _status = 'Ready to request a verified email.';
bool _isLoading = false;
Future<void> _requestCredential() async {
setState(() {
_isLoading = true;
_status = 'Creating a one-time verification session…';
});
try {
final session = await _createSession();
if (!mounted) return;
setState(() => _status = 'Opening the Android credential selector…');
final presentation = await _credentials.request(
requestJson: session.requestJson,
);
if (!mounted) return;
setState(() => _status = 'Verifying the presentation on the server…');
final result = await _completeSession(
session.id,
presentation.credentialJson,
);
if (!mounted) return;
setState(
() => _status = 'Verified ${result.displayName} (${result.email}).',
);
} on DigitalCredentialException catch (error) {
if (mounted) setState(() => _status = error.message);
} on _VerifierException catch (error) {
if (mounted) setState(() => _status = error.message);
} catch (_) {
if (mounted) {
setState(
() => _status = 'Could not reach the local verifier. Is it running?',
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _requestAgeOver18() async {
setState(() {
_isLoading = true;
_status = 'Creating a one-time 18+ request…';
});
try {
final requestJson = await _createAgeOver18Request();
if (!mounted) return;
setState(() => _status = 'Opening the Android credential selector…');
await _credentials.request(requestJson: requestJson);
if (mounted) {
setState(
() => _status =
'Received an unverified 18+ presentation. No access was granted.',
);
}
} on DigitalCredentialException catch (error) {
if (mounted) setState(() => _status = error.message);
} on _VerifierException catch (error) {
if (mounted) setState(() => _status = error.message);
} catch (_) {
if (mounted) {
setState(
() => _status = 'Could not reach the local verifier. Is it running?',
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
Future<_VerificationSession> _createSession() async {
final response = await http.post(
Uri.parse('$_verifierBaseUrl/verification-sessions'),
);
final body = _decodeBody(response);
if (response.statusCode != 201) {
throw _VerifierException(_errorMessage(body));
}
return _VerificationSession(
id: body['sessionId'] as String,
requestJson: body['requestJson'] as String,
);
}
Future<String> _createAgeOver18Request() async {
final response = await http.post(
Uri.parse('$_verifierBaseUrl/age-over-18-requests'),
);
final body = _decodeBody(response);
if (response.statusCode != 201 || body['requestJson'] is! String) {
throw _VerifierException(_errorMessage(body));
}
return body['requestJson'] as String;
}
Future<_VerifiedEmail> _completeSession(
String sessionId,
String credentialJson,
) async {
final response = await http.post(
Uri.parse('$_verifierBaseUrl/verification-sessions/$sessionId/complete'),
headers: {'content-type': 'application/json'},
body: jsonEncode({'credentialJson': credentialJson}),
);
final body = _decodeBody(response);
if (response.statusCode != 200) {
throw _VerifierException(_errorMessage(body));
}
return _VerifiedEmail(
email: body['email'] as String,
displayName: body['displayName'] as String,
);
}
Map<String, dynamic> _decodeBody(http.Response response) {
final body = jsonDecode(response.body);
if (body is! Map<String, dynamic>) {
throw const _VerifierException('Invalid verifier response.');
}
return body;
}
String _errorMessage(Map<String, dynamic> body) =>
body['error'] as String? ?? 'Verification failed.';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Verified email example')),
body: ListView(
padding: const EdgeInsets.all(24),
children: [
const Text(
'This demo gets a one-time request from its verifier, then sends the raw presentation back for server-side validation.',
),
const SizedBox(height: 16),
FilledButton(
onPressed: _isLoading ? null : _requestCredential,
child: Text(_isLoading ? 'Verifying…' : 'Request verified email'),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: _isLoading ? null : _requestAgeOver18,
child: const Text('Request 18+ proof (mDL wallet required)'),
),
const SizedBox(height: 16),
Text(_status),
const SizedBox(height: 24),
const Text(
'Local verifier: 127.0.0.1:8787 via adb reverse. The demo does not create an account.',
style: TextStyle(fontSize: 12),
),
],
),
);
}
}
class _VerificationSession {
const _VerificationSession({required this.id, required this.requestJson});
final String id;
final String requestJson;
}
class _VerifiedEmail {
const _VerifiedEmail({required this.email, required this.displayName});
final String email;
final String displayName;
}
class _VerifierException implements Exception {
const _VerifierException(this.message);
final String message;
}