voo_authstack_client 0.1.7 copy "voo_authstack_client: ^0.1.7" to clipboard
voo_authstack_client: ^0.1.7 copied to clipboard

Flutter SDK for Voo AuthStack - a centralized authentication platform. Supports email/password, OAuth providers (Google, GitHub, Microsoft, Apple, Discord), automatic token refresh, and provider linking.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:voo_toast/voo_toast.dart';
import 'package:voo_authstack_client/voo_authstack_client.dart';

// Update this to your Voo AuthStack API URL
const baseUrl = 'https://auth.voostack.com/api';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  VooToastController.init();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Voo AuthStack Demo',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), useMaterial3: true),
    darkTheme: ThemeData(
      colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.dark),
      useMaterial3: true,
    ),
    home: const VooToastOverlay(child: AuthDemoScreen()),
  );
}

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

  @override
  State<AuthDemoScreen> createState() => _AuthDemoScreenState();
}

class _AuthDemoScreenState extends State<AuthDemoScreen> {
  late final VooAuthstackService _authService;
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _firstNameController = TextEditingController();
  final _lastNameController = TextEditingController();

  bool _isLoading = false;
  bool _isRegisterMode = false;
  List<LinkedProvider> _linkedProviders = [];

  @override
  void initState() {
    super.initState();
    _initAuthService();
  }

  Future<void> _initAuthService() async {
    _authService = VooAuthstackService(config: const VooAuthstackConfig(baseUrl: baseUrl));

    // Listen to auth state changes
    _authService.statusStream.listen((AuthStatus status) {
      setState(() {});
      if (status == AuthStatus.authenticated) {
        _loadLinkedProviders();
      }
    });

    // Initialize and check for stored tokens
    await _authService.initialize();
    setState(() {});
  }

  Future<void> _loadLinkedProviders() async {
    try {
      final providers = await _authService.getLinkedProviders();
      setState(() => _linkedProviders = providers);
    } catch (e) {
      // Ignore errors loading providers
    }
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    _firstNameController.dispose();
    _lastNameController.dispose();
    _authService.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final colorScheme = theme.colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Voo AuthStack Demo'),
        centerTitle: true,
        actions: [if (_authService.isAuthenticated) IconButton(icon: const Icon(Icons.logout), onPressed: _logout, tooltip: 'Logout')],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(24),
        child: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 400),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                // Status Card
                _buildStatusCard(colorScheme),
                const SizedBox(height: 24),

                if (_authService.isAuthenticated) ...[
                  // User Info
                  _buildUserCard(colorScheme),
                  const SizedBox(height: 24),

                  // Linked Providers
                  _buildLinkedProvidersCard(colorScheme),
                  const SizedBox(height: 24),

                  // Token Info
                  _buildTokenCard(colorScheme),
                ] else ...[
                  // Login/Register Form
                  _buildAuthForm(colorScheme),
                  const SizedBox(height: 24),

                  // OAuth Providers
                  _buildOAuthSection(colorScheme),
                ],
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildStatusCard(ColorScheme colorScheme) {
    final status = _authService.status;
    final (icon, color, label) = switch (status) {
      AuthStatus.authenticated => (Icons.check_circle, Colors.green, 'Authenticated'),
      AuthStatus.authenticating => (Icons.hourglass_empty, Colors.orange, 'Authenticating...'),
      AuthStatus.refreshing => (Icons.refresh, Colors.blue, 'Refreshing token...'),
      AuthStatus.error => (Icons.error, Colors.red, 'Error'),
      AuthStatus.unauthenticated => (Icons.lock_outline, Colors.grey, 'Not authenticated'),
    };

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Icon(icon, color: color, size: 32),
            const SizedBox(width: 16),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('Auth Status', style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 12)),
                Text(
                  label,
                  style: TextStyle(fontWeight: FontWeight.bold, color: color, fontSize: 18),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildUserCard(ColorScheme colorScheme) {
    final user = _authService.currentUser;
    if (user == null) return const SizedBox.shrink();

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                CircleAvatar(
                  radius: 28,
                  backgroundColor: colorScheme.primaryContainer,
                  backgroundImage: user.avatarUrl != null ? NetworkImage(user.avatarUrl!) : null,
                  child: user.avatarUrl == null
                      ? Text(
                          user.initials,
                          style: TextStyle(color: colorScheme.onPrimaryContainer, fontWeight: FontWeight.bold),
                        )
                      : null,
                ),
                const SizedBox(width: 16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(user.fullName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
                      Text(user.email, style: TextStyle(color: colorScheme.onSurfaceVariant)),
                    ],
                  ),
                ),
              ],
            ),
            const Divider(height: 24),
            _buildInfoRow('User ID', user.id),
            _buildInfoRow('Email Verified', user.emailVerified ? 'Yes' : 'No'),
            _buildInfoRow('Account Status', user.isActive ? 'Active' : 'Inactive'),
          ],
        ),
      ),
    );
  }

  Widget _buildLinkedProvidersCard(ColorScheme colorScheme) => Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              const Icon(Icons.link),
              const SizedBox(width: 8),
              const Text('Linked Providers', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
              const Spacer(),
              IconButton(icon: const Icon(Icons.refresh, size: 20), onPressed: _loadLinkedProviders, tooltip: 'Refresh'),
            ],
          ),
          const Divider(),
          if (_linkedProviders.isEmpty)
            const Padding(
              padding: EdgeInsets.symmetric(vertical: 16),
              child: Center(child: Text('No linked providers')),
            )
          else
            ...OAuthProvider.values.map((OAuthProvider provider) {
              final linked = _linkedProviders.where((p) => p.providerType.toLowerCase() == provider.value.toLowerCase()).firstOrNull;
              return _buildProviderRow(provider, linked, colorScheme);
            }),
        ],
      ),
    ),
  );

  Widget _buildProviderRow(OAuthProvider provider, LinkedProvider? linked, ColorScheme colorScheme) {
    final (icon, color) = _getProviderStyle(provider);
    final isLinked = linked != null;

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Container(
            width: 36,
            height: 36,
            decoration: BoxDecoration(color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8)),
            child: Icon(icon, color: color, size: 20),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(provider.value[0].toUpperCase() + provider.value.substring(1), style: const TextStyle(fontWeight: FontWeight.w500)),
                if (linked?.email != null) Text(linked!.email!, style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant)),
              ],
            ),
          ),
          if (isLinked)
            TextButton(onPressed: () => _unlinkProvider(provider), child: const Text('Unlink'))
          else
            FilledButton.tonal(onPressed: () => _showLinkProviderDialog(provider), child: const Text('Link')),
        ],
      ),
    );
  }

  Widget _buildTokenCard(ColorScheme colorScheme) {
    final tokens = _authService.currentTokens;
    if (tokens == null) return const SizedBox.shrink();

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Row(
              children: [
                Icon(Icons.vpn_key),
                SizedBox(width: 8),
                Text('Token Info', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
              ],
            ),
            const Divider(),
            _buildInfoRow('Token Type', tokens.tokenType),
            _buildInfoRow('Expires At', '${tokens.expiresAt.hour}:${tokens.expiresAt.minute.toString().padLeft(2, '0')}'),
            _buildInfoRow('Is Expired', tokens.isExpired ? 'Yes' : 'No'),
            const SizedBox(height: 12),
            Row(
              children: [
                Expanded(
                  child: FilledButton.tonalIcon(onPressed: _refreshToken, icon: const Icon(Icons.refresh, size: 18), label: const Text('Refresh Token')),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildAuthForm(ColorScheme colorScheme) => Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(
            _isRegisterMode ? 'Create Account' : 'Sign In',
            style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 24),
          if (_isRegisterMode) ...[
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _firstNameController,
                    decoration: const InputDecoration(labelText: 'First Name', border: OutlineInputBorder()),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: TextField(
                    controller: _lastNameController,
                    decoration: const InputDecoration(labelText: 'Last Name', border: OutlineInputBorder()),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
          ],
          TextField(
            controller: _emailController,
            decoration: const InputDecoration(labelText: 'Email', border: OutlineInputBorder(), prefixIcon: Icon(Icons.email_outlined)),
            keyboardType: TextInputType.emailAddress,
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _passwordController,
            decoration: const InputDecoration(labelText: 'Password', border: OutlineInputBorder(), prefixIcon: Icon(Icons.lock_outline)),
            obscureText: true,
          ),
          const SizedBox(height: 24),
          FilledButton(
            onPressed: _isLoading ? null : (_isRegisterMode ? _register : _login),
            child: _isLoading
                ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
                : Text(_isRegisterMode ? 'Create Account' : 'Sign In'),
          ),
          const SizedBox(height: 12),
          TextButton(
            onPressed: () => setState(() => _isRegisterMode = !_isRegisterMode),
            child: Text(_isRegisterMode ? 'Already have an account? Sign In' : "Don't have an account? Register"),
          ),
        ],
      ),
    ),
  );

  Widget _buildOAuthSection(ColorScheme colorScheme) => Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          Row(
            children: [
              const Expanded(child: Divider()),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Text('Or continue with', style: TextStyle(color: colorScheme.onSurfaceVariant)),
              ),
              const Expanded(child: Divider()),
            ],
          ),
          const SizedBox(height: 16),
          Wrap(
            spacing: 12,
            runSpacing: 12,
            alignment: WrapAlignment.center,
            children: OAuthProvider.values.map((OAuthProvider provider) {
              final (icon, color) = _getProviderStyle(provider);
              return _buildOAuthButton(provider, icon, color);
            }).toList(),
          ),
          const SizedBox(height: 16),
          Text(
            'Note: OAuth requires a valid token from the provider',
            style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    ),
  );

  Widget _buildOAuthButton(OAuthProvider provider, IconData icon, Color color) => FilledButton.tonal(
    onPressed: () => _showOAuthDialog(provider),
    style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12)),
    child: Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(icon, color: color, size: 20),
        const SizedBox(width: 8),
        Text(provider.value[0].toUpperCase() + provider.value.substring(1)),
      ],
    ),
  );

  Widget _buildInfoRow(String label, String value) => Padding(
    padding: const EdgeInsets.symmetric(vertical: 4),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(label, style: const TextStyle(fontWeight: FontWeight.w500)),
        Text(value, style: TextStyle(color: Theme.of(context).colorScheme.primary)),
      ],
    ),
  );

  (IconData, Color) _getProviderStyle(OAuthProvider provider) => switch (provider) {
    OAuthProvider.google => (Icons.g_mobiledata, const Color(0xFFDB4437)),
    OAuthProvider.github => (Icons.code, const Color(0xFF24292E)),
    OAuthProvider.microsoft => (Icons.window, const Color(0xFF00A4EF)),
    OAuthProvider.apple => (Icons.apple, const Color(0xFF000000)),
    OAuthProvider.discord => (Icons.discord, const Color(0xFF5865F2)),
    OAuthProvider.authstack => (Icons.security, const Color(0xFF6366F1)),
  };

  // Actions
  Future<void> _login() async {
    if (_emailController.text.isEmpty || _passwordController.text.isEmpty) {
      VooToast.showWarning(message: 'Please fill in all fields');
      return;
    }

    setState(() => _isLoading = true);
    try {
      await _authService.loginWithEmail(email: _emailController.text, password: _passwordController.text);
      VooToast.showSuccess(message: 'Login successful!');
      _clearForm();
    } on VooAuthstackException catch (e) {
      VooToast.showError(message: e.message);
    } finally {
      setState(() => _isLoading = false);
    }
  }

  Future<void> _register() async {
    if (_emailController.text.isEmpty || _passwordController.text.isEmpty) {
      VooToast.showWarning(message: 'Please fill in all fields');
      return;
    }

    setState(() => _isLoading = true);
    try {
      await _authService.register(
        email: _emailController.text,
        password: _passwordController.text,
        firstName: _firstNameController.text.isEmpty ? null : _firstNameController.text,
        lastName: _lastNameController.text.isEmpty ? null : _lastNameController.text,
      );
      VooToast.showSuccess(message: 'Account created successfully!');
      _clearForm();
    } on VooAuthstackException catch (e) {
      VooToast.showError(message: e.message);
    } finally {
      setState(() => _isLoading = false);
    }
  }

  Future<void> _logout() async {
    await _authService.logout();
    setState(() => _linkedProviders = []);
    VooToast.showInfo(message: 'Logged out');
  }

  Future<void> _refreshToken() async {
    try {
      await _authService.refreshToken();
      VooToast.showSuccess(message: 'Token refreshed!');
      setState(() {});
    } on VooAuthstackException catch (e) {
      VooToast.showError(message: e.message);
    }
  }

  Future<void> _unlinkProvider(OAuthProvider provider) async {
    try {
      await _authService.unlinkProvider(provider);
      VooToast.showSuccess(message: '${provider.value} unlinked');
      await _loadLinkedProviders();
    } on VooAuthstackException catch (e) {
      VooToast.showError(message: e.message);
    }
  }

  void _showOAuthDialog(OAuthProvider provider) {
    final tokenController = TextEditingController();

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Login with ${provider.value}'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('Enter your OAuth token from the provider:'),
            const SizedBox(height: 16),
            TextField(
              controller: tokenController,
              decoration: const InputDecoration(labelText: 'OAuth Token', border: OutlineInputBorder()),
              maxLines: 3,
            ),
          ],
        ),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
          FilledButton(
            onPressed: () async {
              Navigator.pop(context);
              if (tokenController.text.isNotEmpty) {
                try {
                  await _authService.loginWithOAuthToken(provider: provider, token: tokenController.text);
                  VooToast.showSuccess(message: 'Logged in with ${provider.value}!');
                } on VooAuthstackException catch (e) {
                  VooToast.showError(message: e.message);
                }
              }
            },
            child: const Text('Login'),
          ),
        ],
      ),
    );
  }

  void _showLinkProviderDialog(OAuthProvider provider) {
    final tokenController = TextEditingController();

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('Link ${provider.value}'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('Enter your OAuth token to link this provider:'),
            const SizedBox(height: 16),
            TextField(
              controller: tokenController,
              decoration: const InputDecoration(labelText: 'OAuth Token', border: OutlineInputBorder()),
              maxLines: 3,
            ),
          ],
        ),
        actions: [
          TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
          FilledButton(
            onPressed: () async {
              Navigator.pop(context);
              if (tokenController.text.isNotEmpty) {
                try {
                  await _authService.linkProviderWithToken(provider: provider, token: tokenController.text);
                  VooToast.showSuccess(message: '${provider.value} linked!');
                  await _loadLinkedProviders();
                } on VooAuthstackException catch (e) {
                  VooToast.showError(message: e.message);
                }
              }
            },
            child: const Text('Link'),
          ),
        ],
      ),
    );
  }

  void _clearForm() {
    _emailController.clear();
    _passwordController.clear();
    _firstNameController.clear();
    _lastNameController.clear();
  }
}
1
likes
150
points
383
downloads

Publisher

verified publishervoostack.com

Weekly Downloads

Flutter SDK for Voo AuthStack - a centralized authentication platform. Supports email/password, OAuth providers (Google, GitHub, Microsoft, Apple, Discord), automatic token refresh, and provider linking.

Homepage
Repository (GitHub)
View/report issues

Topics

#flutter #authentication #oauth #jwt #security

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

dio, equatable, flutter, json_annotation, voo_core

More

Packages that depend on voo_authstack_client