flutter_next_auth 1.2.1
flutter_next_auth: ^1.2.1 copied to clipboard
A Flutter package for NextERP (Frappe/ERPNext) authentication with secure session management.
import 'package:flutter/material.dart';
import 'package:flutter_next_auth/flutter_next_auth.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize with your NextERP server URL
await flutternext.initialize(
baseUrl: 'https://demo.erpnext.com', // Replace with your server URL
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Next Auth Example',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
_checkSession();
}
Future<void> _checkSession() async {
// Wait a bit for splash effect
await Future.delayed(const Duration(seconds: 1));
// Try to restore session
final result = await flutternext.relogin();
if (mounted) {
if (result.success) {
// Navigate to home
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
} else {
// Navigate to login
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
}
}
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
final result = await flutternext.login(
usr: _usernameController.text.trim(),
pwd: _passwordController.text,
);
if (mounted) {
setState(() => _isLoading = false);
if (result.success) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.message ?? 'Login failed'),
backgroundColor: Colors.red,
),
);
}
}
}
void _navigateToForgotPassword() {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ForgotPasswordScreen()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 48),
const Icon(
Icons.lock_outline,
size: 80,
color: Colors.blue,
),
const SizedBox(height: 48),
TextFormField(
controller: _usernameController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Username / Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your username';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: 'Password',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() => _obscurePassword = !_obscurePassword);
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Login', style: TextStyle(fontSize: 16)),
),
const SizedBox(height: 16),
TextButton(
onPressed: _navigateToForgotPassword,
child: const Text('Forgot Password?'),
),
],
),
),
),
),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
Future<void> _handleLogout(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Logout'),
content: const Text('Are you sure you want to logout?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Logout'),
),
],
),
);
if (confirmed != true) return;
if (!context.mounted) return;
await flutternext.logout();
if (!context.mounted) return;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
}
void _navigateToChangePassword(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ChangePasswordScreen()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => _handleLogout(context),
),
],
),
body: FutureBuilder<UserProfile?>(
future: flutternext.getUserProfile(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
final user = snapshot.data;
if (user == null) {
return const Center(
child: Text('No user data available'),
);
}
return ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Profile Information',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildInfoRow('Username', user.username),
_buildInfoRow('Full Name', user.fullName ?? 'N/A'),
_buildInfoRow('Email', user.email ?? 'N/A'),
],
),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _navigateToChangePassword(context),
icon: const Icon(Icons.password),
label: const Text('Change Password'),
),
],
);
},
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
child: Text(value),
),
],
),
);
}
}
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
Future<void> _handleResetPassword() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
final result = await flutternext.resetPassword(
user: _emailController.text.trim(),
);
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.message ?? 'Request sent'),
backgroundColor: result.success ? Colors.green : Colors.red,
),
);
if (result.success) {
Navigator.pop(context);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Forgot Password'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 48),
const Icon(
Icons.email_outlined,
size: 80,
color: Colors.blue,
),
const SizedBox(height: 24),
const Text(
'Enter your email address and we\'ll send you instructions to reset your password.',
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _handleResetPassword,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Send Reset Link', style: TextStyle(fontSize: 16)),
),
],
),
),
),
),
);
}
}
class ChangePasswordScreen extends StatefulWidget {
const ChangePasswordScreen({super.key});
@override
State<ChangePasswordScreen> createState() => _ChangePasswordScreenState();
}
class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
final _formKey = GlobalKey<FormState>();
final _oldPasswordController = TextEditingController();
final _newPasswordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _isLoading = false;
bool _obscureOld = true;
bool _obscureNew = true;
bool _obscureConfirm = true;
@override
void dispose() {
_oldPasswordController.dispose();
_newPasswordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _handleChangePassword() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
final result = await flutternext.changePassword(
oldPassword: _oldPasswordController.text,
newPassword: _newPasswordController.text,
);
if (mounted) {
setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.message ?? 'Password changed'),
backgroundColor: result.success ? Colors.green : Colors.red,
),
);
if (result.success) {
Navigator.pop(context);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Change Password'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 24),
TextFormField(
controller: _oldPasswordController,
obscureText: _obscureOld,
decoration: InputDecoration(
labelText: 'Current Password',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(_obscureOld ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => _obscureOld = !_obscureOld),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your current password';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _newPasswordController,
obscureText: _obscureNew,
decoration: InputDecoration(
labelText: 'New Password',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(_obscureNew ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => _obscureNew = !_obscureNew),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a new password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _confirmPasswordController,
obscureText: _obscureConfirm,
decoration: InputDecoration(
labelText: 'Confirm New Password',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock_outline),
suffixIcon: IconButton(
icon: Icon(_obscureConfirm ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please confirm your new password';
}
if (value != _newPasswordController.text) {
return 'Passwords do not match';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _handleChangePassword,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Change Password', style: TextStyle(fontSize: 16)),
),
],
),
),
),
),
);
}
}