flutter_digital_credentials 0.1.0-dev.3
flutter_digital_credentials: ^0.1.0-dev.3 copied to clipboard
Android Credential Manager Digital Credentials verifier bridge for Flutter.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.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',
);
const _firebasePnvTestToken = String.fromEnvironment('FIREBASE_PNV_TEST_TOKEN');
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<void> _requestPhoneNumber() async {
if (_firebasePnvTestToken.isEmpty) {
setState(
() => _status = 'Pass FIREBASE_PNV_TEST_TOKEN with --dart-define before testing phone verification.',
);
return;
}
setState(() {
_isLoading = true;
_status = 'Opening the Android phone-number consent sheet…';
});
try {
final verification =
await _FirebasePhoneVerification.getVerifiedPhoneNumber(
_firebasePnvTestToken,
);
if (!mounted) return;
setState(
() => _status = 'Verifying the signed phone token on the server…',
);
final phoneNumber = await _completePhoneVerification(verification.token);
if (mounted) {
setState(() => _status = 'Verified phone number: $phoneNumber');
}
} on PlatformException catch (error) {
if (mounted) setState(() => _status = error.message ?? error.code);
} 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,
);
}
Future<String> _completePhoneVerification(String token) async {
final response = await http.post(
Uri.parse('$_verifierBaseUrl/phone-verification/complete'),
headers: {'content-type': 'application/json'},
body: jsonEncode({'token': token}),
);
final body = _decodeBody(response);
if (response.statusCode != 200 || body['phoneNumber'] is! String) {
throw _VerifierException(_errorMessage(body));
}
return body['phoneNumber'] 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: 12),
OutlinedButton(
onPressed: _isLoading ? null : _requestPhoneNumber,
child: const Text('Verify phone number (Firebase test mode)'),
),
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;
}
class _FirebasePhoneVerification {
static const _channel = MethodChannel(
'com.wreos.flutter_digital_credentials_example/firebase_pnv',
);
static Future<_PhoneVerification> getVerifiedPhoneNumber(
String testToken,
) async {
final result = await _channel.invokeMapMethod<String, dynamic>(
'getVerifiedPhoneNumber',
{'testToken': testToken},
);
final token = result?['token'];
if (token is! String || token.isEmpty) {
throw PlatformException(
code: 'invalid_phone_verification_response',
message: 'Firebase PNV did not return a verification token.',
);
}
return _PhoneVerification(token: token);
}
}
class _PhoneVerification {
const _PhoneVerification({required this.token});
final String token;
}