github_oauth_signin 1.0.5 copy "github_oauth_signin: ^1.0.5" to clipboard
github_oauth_signin: ^1.0.5 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';
  static const String _clientSecret = 'YOUR_CLIENT_SECRET';
  static const String _redirectUrl = 'YOUR_REDIRECT_URL';

  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,
              ),
            ),
          ],
        ),
      );
}
2
likes
160
points
282
downloads

Publisher

unverified uploader

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

Documentation
API reference

License

MIT (license)

Dependencies

flutter, http, url_launcher, webview_flutter

More

Packages that depend on github_oauth_signin