adding packages
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
export 'package:pocketbase/pocketbase.dart';
|
||||
export 'src/auth_service.dart';
|
||||
export 'src/auth_interceptor.dart';
|
||||
export 'src/persistent_auth_store.dart';
|
||||
export 'src/ui/login_screen.dart';
|
||||
export 'src/ui/register_screen.dart';
|
||||
export 'src/ui/forgot_password_screen.dart';
|
||||
export 'src/ui/update_profile_screen.dart';
|
||||
export 'src/ui/change_email_screen.dart';
|
||||
export 'src/ui/verify_email_screen.dart';
|
||||
export 'src/ui/delete_account_screen.dart';
|
||||
|
||||
export 'src/view_models/login_view_model.dart';
|
||||
export 'src/view_models/register_view_model.dart';
|
||||
export 'src/view_models/forgot_password_view_model.dart';
|
||||
export 'src/view_models/update_profile_view_model.dart';
|
||||
export 'src/view_models/change_email_view_model.dart';
|
||||
export 'src/view_models/verify_email_view_model.dart';
|
||||
export 'src/view_models/delete_account_view_model.dart';
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_interceptor/http_interceptor.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
class TokenRetryPolicy extends RetryPolicy {
|
||||
final PocketBase _pb;
|
||||
|
||||
TokenRetryPolicy(this._pb);
|
||||
|
||||
@override
|
||||
// Limit retries to 1 to prevent infinite loops if the refresh itself fails.
|
||||
int get maxRetryAttempts => 1;
|
||||
|
||||
@override
|
||||
Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
|
||||
// Check for 401 Unauthorized status
|
||||
if (response.statusCode == 401) {
|
||||
// Check if we even have a token to refresh.
|
||||
// If we are anonymous, a 401 is valid and shouldn't trigger a refresh.
|
||||
if (!_pb.authStore.isValid) return false;
|
||||
|
||||
// print('[Auth] Token expired (401). Attempting refresh...');
|
||||
|
||||
try {
|
||||
// PERFORMS THE AUTO REFRESH
|
||||
// The SDK's authRefresh method automatically rotates the token
|
||||
// and updates the AuthStore via the save() callback we defined earlier.
|
||||
await _pb.collection('users').authRefresh();
|
||||
|
||||
return true; // Signal to retry the request
|
||||
} catch (e) {
|
||||
// print('[Auth] Refresh failed: $e. Logging out.');
|
||||
// If refresh fails (e.g., user banned, password changed),
|
||||
// we must clear the store to trigger a logout in the UI.
|
||||
_pb.authStore.clear();
|
||||
return false; // Do not retry
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
http.Client clientFactory(PocketBase pb) {
|
||||
return InterceptedClient.build(
|
||||
interceptors: [
|
||||
// Optional: Add logging interceptor here for debugging
|
||||
],
|
||||
retryPolicy: TokenRetryPolicy(pb),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'dart:async';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
class AuthService {
|
||||
final PocketBase _pb;
|
||||
// Broadcast controller allows multiple listeners (UI, Router, Analytics)
|
||||
final StreamController<RecordModel?> _userController =
|
||||
StreamController<RecordModel?>.broadcast();
|
||||
|
||||
AuthService(this._pb) {
|
||||
// 1. Emit Initial State
|
||||
// We synchronously check the store. If valid, emit the model.
|
||||
_emitCurrent();
|
||||
|
||||
// 2. Listen to PocketBase AuthStore changes
|
||||
// The onChange stream fires whenever authStore.save() or authStore.clear() is called.
|
||||
_pb.authStore.onChange.listen((event) {
|
||||
_emitCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
void _emitCurrent() {
|
||||
final model = _pb.authStore.record;
|
||||
if (_pb.authStore.isValid && model is RecordModel) {
|
||||
_userController.add(model);
|
||||
} else {
|
||||
_userController.add(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Expose the stream for the UI to consume.
|
||||
// We use .distinct() to ensure the UI only rebuilds if the user object actually changes.
|
||||
Stream<RecordModel?> get authStateChanges =>
|
||||
_userController.stream.distinct();
|
||||
|
||||
// Current value accessor for synchronous checks (e.g. inside guards)
|
||||
RecordModel? get currentUser => _pb.authStore.record;
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
await _pb.collection('users').authWithPassword(email, password);
|
||||
}
|
||||
|
||||
Future<void> signup(String email, String password) async {
|
||||
await _pb
|
||||
.collection('users')
|
||||
.create(
|
||||
body: {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'passwordConfirm': password,
|
||||
'name': email.split('@').first,
|
||||
},
|
||||
);
|
||||
// Auto-login after signup
|
||||
await login(email, password);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
_pb.authStore.clear();
|
||||
}
|
||||
|
||||
Future<void> resetPassword(String email) async {
|
||||
await _pb.collection('users').requestPasswordReset(email);
|
||||
}
|
||||
|
||||
Future<RecordModel> updateProfile(Map<String, dynamic> body) async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
throw ClientException(
|
||||
url: Uri(),
|
||||
response: {'message': 'User not logged in'},
|
||||
);
|
||||
}
|
||||
return await _pb.collection('users').update(user.id, body: body);
|
||||
}
|
||||
|
||||
Future<void> requestEmailChange(String newEmail) async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
throw ClientException(
|
||||
url: Uri(),
|
||||
response: {'message': 'User not logged in'},
|
||||
);
|
||||
}
|
||||
await _pb.collection('users').requestEmailChange(newEmail);
|
||||
}
|
||||
|
||||
Future<void> requestVerification() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
throw ClientException(
|
||||
url: Uri(),
|
||||
response: {'message': 'User not logged in'},
|
||||
);
|
||||
}
|
||||
await _pb
|
||||
.collection('users')
|
||||
.requestVerification(user.getStringValue('email'));
|
||||
}
|
||||
|
||||
Future<void> deleteAccount() async {
|
||||
final user = currentUser;
|
||||
if (user == null) {
|
||||
throw ClientException(
|
||||
url: Uri(),
|
||||
response: {'message': 'User not logged in'},
|
||||
);
|
||||
}
|
||||
await _pb.collection('users').delete(user.id);
|
||||
_pb.authStore.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
/// Abstract contract for authentication persistence.
|
||||
/// Implement this to use different storage solutions (e.g., Hive, SecureStorage).
|
||||
abstract class AuthPersistence {
|
||||
Future<void> saveAuthData(String data);
|
||||
Future<String?> loadAuthData();
|
||||
Future<void> clearAuthData();
|
||||
}
|
||||
|
||||
/// SharedPreferences implementation of [AuthPersistence].
|
||||
class SharedPrefsAuthPersistence implements AuthPersistence {
|
||||
final SharedPreferences _prefs;
|
||||
static const String _kAuthKey = 'pb_auth_token';
|
||||
|
||||
SharedPrefsAuthPersistence(this._prefs);
|
||||
|
||||
@override
|
||||
Future<void> saveAuthData(String data) async {
|
||||
await _prefs.setString(_kAuthKey, data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> loadAuthData() async {
|
||||
return _prefs.getString(_kAuthKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearAuthData() async {
|
||||
await _prefs.remove(_kAuthKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// The custom AuthStore that connects PocketBase to any [AuthPersistence] implementation.
|
||||
class PersistentAuthStore extends AsyncAuthStore {
|
||||
PersistentAuthStore({required AuthPersistence persistence, super.initial})
|
||||
: super(
|
||||
save: (String data) => persistence.saveAuthData(data),
|
||||
clear: () => persistence.clearAuthData(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../view_models/change_email_view_model.dart';
|
||||
|
||||
class PocketBaseChangeEmailScreen extends StatefulWidget {
|
||||
final AuthService authService;
|
||||
|
||||
const PocketBaseChangeEmailScreen({super.key, required this.authService});
|
||||
|
||||
@override
|
||||
State<PocketBaseChangeEmailScreen> createState() =>
|
||||
_PocketBaseChangeEmailScreenState();
|
||||
}
|
||||
|
||||
class _PocketBaseChangeEmailScreenState
|
||||
extends State<PocketBaseChangeEmailScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
late final ChangeEmailViewModel _viewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_viewModel = ChangeEmailViewModel(widget.authService);
|
||||
}
|
||||
|
||||
Future<void> _requestEmailChange() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
try {
|
||||
await _viewModel.requestEmailChange(_emailController.text.trim());
|
||||
} catch (_) {
|
||||
// Error handled by view model
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _viewModel,
|
||||
builder: (context, _) {
|
||||
if (_viewModel.success) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Change Email')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Verification email sent!',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Check your new email address for instructions to verify the change.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Go Back'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Change Email')),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Enter your new email address.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_viewModel.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
_viewModel.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'New Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter the new email';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _viewModel.isLoading
|
||||
? null
|
||||
: _requestEmailChange,
|
||||
child: _viewModel.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Request Change'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_viewModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../view_models/delete_account_view_model.dart';
|
||||
|
||||
class PocketBaseDeleteAccountScreen extends StatefulWidget {
|
||||
final AuthService authService;
|
||||
|
||||
const PocketBaseDeleteAccountScreen({super.key, required this.authService});
|
||||
|
||||
@override
|
||||
State<PocketBaseDeleteAccountScreen> createState() =>
|
||||
_PocketBaseDeleteAccountScreenState();
|
||||
}
|
||||
|
||||
class _PocketBaseDeleteAccountScreenState
|
||||
extends State<PocketBaseDeleteAccountScreen> {
|
||||
final _confirmController = TextEditingController();
|
||||
late final DeleteAccountViewModel _viewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_viewModel = DeleteAccountViewModel(widget.authService);
|
||||
}
|
||||
|
||||
Future<void> _deleteAccount() async {
|
||||
if (_confirmController.text != 'DELETE') {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please type DELETE to confirm.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _viewModel.deleteAccount();
|
||||
// Navigation handled by auth state change
|
||||
} catch (_) {
|
||||
// Error handled by view model
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _viewModel,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Delete Account'),
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Colors.red,
|
||||
size: 80,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Are you sure you want to delete your account?',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'This action is irreversible. All your data will be permanently removed.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (_viewModel.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
_viewModel.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _confirmController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type DELETE to confirm',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _viewModel.isLoading ? null : _deleteAccount,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: _viewModel.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Text('Permanently Delete Account'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_confirmController.dispose();
|
||||
_viewModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../view_models/forgot_password_view_model.dart';
|
||||
|
||||
class PocketBaseForgotPasswordScreen extends StatefulWidget {
|
||||
final AuthService authService;
|
||||
final VoidCallback? onBackToLogin;
|
||||
|
||||
const PocketBaseForgotPasswordScreen({
|
||||
super.key,
|
||||
required this.authService,
|
||||
this.onBackToLogin,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PocketBaseForgotPasswordScreen> createState() =>
|
||||
_PocketBaseForgotPasswordScreenState();
|
||||
}
|
||||
|
||||
class _PocketBaseForgotPasswordScreenState
|
||||
extends State<PocketBaseForgotPasswordScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
late final ForgotPasswordViewModel _viewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_viewModel = ForgotPasswordViewModel(widget.authService);
|
||||
}
|
||||
|
||||
Future<void> _resetPassword() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
try {
|
||||
await _viewModel.resetPassword(_emailController.text.trim());
|
||||
} catch (_) {
|
||||
// Error handled by view model
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _viewModel,
|
||||
builder: (context, _) {
|
||||
if (_viewModel.success) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Forgot Password')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Password reset link sent!',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Check your email for instructions to reset your password.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: widget.onBackToLogin,
|
||||
child: const Text('Back to Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Forgot Password')),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Enter your email address to receive a password reset link.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_viewModel.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
_viewModel.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your email';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _viewModel.isLoading ? null : _resetPassword,
|
||||
child: _viewModel.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Send Reset Link'),
|
||||
),
|
||||
if (widget.onBackToLogin != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: _viewModel.isLoading
|
||||
? null
|
||||
: widget.onBackToLogin,
|
||||
child: const Text('Back to Login'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_viewModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../pocketbase_auth.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../view_models/login_view_model.dart';
|
||||
|
||||
class PocketBaseLoginScreen extends StatefulWidget {
|
||||
final AuthService authService;
|
||||
final VoidCallback? onSignup;
|
||||
final VoidCallback? onForgotPassword;
|
||||
|
||||
const PocketBaseLoginScreen({
|
||||
super.key,
|
||||
required this.authService,
|
||||
this.onSignup,
|
||||
this.onForgotPassword,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PocketBaseLoginScreen> createState() => _PocketBaseLoginScreenState();
|
||||
}
|
||||
|
||||
class _PocketBaseLoginScreenState extends State<PocketBaseLoginScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
late final LoginViewModel _viewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_viewModel = LoginViewModel(widget.authService);
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
try {
|
||||
await _viewModel.login(
|
||||
_emailController.text.trim(),
|
||||
_passwordController.text.trim(),
|
||||
);
|
||||
// Navigation is handled by auth state stream
|
||||
} catch (_) {
|
||||
// Error is handled in ViewModel and UI updates via listener
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _viewModel,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Login')),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (_viewModel.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
_viewModel.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your email';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Password',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _viewModel.isLoading ? null : _login,
|
||||
child: _viewModel.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Login'),
|
||||
),
|
||||
if (widget.onForgotPassword != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: _viewModel.isLoading
|
||||
? null
|
||||
: widget.onForgotPassword,
|
||||
child: const Text('Forgot Password?'),
|
||||
),
|
||||
],
|
||||
if (widget.onSignup != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: _viewModel.isLoading
|
||||
? null
|
||||
: widget.onSignup,
|
||||
child: const Text('Create an account'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_viewModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../auth_service.dart';
|
||||
import '../view_models/register_view_model.dart';
|
||||
|
||||
class PocketBaseRegisterScreen extends StatefulWidget {
|
||||
final AuthService authService;
|
||||
final VoidCallback? onLogin;
|
||||
|
||||
const PocketBaseRegisterScreen({
|
||||
super.key,
|
||||
required this.authService,
|
||||
this.onLogin,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PocketBaseRegisterScreen> createState() =>
|
||||
_PocketBaseRegisterScreenState();
|
||||
}
|
||||
|
||||
class _PocketBaseRegisterScreenState extends State<PocketBaseRegisterScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
late final RegisterViewModel _viewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_viewModel = RegisterViewModel(widget.authService);
|
||||
}
|
||||
|
||||
Future<void> _signup() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
try {
|
||||
await _viewModel.signup(
|
||||
_emailController.text.trim(),
|
||||
_passwordController.text.trim(),
|
||||
_confirmPasswordController.text.trim(),
|
||||
);
|
||||
// Navigation is handled by auth state stream
|
||||
} catch (_) {
|
||||
// Error handled by view model
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _viewModel,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Register')),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (_viewModel.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
_viewModel.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your email';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Password',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
}
|
||||
if (value.length < 8) {
|
||||
return 'Password must be at least 8 characters';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _confirmPasswordController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Confirm Password',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please confirm your password';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _viewModel.isLoading ? null : _signup,
|
||||
child: _viewModel.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Register'),
|
||||
),
|
||||
if (widget.onLogin != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: _viewModel.isLoading ? null : widget.onLogin,
|
||||
child: const Text('Already have an account? Login'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
_viewModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../view_models/update_profile_view_model.dart';
|
||||
|
||||
class PocketBaseUpdateProfileScreen extends StatefulWidget {
|
||||
final AuthService authService;
|
||||
|
||||
const PocketBaseUpdateProfileScreen({super.key, required this.authService});
|
||||
|
||||
@override
|
||||
State<PocketBaseUpdateProfileScreen> createState() =>
|
||||
_PocketBaseUpdateProfileScreenState();
|
||||
}
|
||||
|
||||
class _PocketBaseUpdateProfileScreenState
|
||||
extends State<PocketBaseUpdateProfileScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
late final UpdateProfileViewModel _viewModel;
|
||||
bool _isInit = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_viewModel = UpdateProfileViewModel(widget.authService);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_isInit) {
|
||||
final user = widget.authService.currentUser;
|
||||
if (user != null) {
|
||||
_nameController.text = user.getStringValue('name');
|
||||
}
|
||||
_isInit = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateProfile() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
try {
|
||||
await _viewModel.updateProfile(name: _nameController.text.trim());
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Profile updated successfully')),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// Error handled by view model
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: _viewModel,
|
||||
builder: (context, _) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Update Profile')),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (_viewModel.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
_viewModel.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your name';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: _viewModel.isLoading ? null : _updateProfile,
|
||||
child: _viewModel.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Update Profile'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_viewModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../view_models/verify_email_view_model.dart';
|
||||
|
||||
class PocketBaseVerifyEmailScreen extends StatefulWidget {
|
||||
final AuthService authService;
|
||||
|
||||
const PocketBaseVerifyEmailScreen({super.key, required this.authService});
|
||||
|
||||
@override
|
||||
State<PocketBaseVerifyEmailScreen> createState() =>
|
||||
_PocketBaseVerifyEmailScreenState();
|
||||
}
|
||||
|
||||
class _PocketBaseVerifyEmailScreenState
|
||||
extends State<PocketBaseVerifyEmailScreen> {
|
||||
late final VerifyEmailViewModel _viewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_viewModel = VerifyEmailViewModel(widget.authService);
|
||||
}
|
||||
|
||||
Future<void> _requestVerification() async {
|
||||
try {
|
||||
await _viewModel.requestVerification();
|
||||
} catch (_) {
|
||||
// Error handled by view model
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// We need to re-fetch the user to check verification status, or rely on auth state updates.
|
||||
// However, for this simple screen, we might just use the current user from authService.
|
||||
// Ideally, the ViewModel should also expose the user or verification status if it changes.
|
||||
// For now, let's keep using widget.authService.currentUser but maybe we should move that to VM too?
|
||||
// Let's stick to the screen accessing authService for user info as it was doing.
|
||||
final user = widget.authService.currentUser;
|
||||
final isVerified = user?.getBoolValue('verified') ?? false;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: _viewModel,
|
||||
builder: (context, _) {
|
||||
if (_viewModel.success) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Verify Email')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Verification email sent!',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Check your email for instructions to verify your account.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Go Back'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Verify Email')),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (isVerified) ...[
|
||||
const Icon(Icons.verified, color: Colors.green, size: 64),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Your email is verified!',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
] else ...[
|
||||
const Icon(
|
||||
Icons.warning_amber,
|
||||
color: Colors.orange,
|
||||
size: 64,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Your email is not verified.',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Email: ${user?.getStringValue('email')}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (_viewModel.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
_viewModel.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _viewModel.isLoading
|
||||
? null
|
||||
: _requestVerification,
|
||||
child: _viewModel.isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Send Verification Email'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_viewModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
String parseErrorMessage(dynamic error) {
|
||||
if (error is ClientException) {
|
||||
final response = error.response;
|
||||
if (response['message'] != null) {
|
||||
return response['message'] as String;
|
||||
}
|
||||
}
|
||||
return error.toString();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class ChangeEmailViewModel extends ChangeNotifier {
|
||||
final AuthService _authService;
|
||||
|
||||
ChangeEmailViewModel(this._authService);
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
bool _success = false;
|
||||
bool get success => _success;
|
||||
|
||||
Future<void> requestEmailChange(String newEmail) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_success = false;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _authService.requestEmailChange(newEmail);
|
||||
_success = true;
|
||||
} catch (e) {
|
||||
_errorMessage = parseErrorMessage(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class DeleteAccountViewModel extends ChangeNotifier {
|
||||
final AuthService _authService;
|
||||
|
||||
DeleteAccountViewModel(this._authService);
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
Future<void> deleteAccount() async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _authService.deleteAccount();
|
||||
} catch (e) {
|
||||
_errorMessage = parseErrorMessage(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class ForgotPasswordViewModel extends ChangeNotifier {
|
||||
final AuthService _authService;
|
||||
|
||||
ForgotPasswordViewModel(this._authService);
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
bool _success = false;
|
||||
bool get success => _success;
|
||||
|
||||
Future<void> resetPassword(String email) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_success = false;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _authService.resetPassword(email);
|
||||
_success = true;
|
||||
} catch (e) {
|
||||
_errorMessage = parseErrorMessage(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class LoginViewModel extends ChangeNotifier {
|
||||
final AuthService _authService;
|
||||
|
||||
LoginViewModel(this._authService);
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
Future<void> login(String email, String password) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _authService.login(email, password);
|
||||
} catch (e) {
|
||||
_errorMessage = parseErrorMessage(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class RegisterViewModel extends ChangeNotifier {
|
||||
final AuthService _authService;
|
||||
|
||||
RegisterViewModel(this._authService);
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
Future<void> signup(
|
||||
String email,
|
||||
String password,
|
||||
String confirmPassword,
|
||||
) async {
|
||||
if (password != confirmPassword) {
|
||||
_errorMessage = "Passwords do not match";
|
||||
notifyListeners();
|
||||
return; // Or throw custom error
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _authService.signup(email, password);
|
||||
} catch (e) {
|
||||
_errorMessage = parseErrorMessage(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class UpdateProfileViewModel extends ChangeNotifier {
|
||||
final AuthService _authService;
|
||||
|
||||
UpdateProfileViewModel(this._authService);
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
bool _success = false;
|
||||
bool get success => _success;
|
||||
|
||||
Future<void> updateProfile({String? name, String? avatar}) async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_success = false;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final body = <String, dynamic>{};
|
||||
if (name != null) body['name'] = name;
|
||||
if (avatar != null) body['avatar'] = avatar;
|
||||
|
||||
await _authService.updateProfile(body);
|
||||
_success = true;
|
||||
} catch (e) {
|
||||
_errorMessage = parseErrorMessage(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../auth_service.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class VerifyEmailViewModel extends ChangeNotifier {
|
||||
final AuthService _authService;
|
||||
|
||||
VerifyEmailViewModel(this._authService);
|
||||
|
||||
bool _isLoading = false;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
String? _errorMessage;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
bool _success = false;
|
||||
bool get success => _success;
|
||||
|
||||
Future<void> requestVerification() async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_success = false;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _authService.requestVerification();
|
||||
_success = true;
|
||||
} catch (e) {
|
||||
_errorMessage = parseErrorMessage(e);
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user