passkey_ui_kit 0.1.0
passkey_ui_kit: ^0.1.0 copied to clipboard
Drop-in, themeable UI screens and widgets for passkey (passwordless, biometric) authentication in Flutter. Built on top of the open-source passkeys plugin.
import 'package:flutter/material.dart';
import 'package:passkey_ui_kit/passkey_ui_kit.dart';
import 'package:passkeys/authenticator.dart';
import 'demo_authenticator.dart';
import 'mock_passkey_backend.dart';
void main() => runApp(const PasskeyDemoApp());
/// A single shared in-memory backend so the demo screens see the same passkeys.
final MockPasskeyBackend demoBackend = MockPasskeyBackend();
/// A fake authenticator so the demo runs without biometric hardware. In a real
/// app you'd omit this entirely and let the widgets use the real platform one.
final PasskeyAuthenticator demoAuthenticator = DemoPasskeyAuthenticator();
class PasskeyDemoApp extends StatelessWidget {
const PasskeyDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'passkey_ui_kit demo',
theme: ThemeData(
colorSchemeSeed: Colors.indigo,
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
void _snack(BuildContext context, String message) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('passkey_ui_kit demo')),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
Text(
'This demo uses an in-memory backend and a fake authenticator, so '
'it runs anywhere with no server and no biometrics.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 24),
// 1. Onboarding intro screen.
_DemoTile(
title: 'Onboarding intro screen',
subtitle: 'PasskeyIntroScreen',
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => PasskeyIntroScreen(
backend: demoBackend,
username: 'ada@example.com',
authenticator: demoAuthenticator,
onComplete: () {
Navigator.of(context).pop();
_snack(context, 'Passkey set up 🎉');
},
onSkip: () => Navigator.of(context).pop(),
),
),
),
),
// 2. Sign-in button + fallback.
_DemoTile(
title: 'Sign-in button + fallback',
subtitle: 'PasskeySignInButton / PasskeyFallbackFlow',
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const SignInDemo()),
),
),
// 3. Manage passkeys.
_DemoTile(
title: 'Manage passkeys',
subtitle: 'PasskeyManagementScreen',
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => PasskeyManagementScreen(
backend: demoBackend,
username: 'ada@example.com',
authenticator: demoAuthenticator,
),
),
),
),
],
),
);
}
}
/// Demonstrates a login screen: a passkey button with a password form fallback.
class SignInDemo extends StatelessWidget {
const SignInDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sign in')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: PasskeyFallbackFlow(
authenticator: demoAuthenticator,
showFallbackAlways: true,
// `checkAvailability: false` on the button below forces it to show
// in the simulator; drop it in a real app.
passkeyWidget: PasskeySignInButton(
backend: demoBackend,
authenticator: demoAuthenticator,
checkAvailability: false,
onSuccess: (session) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Signed in! token=${session.token}')),
);
},
onError: (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(e.userMessage)));
},
),
fallbackWidget: const _PasswordFormPlaceholder(),
),
),
),
);
}
}
class _PasswordFormPlaceholder extends StatelessWidget {
const _PasswordFormPlaceholder();
@override
Widget build(BuildContext context) {
return Column(
children: [
const TextField(
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
const TextField(
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: FilledButton.tonal(
onPressed: () {},
child: const Text('Sign in with password'),
),
),
],
);
}
}
class _DemoTile extends StatelessWidget {
const _DemoTile({
required this.title,
required this.subtitle,
required this.onTap,
});
final String title;
final String subtitle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
),
);
}
}