voostackauth_client 0.1.0
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();
}
}