github_oauth_signin 1.0.5
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.
// 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,
),
),
],
),
);
}