Skip to main content

zuraffa_auth 1.1.1 copy "zuraffa_auth: ^1.1.1" to clipboard
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'),
                ),
              ],
            ),
          ],
        ],
      ),
    );
  }
}
0
likes
160
points
212
downloads

Documentation

API reference

Publisher

verified publisherzuraffa.com

Weekly Downloads

Typed authentication for the Zuraffa ecosystem: pluggable providers behind a pure-Dart port, token auto-refresh, and session persistence via zuraffa_session.

Homepage
Repository (GitHub)
View/report issues

Topics

#authentication #sessions #clean-architecture #zuraffa

License

MIT (license)

Dependencies

json_annotation, zorphy_annotation, zuraffa, zuraffa_session

More

Packages that depend on zuraffa_auth