zuraffa_auth 1.1.1
zuraffa_auth: ^1.1.1 copied to clipboard
Typed authentication for the Zuraffa ecosystem: pluggable providers behind a pure-Dart port, token auto-refresh, and session persistence via zuraffa_session.
example/lib/main.dart
// The zuraffa_auth example: every plugin operation, wired end to end,
// on android, ios and macos.
//
// The composition root binds [registerAuthDependencies] with a demo
// [AuthProvider] and a PERSISTENT [FileSessionStore] — kill the app and
// re-launch: [AuthService.restore] brings the session back. The panels
// exercise: signIn (invalid-credentials path included), the session
// card (identity + token + expiry), restore, refreshNow (the token
// rotates), and signOut. Errors surface verbatim in the banner.
library;
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:zuraffa_auth/zuraffa_auth.dart';
import 'package:zuraffa_session/zuraffa_session.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// HOME is the app sandbox on iOS/macOS — no path_provider needed (its
// foundation layer drags in the objective_c native-assets hook).
final home = Directory(Platform.environment['HOME'] ?? '.');
registerAuthDependencies(
GetIt.instance,
providers: [DemoAuthProvider()],
// REAL persistence: the session survives app restarts.
sessions: FileSessionStore(home),
);
runApp(ZuraffaAuthExampleApp(
initialService: GetIt.instance<AuthService>(),
));
}
/// A working identity provider: secret `password` signs in, anything
/// else is rejected with the package's invalidCredentials error; the
/// refresh token rotates the access token.
class DemoAuthProvider implements AuthProvider {
@override
String get id => 'demo';
@override
AuthProviderKind get kind => AuthProviderKind.password;
int _issued = 0;
@override
Future<AuthResult> signIn(AuthCredentials credentials) async {
if (credentials.secret != 'password') {
throw AuthException.invalidCredentials(id);
}
_issued++;
return AuthResult(
token: AuthToken(
accessToken: 'demo-access-$_issued',
refreshToken: 'demo-refresh-$_issued',
tokenType: 'Bearer',
expiresAt: DateTime.now().millisecondsSinceEpoch + 3600000,
),
subject: credentials.identifier,
displayName: credentials.identifier,
email: credentials.identifier.contains('@')
? credentials.identifier
: null,
);
}
@override
Future<void> signOut(AuthToken token) async {}
@override
Future<AuthToken> refresh(AuthToken token) async {
if (token.refreshToken == null) {
throw AuthException.refreshFailed(id);
}
_issued++;
return AuthToken(
accessToken: 'demo-access-$_issued',
refreshToken: token.refreshToken,
tokenType: 'Bearer',
expiresAt: DateTime.now().millisecondsSinceEpoch + 3600000,
);
}
}
class ZuraffaAuthExampleApp extends StatelessWidget {
const ZuraffaAuthExampleApp({super.key, this.initialService});
final AuthService? initialService;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'zuraffa_auth example',
theme: ThemeData(colorSchemeSeed: const Color(0xFF7B61FF), useMaterial3: true),
home: AuthPage(service: initialService),
);
}
}
class AuthPage extends StatefulWidget {
const AuthPage({super.key, this.service});
final AuthService? service;
@override
State<AuthPage> createState() => _AuthPageState();
}
class _AuthPageState extends State<AuthPage> {
late AuthService _auth;
AuthSession? _session;
String? _error;
final _identifier = TextEditingController(text: 'ada@example.dev');
final _secret = TextEditingController(text: 'password');
@override
void initState() {
super.initState();
_auth = widget.service ?? GetIt.instance<AuthService>();
_restore();
}
Future<void> _restore() async {
try {
setState(() => _error = null);
final session = await _auth.restore();
setState(() => _session = session);
} on AuthException catch (error) {
setState(() => _error = '${error.code}: ${error.message}');
}
}
Future<void> _signIn() async {
try {
setState(() => _error = null);
final session = await _auth.signIn(
'demo',
AuthCredentials(
identifier: _identifier.text.trim(),
secret: _secret.text,
),
);
setState(() => _session = session);
} on AuthException catch (error) {
setState(() => _error = '${error.code}: ${error.message}');
}
}
Future<void> _refreshNow() async {
try {
setState(() => _error = null);
final session = await _auth.refreshNow();
setState(() => _session = session);
} on AuthException catch (error) {
setState(() => _error = '${error.code}: ${error.message}');
}
}
Future<void> _signOut() async {
try {
setState(() => _error = null);
await _auth.signOut();
setState(() => _session = null);
} on AuthException catch (error) {
setState(() => _error = '${error.code}: ${error.message}');
}
}
@override
void dispose() {
_identifier.dispose();
_secret.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final session = _session;
return Scaffold(
appBar: AppBar(title: const Text('zuraffa_auth')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
if (_error != null)
Card(
color: Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(_error!, key: const Key('error-banner')),
),
),
if (session == null) ...[
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Sign in (provider: demo)',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
TextField(
key: const Key('field-identifier'),
controller: _identifier,
decoration: const InputDecoration(
labelText: 'identifier (email or username)'),
),
TextField(
key: const Key('field-secret'),
controller: _secret,
obscureText: true,
decoration: const InputDecoration(
labelText: "secret (try 'password', or anything else)"),
),
const SizedBox(height: 8),
FilledButton(
key: const Key('sign-in'),
onPressed: _signIn,
child: const Text('signIn'),
),
],
),
),
),
Card(
child: ListTile(
key: const Key('restore'),
leading: const Icon(Icons.restore),
title: const Text('restore'),
subtitle: const Text(
're-read the persisted session (restart the app to see it survive)'),
trailing: IconButton(
icon: const Icon(Icons.refresh), onPressed: _restore),
),
),
] else ...[
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
key: const Key('session-card'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Session',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text('subject: ${session.identity.subject}'),
Text('displayName: ${session.identity.displayName}'),
if (session.identity.email != null)
Text('email: ${session.identity.email}'),
Text('provider: ${session.identity.provider.name}'),
Text('access token: ${session.token.accessToken}'),
Text(
'expires: ${DateTime.fromMillisecondsSinceEpoch(session.token.expiresAt ?? 0)}'),
],
),
),
),
Wrap(
spacing: 8,
children: [
OutlinedButton(
key: const Key('refresh-now'),
onPressed: _refreshNow,
child: const Text('refreshNow'),
),
OutlinedButton(
key: const Key('restore'),
onPressed: _restore,
child: const Text('restore'),
),
FilledButton(
key: const Key('sign-out'),
onPressed: _signOut,
child: const Text('signOut'),
),
],
),
],
],
),
);
}
}