Getting Started

First Auth

Your first end-to-end sign-in — email/password in under 50 lines.

Build a working sign-in from scratch in one page. You'll have a configured CredentialsProvider, a persisted session, and a reactive StreamBuilder before you reach the end.

This page uses a mocked authorize callback so you can follow along without a real backend. Swap the mock for your actual API call when you're ready.


1. Install

pubspec.yaml
dependencies:
  authyra_flutter: ^0.1.0
flutter pub get

2. Initialize at startup

lib/main.dart
import 'package:authyra_flutter/authyra_flutter.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Authyra.initialize(
    client: AuthyraClient(
      providers: [
        CredentialsProvider.withTokens(
          id: 'email',
          authorize: (creds) async {
            // Replace this mock with your real API call:
            //   final res = await myApi.post('/auth/login', body: creds);
            if (creds?['email'] == 'demo@example.com' &&
                creds?['password'] == 'password') {
              return AuthSignInResult(
                user: AuthUser(id: '1', email: creds!['email'] as String, name: 'Demo User'),
                accessToken:  'demo-access-token',
                refreshToken: 'demo-refresh-token',
                expiresAt:    DateTime.now().add(const Duration(hours: 1)),
              );
            }
            return null; // wrong credentials → throws AuthenticationFailedException
          },
        ),
      ],
      storage: SecureAuthStorage(),
      config: const AuthConfig(autoRefresh: true),
    ),
  );

  runApp(const MyApp());
}

Authyra.initialize restores any previously persisted session, so returning users are authenticated immediately on next launch.


3. React to auth state

Wrap your root widget in a StreamBuilder on authStateChanges:

lib/main.dart (continued)
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: StreamBuilder<AuthState>(
        stream: Authyra.instance.authStateChanges,
        builder: (context, snapshot) {
          final state = snapshot.data ?? AuthState.unauthenticated();
          return switch (state.type) {
            AuthStateType.authenticated   => HomePage(user: state.user!),
            AuthStateType.unauthenticated => const LoginPage(),
            AuthStateType.error           => Scaffold(
                body: Center(child: Text('Error: ${state.error}')),
              ),
          };
        },
      ),
    );
  }
}

The StreamBuilder re-renders whenever the auth state changes — no manual navigation calls needed.


4. Build the login form

lib/pages/login_page.dart
import 'package:authyra_flutter/authyra_flutter.dart';
import 'package:flutter/material.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _email    = TextEditingController();
  final _password = TextEditingController();
  bool    _loading = false;
  String? _error;

  Future<void> _signIn() async {
    setState(() { _loading = true; _error = null; });
    try {
      await Authyra.instance.signIn('email', params: {
        'email':    _email.text.trim(),
        'password': _password.text,
      });
      // StreamBuilder above detects the new AuthState and renders HomePage.
    } on AuthenticationFailedException {
      setState(() => _error = 'Invalid email or password.');
    } catch (_) {
      setState(() => _error = 'Something went wrong. Try again.');
    } finally {
      setState(() => _loading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text('Sign in',
                style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
            const SizedBox(height: 32),
            TextField(
              controller: _email,
              decoration: const InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.emailAddress,
            ),
            const SizedBox(height: 12),
            TextField(
              controller: _password,
              decoration: const InputDecoration(
                labelText: 'Password',
                border: OutlineInputBorder(),
              ),
              obscureText: true,
            ),
            if (_error != null) ...[
              const SizedBox(height: 12),
              Text(_error!, style: const TextStyle(color: Colors.red)),
            ],
            const SizedBox(height: 24),
            FilledButton(
              onPressed: _loading ? null : _signIn,
              child: _loading
                  ? const SizedBox.square(
                      dimension: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Text('Sign in'),
            ),
          ],
        ),
      ),
    );
  }
}

5. Build the home page

lib/pages/home_page.dart
import 'package:authyra_flutter/authyra_flutter.dart';
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  final AuthUser user;
  const HomePage({super.key, required this.user});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hello, ${user.name ?? user.email ?? 'there'}!'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            tooltip: 'Sign out',
            onPressed: () => Authyra.instance.signOut(),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('User ID: ${user.id}'),
            if (user.email != null) Text('Email: ${user.email}'),
          ],
        ),
      ),
    );
  }
}

Authyra.instance.signOut() clears the session and emits AuthState.unauthenticated(). The StreamBuilder re-renders LoginPage automatically.


Try it

Run the app and sign in with:

  • Email: demo@example.com
  • Password: password

Close the app and reopen it — the session is restored from SecureAuthStorage and you land directly on HomePage.


Connect to a real backend

Replace the mock authorize callback with your actual API:

authorize: (creds) async {
  final res = await myApi.post('/auth/login', body: {
    'email':    creds!['email'],
    'password': creds['password'],
  });

  if (res.statusCode == 401) return null;          // wrong credentials
  if (res.statusCode != 200) throw AuthenticationFailedException(
    'Login failed: HTTP ${res.statusCode}',
    providerName: 'email',
  );

  return AuthSignInResult(
    user: AuthUser(
      id:    res.data['id'],
      email: res.data['email'],
      name:  res.data['name'],
    ),
    accessToken:  res.data['accessToken'],
    refreshToken: res.data['refreshToken'],
    expiresAt:    DateTime.parse(res.data['expiresAt']),
  );
},

Returning null causes signIn to throw AuthenticationFailedException. Throw it explicitly for non-credential errors (network, server 5xx).


Next steps

Copyright © 2026