voostackauth_client 0.1.0 copy "voostackauth_client: ^0.1.0" to clipboard
voostackauth_client: ^0.1.0 copied to clipboard

Flutter SDK for VooStackAuth - 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:voostackauth_client/voostackauth_client.dart';

// Update this to your VooStackAuth 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) {
    return MaterialApp(
      title: 'VooStackAuth Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          brightness: Brightness.light,
        ),
        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 VooStackAuthService _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 = VooStackAuthService(
      config: const VooStackAuthConfig(baseUrl: baseUrl),
    );

    // Listen to auth state changes
    _authService.statusStream.listen((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('VooStackAuth 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) {
    return 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((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) {
    return 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) {
    return 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((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) {
    return 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) {
    return 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) {
    return 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)),
    };
  }

  // 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 VooStackAuthException 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 VooStackAuthException 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 VooStackAuthException 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 VooStackAuthException 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 VooStackAuthException 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 VooStackAuthException catch (e) {
                  VooToast.showError(message: e.message);
                }
              }
            },
            child: const Text('Link'),
          ),
        ],
      ),
    );
  }

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

Publisher

verified publishervoostack.com

Weekly Downloads

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

Topics

#flutter #authentication #oauth #jwt #security

Documentation

API reference

License

MIT (license)

Dependencies

dio, equatable, flutter, json_annotation, voo_core

More

Packages that depend on voostackauth_client