github_oauth_signin 1.0.6 copy "github_oauth_signin: ^1.0.6" to clipboard
github_oauth_signin: ^1.0.6 copied to clipboard

A comprehensive Flutter package for GitHub OAuth authentication with user data fetching. Supports both mobile and web platforms with a clean, easy-to-use API.

example/lib/main.dart

// ignore_for_file: use_build_context_synchronously

import 'package:flutter/material.dart';
import 'package:github_oauth_signin/github_oauth_signin.dart';

void main() {
  runApp(const MyApp());
}

/// The main application widget for the GitHub OAuth signin example.
class MyApp extends StatelessWidget {
  /// Creates the main application widget.
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'GitHub OAuth Sign In Example',
    theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
    home: const MyHomePage(title: 'GitHub OAuth Demo'),
  );
}

/// The home page widget that demonstrates GitHub OAuth signin functionality.
class MyHomePage extends StatefulWidget {
  /// Creates the home page widget.
  ///
  /// The [title] parameter is displayed in the app bar.
  const MyHomePage({required this.title, super.key});

  /// The title to display in the app bar.
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // Todo: Replace these with your actual GitHub OAuth app credentials
  // Get them from: https://github.com/settings/developers
  static const String _clientId = 'your-client-id-here';
  static const String _clientSecret = 'your-client-secret-here';
  static const String _redirectUrl = 'https://your-app.com/callback';

  bool _isLoading = false;
  bool _isAuthorizing = false;
  GitHubSignInResult? _lastResult;
  String? _authorizationCode;

  /// Initialize GitHubSignIn with configuration
  ///
  /// See the library documentation for all available options:
  /// - scope: OAuth scopes to request (default: 'user,gist,user:email')
  /// - title: Title for the sign-in page
  /// - centerTitle: Whether to center the title
  /// - allowSignUp: Allow new user registration (default: true)
  /// - clearCache: Clear browser cache before sign-in (default: true)
  late final GitHubSignIn gitHubSignIn = GitHubSignIn(
    clientId: _clientId,
    clientSecret: _clientSecret,
    redirectUrl: _redirectUrl,
    title: 'GitHub Connection',
    centerTitle: false,
  );

  /// Initiates the GitHub OAuth sign-in flow
  ///
  /// This method:
  /// 1. Shows a loading indicator
  /// 2. Opens the GitHub authorization page
  /// 3. Exchanges the authorization code for an access token
  /// 4. Fetches the user's profile data and verified email
  /// 5. Handles the result and displays appropriate UI
  Future<void> _gitHubSignIn(BuildContext context) async {
    // Validate configuration
    if (_clientId == 'your-client-id-here' ||
        _clientSecret == 'your-client-secret-here' ||
        _redirectUrl == 'https://your-app.com/callback') {
      _showErrorDialog(
        context,
        'Configuration Required',
        'Please update the GitHub OAuth credentials in main.dart:\n\n'
            '- _clientId\n'
            '- _clientSecret\n'
            '- _redirectUrl\n\n'
            'Get your credentials from:\n'
            'https://github.com/settings/developers',
      );
      return;
    }

    setState(() {
      _isLoading = true;
      _lastResult = null;
    });

    try {
      // Start the OAuth flow
      // This will:
      // - Open GitHub authorization page (WebView on mobile, browser on web)
      // - Wait for user authorization
      // - Exchange code for access token
      // - Fetch user profile data
      final GitHubSignInResult result = await gitHubSignIn.signIn(context);

      setState(() {
        _isLoading = false;
        _lastResult = result;
      });

      // Handle the result based on status
      switch (result.status) {
        case GitHubSignInResultStatus.ok:
          debugPrint('✅ GitHub Sign In Successful!');
          debugPrint('🔑 Access Token: ${result.token}');

          if (result.userData != null) {
            debugPrint('👤 User Data:');
            debugPrint('   - Name: ${result.userData!['name']}');
            debugPrint('   - Email: ${result.userData!['email']}');
            debugPrint('   - Username: ${result.userData!['login']}');
            debugPrint('   - Avatar: ${result.userData!['avatar_url']}');
            debugPrint('   - Bio: ${result.userData!['bio']}');
            debugPrint(
              '   - Public Repos: ${result.userData!['public_repos']}',
            );
            debugPrint('   - Followers: ${result.userData!['followers']}');
            debugPrint('   - Following: ${result.userData!['following']}');

            // Show success dialog with user info
            if (!mounted) {
              return;
            }
            _showUserInfoDialog(context, result.userData!);
          } else {
            debugPrint('⚠️ User data could not be fetched');
            if (!mounted) {
              return;
            }
            final String tokenPreview = result.token != null
                ? '${result.token!.substring(0, 20)}...'
                : 'N/A';
            _showErrorDialog(
              context,
              'Partial Success',
              'Authentication successful, but user'
                  ' data could not be fetched.\n\n'
                  'Access Token: $tokenPreview',
            );
          }
          break;

        case GitHubSignInResultStatus.cancelled:
          debugPrint('❌ GitHub Sign In Cancelled');
          debugPrint('Error: ${result.errorMessage}');
          if (!mounted) {
            return;
          }
          _showErrorDialog(
            context,
            'Sign In Cancelled',
            'The sign-in process was cancelled.\n\n${result.errorMessage}',
            isError: false,
          );
          break;

        case GitHubSignInResultStatus.failed:
          debugPrint('❌ GitHub Sign In Failed');
          debugPrint('Error: ${result.errorMessage}');
          if (!mounted) {
            return;
          }
          _showErrorDialog(
            context,
            'Sign In Failed',
            'An error occurred during sign-in:\n\n${result.errorMessage}',
          );
          break;
      }
    } on Exception catch (e) {
      setState(() {
        _isLoading = false;
      });
      debugPrint('❌ Exception during sign-in: $e');
      if (!mounted) {
        return;
      }
      _showErrorDialog(
        context,
        'Unexpected Error',
        'An unexpected error occurred:\n\n$e',
      );
    }
  }

  /// Initiates the GitHub OAuth authorization flow (authorize only)
  ///
  /// This method:
  /// 1. Shows a loading indicator
  /// 2. Opens the GitHub authorization page
  /// 3. Returns the authorization code (without token exchange)
  /// 4. Displays the authorization code result
  ///
  /// Note: This method only performs authorization and returns the code.
  /// You would need to exchange the code for an access token manually.
  Future<void> _gitHubAuthorize(BuildContext context) async {
    // Validate configuration
    if (_clientId == 'your-client-id-here' ||
        _clientSecret == 'your-client-secret-here' ||
        _redirectUrl == 'https://your-app.com/callback') {
      _showErrorDialog(
        context,
        'Configuration Required',
        'Please update the GitHub OAuth credentials in main.dart:\n\n'
            '- _clientId\n'
            '- _clientSecret\n'
            '- _redirectUrl\n\n'
            'Get your credentials from:\n'
            'https://github.com/settings/developers',
      );
      return;
    }

    setState(() {
      _isAuthorizing = true;
      _authorizationCode = null;
      _lastResult = null;
    });

    try {
      // Start the OAuth authorization flow (authorize only)
      // This will:
      // - Open GitHub authorization page (WebView on mobile, browser on web)
      // - Wait for user authorization
      // - Return the authorization code (without token exchange)
      final Object authorizedResult = await gitHubSignIn.authorize(context);

      setState(() {
        _isAuthorizing = false;
      });

      // Handle the result based on type
      if (authorizedResult is GitHubSignInResult) {
        // Authorization was cancelled or failed
        setState(() {
          _lastResult = authorizedResult;
        });

        switch (authorizedResult.status) {
          case GitHubSignInResultStatus.cancelled:
            debugPrint('❌ GitHub Authorization Cancelled');
            debugPrint('Error: ${authorizedResult.errorMessage}');
            if (!mounted) {
              return;
            }
            _showErrorDialog(
              context,
              'Authorization Cancelled',
              'The authorization process was cancelled.\n\n'
                  '${authorizedResult.errorMessage}',
              isError: false,
            );
            break;

          case GitHubSignInResultStatus.failed:
            debugPrint('❌ GitHub Authorization Failed');
            debugPrint('Error: ${authorizedResult.errorMessage}');
            if (!mounted) {
              return;
            }
            _showErrorDialog(
              context,
              'Authorization Failed',
              'An error occurred during authorization:\n\n'
                  '${authorizedResult.errorMessage}',
            );
            break;

          case GitHubSignInResultStatus.ok:
            // This shouldn't happen with authorize(),
            // but handle it just in case
            debugPrint('✅ GitHub Authorization Successful');
            break;
        }
      } else if (authorizedResult is Exception) {
        // An exception occurred
        debugPrint('❌ Exception during authorization: $authorizedResult');
        if (!mounted) {
          return;
        }
        _showErrorDialog(
          context,
          'Authorization Error',
          'An error occurred during authorization:\n\n$authorizedResult',
        );
      } else {
        // Successfully got the authorization code
        final String code = authorizedResult.toString();
        setState(() {
          _authorizationCode = code;
        });

        debugPrint('✅ GitHub Authorization Successful!');
        debugPrint('🔑 Authorization Code: $code');

        if (!mounted) {
          return;
        }
        _showAuthorizationCodeDialog(context, code);
      }
    } on Exception catch (e) {
      setState(() {
        _isAuthorizing = false;
      });
      debugPrint('❌ Exception during authorization: $e');
      if (!mounted) {
        return;
      }
      _showErrorDialog(
        context,
        'Unexpected Error',
        'An unexpected error occurred:\n\n$e',
      );
    }
  }

  /// Shows a dialog with the authorization code
  void _showAuthorizationCodeDialog(BuildContext context, String code) {
    showDialog(
      context: context,
      builder: (BuildContext context) => AlertDialog(
        title: const Row(
          children: <Widget>[
            Icon(Icons.check_circle, color: Colors.green),
            SizedBox(width: 8),
            Text('Authorization Code'),
          ],
        ),
        content: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              const Text(
                'Authorization successful! You'
                ' received an authorization code.\n\n'
                'This code can be exchanged for an access token '
                'using the GitHub API.',
                style: TextStyle(fontSize: 14),
              ),
              const SizedBox(height: 16),
              const Text(
                'Authorization Code:',
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
              ),
              const SizedBox(height: 8),
              Container(
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: Colors.grey.shade100,
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.grey.shade300),
                ),
                child: SelectableText(
                  code,
                  style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
                ),
              ),
              const SizedBox(height: 16),
              const Text(
                'Note: Use the signIn() method to automatically exchange '
                'this code for an access token and fetch user data.',
                style: TextStyle(
                  fontSize: 12,
                  fontStyle: FontStyle.italic,
                  color: Colors.grey,
                ),
              ),
            ],
          ),
        ),
        actions: <Widget>[
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Close'),
          ),
          TextButton.icon(
            onPressed: () {
              Navigator.of(context).pop();
              // Copy to clipboard would require clipboard package
              // For now, just show a snackbar
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                  content: Text('Code is selectable above'),
                  duration: Duration(seconds: 2),
                ),
              );
            },
            icon: const Icon(Icons.copy, size: 18),
            label: const Text('Copy'),
          ),
        ],
      ),
    );
  }

  /// Shows a dialog with user information after successful authentication
  void _showUserInfoDialog(
    BuildContext context,
    Map<String, dynamic> userData,
  ) {
    showDialog(
      context: context,
      builder: (BuildContext context) => AlertDialog(
        title: const Row(
          children: <Widget>[
            Icon(Icons.check_circle, color: Colors.green),
            SizedBox(width: 8),
            Text('GitHub User Info'),
          ],
        ),
        content: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              if (userData['avatar_url'] != null)
                Center(
                  child: CircleAvatar(
                    radius: 40,
                    backgroundImage: NetworkImage(userData['avatar_url']),
                  ),
                ),
              const SizedBox(height: 16),
              _buildInfoRow('Name', userData['name']),
              _buildInfoRow('Username', userData['login']),
              _buildInfoRow('Email', userData['email']),
              _buildInfoRow('Bio', userData['bio']),
              _buildInfoRow('Company', userData['company']),
              _buildInfoRow('Location', userData['location']),
              _buildInfoRow(
                'Public Repos',
                userData['public_repos']?.toString(),
              ),
              _buildInfoRow('Followers', userData['followers']?.toString()),
              _buildInfoRow('Following', userData['following']?.toString()),
            ],
          ),
        ),
        actions: <Widget>[
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Close'),
          ),
        ],
      ),
    );
  }

  /// Shows an error or information dialog
  void _showErrorDialog(
    BuildContext context,
    String title,
    String message, {
    bool isError = true,
  }) {
    showDialog(
      context: context,
      builder: (BuildContext context) => AlertDialog(
        title: Row(
          children: <Widget>[
            Icon(
              isError ? Icons.error : Icons.info,
              color: isError ? Colors.red : Colors.blue,
            ),
            const SizedBox(width: 8),
            Text(title),
          ],
        ),
        content: Text(message),
        actions: <Widget>[
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  Widget _buildInfoRow(String label, String? value) {
    if (value == null || value.isEmpty) {
      return const SizedBox.shrink();
    }

    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          SizedBox(
            width: 80,
            child: Text(
              '$label:',
              style: const TextStyle(fontWeight: FontWeight.bold),
            ),
          ),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: Text(widget.title), elevation: 2),
    body: SafeArea(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            // App Icon/Logo
            const Icon(Icons.code, size: 80, color: Colors.blue),
            const SizedBox(height: 24),

            // Title
            const Text(
              'GitHub OAuth Sign In',
              style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),

            // Description
            const Text(
              'Sign in with your GitHub account to access your '
              'profile and repositories.',
              style: TextStyle(fontSize: 16, color: Colors.grey),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 48),

            // Sign In Button
            ElevatedButton.icon(
              onPressed: (_isLoading || _isAuthorizing)
                  ? null
                  : () => _gitHubSignIn(context),
              icon: _isLoading
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                      ),
                    )
                  : const Icon(Icons.login),
              label: Text(_isLoading ? 'Signing In...' : 'Sign In with GitHub'),
              style: ElevatedButton.styleFrom(
                padding: const EdgeInsets.symmetric(
                  vertical: 16,
                  horizontal: 24,
                ),
                textStyle: const TextStyle(fontSize: 18),
              ),
            ),
            const SizedBox(height: 16),

            // Authorize Only Button
            OutlinedButton.icon(
              onPressed: (_isLoading || _isAuthorizing)
                  ? null
                  : () => _gitHubAuthorize(context),
              icon: _isAuthorizing
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.vpn_key),
              label: Text(
                _isAuthorizing ? 'Authorizing...' : 'Authorize Only (Get Code)',
              ),
              style: OutlinedButton.styleFrom(
                padding: const EdgeInsets.symmetric(
                  vertical: 16,
                  horizontal: 24,
                ),
                textStyle: const TextStyle(fontSize: 18),
              ),
            ),
            const SizedBox(height: 8),

            // Help text for authorize button
            const Text(
              'Authorize Only returns the authorization code without token '
              'exchange',
              style: TextStyle(
                fontSize: 12,
                color: Colors.grey,
                fontStyle: FontStyle.italic,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 24),

            // Last Result Status
            if (_lastResult != null) ...<Widget>[
              const Divider(),
              const SizedBox(height: 16),
              _buildStatusCard(_lastResult!),
            ],

            // Authorization Code Result
            if (_authorizationCode != null) ...<Widget>[
              const Divider(),
              const SizedBox(height: 16),
              _buildAuthorizationCodeCard(_authorizationCode!),
            ],

            // Configuration Warning
            if (_clientId == 'your-client-id-here' ||
                _clientSecret == 'your-client-secret-here' ||
                _redirectUrl == 'https://your-app.com/callback') ...<Widget>[
              const SizedBox(height: 24),
              Container(
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: Colors.orange.shade50,
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(color: Colors.orange.shade200),
                ),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Row(
                      children: <Widget>[
                        Icon(Icons.warning, color: Colors.orange.shade700),
                        const SizedBox(width: 8),
                        Text(
                          'Configuration Required',
                          style: TextStyle(
                            fontWeight: FontWeight.bold,
                            color: Colors.orange.shade900,
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    Text(
                      'Please update the GitHub OAuth credentials in '
                      'main.dart before using this example.',
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.orange.shade900,
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ],
        ),
      ),
    ),
  );

  /// Builds a status card showing the last authentication result
  Widget _buildStatusCard(GitHubSignInResult result) {
    final Color statusColor;
    final IconData statusIcon;
    final String statusText;

    switch (result.status) {
      case GitHubSignInResultStatus.ok:
        statusColor = Colors.green;
        statusIcon = Icons.check_circle;
        statusText = 'Success';
        break;
      case GitHubSignInResultStatus.cancelled:
        statusColor = Colors.orange;
        statusIcon = Icons.cancel;
        statusText = 'Cancelled';
        break;
      case GitHubSignInResultStatus.failed:
        statusColor = Colors.red;
        statusIcon = Icons.error;
        statusText = 'Failed';
        break;
    }

    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: statusColor.withValues(alpha: 0.1),
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: statusColor.withValues(alpha: 0.3)),
      ),
      child: Row(
        children: <Widget>[
          Icon(statusIcon, color: statusColor),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  'Last Status: $statusText',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: statusColor,
                  ),
                ),
                if (result.errorMessage.isNotEmpty)
                  Padding(
                    padding: const EdgeInsets.only(top: 4),
                    child: Text(
                      result.errorMessage,
                      style: const TextStyle(fontSize: 12),
                    ),
                  ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  /// Builds a status card showing the authorization code result
  Widget _buildAuthorizationCodeCard(String code) => Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: Colors.blue.shade50,
      borderRadius: BorderRadius.circular(8),
      border: Border.all(color: Colors.blue.shade200),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        const Row(
          children: <Widget>[
            Icon(Icons.vpn_key, color: Colors.blue),
            SizedBox(width: 12),
            Text(
              'Authorization Code Received',
              style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue),
            ),
          ],
        ),
        const SizedBox(height: 12),
        Container(
          padding: const EdgeInsets.all(12),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(6),
            border: Border.all(color: Colors.blue.shade200),
          ),
          child: SelectableText(
            code,
            style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
          ),
        ),
        const SizedBox(height: 8),
        const Text(
          'This code can be exchanged for an access token '
          'using the GitHub API.',
          style: TextStyle(
            fontSize: 11,
            color: Colors.grey,
            fontStyle: FontStyle.italic,
          ),
        ),
      ],
    ),
  );
}
3
likes
160
points
56
downloads

Publisher

verified publisherdevjay.jemira.in

Weekly Downloads

A comprehensive Flutter package for GitHub OAuth authentication with user data fetching. Supports both mobile and web platforms with a clean, easy-to-use API.

Repository (GitHub)
View/report issues

Topics

#github #oauth #authentication #signin #flutter

Documentation

API reference

License

MIT (license)

Dependencies

flutter, http, url_launcher, webview_flutter

More

Packages that depend on github_oauth_signin