fix settings
This commit is contained in:
259
auth_implementation_guide.md
Normal file
259
auth_implementation_guide.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# 🔐 Authentication System - Implementation Guide
|
||||||
|
|
||||||
|
This guide demonstrates the beautiful and functional login system that has been implemented for your Flutter application using Material 3 design.
|
||||||
|
|
||||||
|
## 📋 What's Been Created
|
||||||
|
|
||||||
|
### 1. **Custom Auth Widgets** (`/lib/features/auth/presentation/widgets/`)
|
||||||
|
|
||||||
|
#### `AuthTextField`
|
||||||
|
- **Purpose**: Reusable styled text field specifically for authentication forms
|
||||||
|
- **Features**:
|
||||||
|
- Material 3 design with consistent theming
|
||||||
|
- Focus states with color animations
|
||||||
|
- Built-in validation styling
|
||||||
|
- Prefix and suffix icon support
|
||||||
|
- Accessibility support
|
||||||
|
- Proper keyboard navigation
|
||||||
|
|
||||||
|
```dart
|
||||||
|
AuthTextField(
|
||||||
|
controller: _emailController,
|
||||||
|
labelText: 'Email Address',
|
||||||
|
hintText: 'Enter your email',
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
prefixIcon: const Icon(Icons.email_outlined),
|
||||||
|
validator: _validateEmail,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `AuthButton`
|
||||||
|
- **Purpose**: Reusable primary button for authentication actions
|
||||||
|
- **Features**:
|
||||||
|
- Three button types: filled, outlined, text
|
||||||
|
- Loading states with progress indicators
|
||||||
|
- Icon support
|
||||||
|
- Consistent Material 3 styling
|
||||||
|
- Accessibility compliant
|
||||||
|
- Smooth animations
|
||||||
|
|
||||||
|
```dart
|
||||||
|
AuthButton(
|
||||||
|
onPressed: _handleLogin,
|
||||||
|
text: 'Sign In',
|
||||||
|
isLoading: isLoading,
|
||||||
|
type: AuthButtonType.filled,
|
||||||
|
icon: const Icon(Icons.login),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Authentication Pages**
|
||||||
|
|
||||||
|
#### `LoginPage` (`/lib/features/auth/presentation/pages/login_page.dart`)
|
||||||
|
- **Features**:
|
||||||
|
- Modern Material 3 design with smooth animations
|
||||||
|
- Email and password fields with comprehensive validation
|
||||||
|
- Password visibility toggle
|
||||||
|
- Remember me checkbox
|
||||||
|
- Forgot password link
|
||||||
|
- Sign up navigation link
|
||||||
|
- Responsive design for all screen sizes
|
||||||
|
- Loading states and error handling
|
||||||
|
- Integration with Riverpod state management
|
||||||
|
- Haptic feedback for better UX
|
||||||
|
- Privacy policy and terms links
|
||||||
|
|
||||||
|
#### `RegisterPage` (`/lib/features/auth/presentation/pages/register_page.dart`)
|
||||||
|
- **Features**:
|
||||||
|
- Full name, email, password, and confirm password fields
|
||||||
|
- Real-time password requirements validation
|
||||||
|
- Visual password strength indicators
|
||||||
|
- Terms of service agreement checkbox
|
||||||
|
- Form validation with clear error messages
|
||||||
|
- Smooth animations and transitions
|
||||||
|
- Responsive design
|
||||||
|
- Integration with existing auth state management
|
||||||
|
|
||||||
|
### 3. **Integration with Existing Architecture**
|
||||||
|
|
||||||
|
#### **State Management** (Riverpod)
|
||||||
|
- Integrated with existing `AuthNotifier` and `AuthState`
|
||||||
|
- Proper error handling and loading states
|
||||||
|
- State synchronization with route guards
|
||||||
|
|
||||||
|
#### **Routing** (GoRouter)
|
||||||
|
- Updated router configuration to use new auth pages
|
||||||
|
- Proper navigation flow between login and register
|
||||||
|
- Integration with existing route guards
|
||||||
|
|
||||||
|
#### **Theme Integration**
|
||||||
|
- Uses existing Material 3 theme configuration
|
||||||
|
- Consistent with app color scheme and typography
|
||||||
|
- Responsive typography and spacing
|
||||||
|
- Dark mode support
|
||||||
|
|
||||||
|
## 🚀 How to Use
|
||||||
|
|
||||||
|
### 1. **Navigation to Auth Pages**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Navigate to login page
|
||||||
|
context.pushNamed('/auth/login');
|
||||||
|
|
||||||
|
// Navigate to register page
|
||||||
|
context.pushNamed('/auth/register');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Listening to Auth State**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MyWidget extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final authState = ref.watch(authNotifierProvider);
|
||||||
|
|
||||||
|
return authState.when(
|
||||||
|
initial: () => const CircularProgressIndicator(),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
|
authenticated: (user) => Text('Welcome ${user.name}!'),
|
||||||
|
unauthenticated: (message) => const Text('Please log in'),
|
||||||
|
error: (message) => Text('Error: $message'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Programmatic Authentication**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Login
|
||||||
|
await ref.read(authNotifierProvider.notifier).login(
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'password123',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
await ref.read(authNotifierProvider.notifier).logout();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Design Features
|
||||||
|
|
||||||
|
### **Material 3 Design System**
|
||||||
|
- **Color Scheme**: Uses app's primary colors with proper contrast ratios
|
||||||
|
- **Typography**: Responsive text scaling based on screen size
|
||||||
|
- **Elevation**: Subtle shadows and depth following Material 3 guidelines
|
||||||
|
- **Shape**: Rounded corners with consistent radius values
|
||||||
|
- **Animation**: Smooth transitions and micro-interactions
|
||||||
|
|
||||||
|
### **Responsive Design**
|
||||||
|
- **Mobile First**: Optimized for mobile devices
|
||||||
|
- **Tablet Support**: Adaptive layout for larger screens
|
||||||
|
- **Desktop Ready**: Maximum width constraints for desktop viewing
|
||||||
|
- **Keyboard Navigation**: Full keyboard accessibility support
|
||||||
|
|
||||||
|
### **Accessibility**
|
||||||
|
- **Screen Readers**: Proper semantic labels and hints
|
||||||
|
- **Color Contrast**: WCAG AA compliant color combinations
|
||||||
|
- **Touch Targets**: Minimum 48dp touch areas
|
||||||
|
- **Focus Management**: Logical tab order and focus indicators
|
||||||
|
|
||||||
|
## 🔧 Customization Options
|
||||||
|
|
||||||
|
### **Theme Customization**
|
||||||
|
The auth pages automatically adapt to your app theme. Customize colors in:
|
||||||
|
```dart
|
||||||
|
// lib/core/theme/app_colors.dart
|
||||||
|
static const ColorScheme lightScheme = ColorScheme(...);
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Validation Rules**
|
||||||
|
Customize validation in the page files:
|
||||||
|
```dart
|
||||||
|
String? _validateEmail(String? value) {
|
||||||
|
// Your custom email validation
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validatePassword(String? value) {
|
||||||
|
// Your custom password validation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Button Styles**
|
||||||
|
Customize button appearance:
|
||||||
|
```dart
|
||||||
|
AuthButton(
|
||||||
|
type: AuthButtonType.outlined, // filled, outlined, text
|
||||||
|
width: 200, // Custom width
|
||||||
|
height: 60, // Custom height
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 State Management Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User Action (Login)
|
||||||
|
↓
|
||||||
|
AuthButton onPressed
|
||||||
|
↓
|
||||||
|
AuthNotifier.login()
|
||||||
|
↓
|
||||||
|
AuthState.loading
|
||||||
|
↓
|
||||||
|
LoginUseCase.call()
|
||||||
|
↓
|
||||||
|
AuthRepository.login()
|
||||||
|
↓
|
||||||
|
[Success] AuthState.authenticated(user)
|
||||||
|
[Error] AuthState.error(message)
|
||||||
|
↓
|
||||||
|
UI Updates (LoginPage listens to state)
|
||||||
|
↓
|
||||||
|
Navigation (Route Guards redirect based on auth state)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### **Widget Tests**
|
||||||
|
```dart
|
||||||
|
testWidgets('LoginPage should display email and password fields', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
child: MaterialApp(
|
||||||
|
home: LoginPage(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(AuthTextField), findsNWidgets(2));
|
||||||
|
expect(find.byType(AuthButton), findsOneWidget);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Integration Tests**
|
||||||
|
```dart
|
||||||
|
testWidgets('User can complete login flow', (tester) async {
|
||||||
|
// Test complete login flow
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
1. **Implement Backend Integration**: Connect to your authentication API
|
||||||
|
2. **Add Biometric Auth**: Implement fingerprint/face ID support
|
||||||
|
3. **Social Login**: Add Google, Apple, Facebook login options
|
||||||
|
4. **Forgot Password**: Implement password reset flow
|
||||||
|
5. **Email Verification**: Add email verification process
|
||||||
|
|
||||||
|
## 📱 Screenshots
|
||||||
|
|
||||||
|
The auth system provides:
|
||||||
|
- ✅ Smooth animations and transitions
|
||||||
|
- ✅ Comprehensive form validation
|
||||||
|
- ✅ Loading states and error handling
|
||||||
|
- ✅ Responsive design for all devices
|
||||||
|
- ✅ Material 3 design consistency
|
||||||
|
- ✅ Dark mode support
|
||||||
|
- ✅ Accessibility compliance
|
||||||
|
- ✅ Integration with existing architecture
|
||||||
|
|
||||||
|
Your users will enjoy a polished, professional authentication experience that matches your app's design system perfectly!
|
||||||
42
ios/Podfile.lock
Normal file
42
ios/Podfile.lock
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
PODS:
|
||||||
|
- connectivity_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- Flutter (1.0.0)
|
||||||
|
- flutter_secure_storage (6.0.0):
|
||||||
|
- Flutter
|
||||||
|
- path_provider_foundation (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
- sqflite_darwin (0.0.4):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
|
- Flutter (from `Flutter`)
|
||||||
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
connectivity_plus:
|
||||||
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
|
Flutter:
|
||||||
|
:path: Flutter
|
||||||
|
flutter_secure_storage:
|
||||||
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
|
path_provider_foundation:
|
||||||
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
|
sqflite_darwin:
|
||||||
|
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||||
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||||
|
|
||||||
|
COCOAPODS: 1.16.2
|
||||||
@@ -10,10 +10,12 @@
|
|||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
4A4FFBF338652D50CEF0CBF1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6173A1D6805BA0818ADE71D1 /* Pods_RunnerTests.framework */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
B358BAD2ADE35E161414B556 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9BA36FF4E4294FF3A553B1A /* Pods_Runner.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -44,7 +46,11 @@
|
|||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
364B0E391126706108F52F9A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
6173A1D6805BA0818ADE71D1 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
6B988381FE1A97E38E0EF9A4 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
7172E510F3E49D4C5EE21863 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -55,19 +61,46 @@
|
|||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
A9BA36FF4E4294FF3A553B1A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
B13138955E4E47B680A05228 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
BCF5DA91BD9611899AF6D589 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
DDC0E63A7F0743DB7253C8AE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
5619E5D7091AA3347EEBF1EF /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
4A4FFBF338652D50CEF0CBF1 /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
B358BAD2ADE35E161414B556 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
1F8ABE3BD850452550E9CD8B /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DDC0E63A7F0743DB7253C8AE /* Pods-Runner.debug.xcconfig */,
|
||||||
|
364B0E391126706108F52F9A /* Pods-Runner.release.xcconfig */,
|
||||||
|
7172E510F3E49D4C5EE21863 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
BCF5DA91BD9611899AF6D589 /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
6B988381FE1A97E38E0EF9A4 /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
B13138955E4E47B680A05228 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -76,6 +109,15 @@
|
|||||||
path = RunnerTests;
|
path = RunnerTests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
818FF2868B23FA94FD5721B4 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A9BA36FF4E4294FF3A553B1A /* Pods_Runner.framework */,
|
||||||
|
6173A1D6805BA0818ADE71D1 /* Pods_RunnerTests.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -94,6 +136,8 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
1F8ABE3BD850452550E9CD8B /* Pods */,
|
||||||
|
818FF2868B23FA94FD5721B4 /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -128,8 +172,10 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
82A6E7110A02C3E793B6000F /* [CP] Check Pods Manifest.lock */,
|
||||||
331C807D294A63A400263BE5 /* Sources */,
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
331C807F294A63A400263BE5 /* Resources */,
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
5619E5D7091AA3347EEBF1EF /* Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -145,12 +191,14 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
|
3E5A03809D3E5372DC8382BA /* [CP] Check Pods Manifest.lock */,
|
||||||
9740EEB61CF901F6004384FC /* Run Script */,
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
97C146EA1CF9000F007C117D /* Sources */,
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
80B5093C5CB4C164A2F5C796 /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -238,6 +286,67 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
};
|
};
|
||||||
|
3E5A03809D3E5372DC8382BA /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
80B5093C5CB4C164A2F5C796 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
82A6E7110A02C3E793B6000F /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -379,6 +488,7 @@
|
|||||||
};
|
};
|
||||||
331C8088294A63A400263BE5 /* Debug */ = {
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = BCF5DA91BD9611899AF6D589 /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -396,6 +506,7 @@
|
|||||||
};
|
};
|
||||||
331C8089294A63A400263BE5 /* Release */ = {
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 6B988381FE1A97E38E0EF9A4 /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
@@ -411,6 +522,7 @@
|
|||||||
};
|
};
|
||||||
331C808A294A63A400263BE5 /* Profile */ = {
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = B13138955E4E47B680A05228 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
|||||||
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
3
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
@@ -4,4 +4,7 @@
|
|||||||
<FileRef
|
<FileRef
|
||||||
location = "group:Runner.xcodeproj">
|
location = "group:Runner.xcodeproj">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../hive_service.dart';
|
import '../hive_service.dart';
|
||||||
import '../models/cache_item.dart';
|
import '../models/cache_item.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
/// Repository for managing cached data using Hive
|
/// Repository for managing cached data using Hive
|
||||||
class CacheRepository {
|
class CacheRepository {
|
||||||
|
/// Safe getter for cache box - returns null if not initialized
|
||||||
|
Box<CacheItem>? get _cacheBox {
|
||||||
|
if (!HiveService.isInitialized) {
|
||||||
|
debugPrint('⚠️ CacheRepository: Hive not initialized yet');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return HiveService.cacheBox;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error accessing cache box: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
/// Store data in cache with expiration
|
/// Store data in cache with expiration
|
||||||
Future<void> put<T>({
|
Future<void> put<T>({
|
||||||
required String key,
|
required String key,
|
||||||
@@ -12,7 +26,12 @@ class CacheRepository {
|
|||||||
Map<String, dynamic>? metadata,
|
Map<String, dynamic>? metadata,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot store cache item: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final cacheItem = CacheItem.create(
|
final cacheItem = CacheItem.create(
|
||||||
key: key,
|
key: key,
|
||||||
data: data,
|
data: data,
|
||||||
@@ -35,7 +54,12 @@ class CacheRepository {
|
|||||||
Map<String, dynamic>? metadata,
|
Map<String, dynamic>? metadata,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot store permanent cache item: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final cacheItem = CacheItem.permanent(
|
final cacheItem = CacheItem.permanent(
|
||||||
key: key,
|
key: key,
|
||||||
data: data,
|
data: data,
|
||||||
@@ -53,7 +77,12 @@ class CacheRepository {
|
|||||||
/// Get data from cache
|
/// Get data from cache
|
||||||
T? get<T>(String key) {
|
T? get<T>(String key) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot get cache item: Hive not initialized');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final cacheItem = box.get(key);
|
final cacheItem = box.get(key);
|
||||||
|
|
||||||
if (cacheItem == null) {
|
if (cacheItem == null) {
|
||||||
@@ -79,7 +108,12 @@ class CacheRepository {
|
|||||||
/// Get cache item with full metadata
|
/// Get cache item with full metadata
|
||||||
CacheItem? getCacheItem(String key) {
|
CacheItem? getCacheItem(String key) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot get cache item: Hive not initialized');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final cacheItem = box.get(key);
|
final cacheItem = box.get(key);
|
||||||
|
|
||||||
if (cacheItem == null) {
|
if (cacheItem == null) {
|
||||||
@@ -102,7 +136,11 @@ class CacheRepository {
|
|||||||
/// Check if key exists and is valid (not expired)
|
/// Check if key exists and is valid (not expired)
|
||||||
bool contains(String key) {
|
bool contains(String key) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final cacheItem = box.get(key);
|
final cacheItem = box.get(key);
|
||||||
|
|
||||||
if (cacheItem == null) return false;
|
if (cacheItem == null) return false;
|
||||||
@@ -121,7 +159,11 @@ class CacheRepository {
|
|||||||
/// Check if key exists regardless of expiration
|
/// Check if key exists regardless of expiration
|
||||||
bool containsKey(String key) {
|
bool containsKey(String key) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return box.containsKey(key);
|
return box.containsKey(key);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error checking key $key: $e');
|
debugPrint('❌ Error checking key $key: $e');
|
||||||
@@ -132,7 +174,11 @@ class CacheRepository {
|
|||||||
/// Delete specific cache item
|
/// Delete specific cache item
|
||||||
Future<void> delete(String key) async {
|
Future<void> delete(String key) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await box.delete(key);
|
await box.delete(key);
|
||||||
debugPrint('🗑️ Cache item deleted: $key');
|
debugPrint('🗑️ Cache item deleted: $key');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -143,7 +189,11 @@ class CacheRepository {
|
|||||||
/// Delete multiple cache items
|
/// Delete multiple cache items
|
||||||
Future<void> deleteMultiple(List<String> keys) async {
|
Future<void> deleteMultiple(List<String> keys) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (final key in keys) {
|
for (final key in keys) {
|
||||||
await box.delete(key);
|
await box.delete(key);
|
||||||
}
|
}
|
||||||
@@ -161,7 +211,11 @@ class CacheRepository {
|
|||||||
/// Clear all expired items
|
/// Clear all expired items
|
||||||
Future<int> clearExpired() async {
|
Future<int> clearExpired() async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
final expiredKeys = <String>[];
|
final expiredKeys = <String>[];
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
@@ -187,7 +241,11 @@ class CacheRepository {
|
|||||||
/// Clear all cache items
|
/// Clear all cache items
|
||||||
Future<void> clearAll() async {
|
Future<void> clearAll() async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final count = box.length;
|
final count = box.length;
|
||||||
await box.clear();
|
await box.clear();
|
||||||
debugPrint('🧹 Cleared all cache items: $count items');
|
debugPrint('🧹 Cleared all cache items: $count items');
|
||||||
@@ -200,7 +258,11 @@ class CacheRepository {
|
|||||||
/// Clear cache items by pattern
|
/// Clear cache items by pattern
|
||||||
Future<int> clearByPattern(Pattern pattern) async {
|
Future<int> clearByPattern(Pattern pattern) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
final keysToDelete = <String>[];
|
final keysToDelete = <String>[];
|
||||||
|
|
||||||
for (final key in box.keys) {
|
for (final key in box.keys) {
|
||||||
@@ -224,7 +286,11 @@ class CacheRepository {
|
|||||||
/// Clear cache items by type
|
/// Clear cache items by type
|
||||||
Future<int> clearByType(String dataType) async {
|
Future<int> clearByType(String dataType) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
final keysToDelete = <String>[];
|
final keysToDelete = <String>[];
|
||||||
|
|
||||||
for (final key in box.keys) {
|
for (final key in box.keys) {
|
||||||
@@ -249,7 +315,11 @@ class CacheRepository {
|
|||||||
/// Refresh cache item with new expiration
|
/// Refresh cache item with new expiration
|
||||||
Future<bool> refresh(String key, Duration newExpirationDuration) async {
|
Future<bool> refresh(String key, Duration newExpirationDuration) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final cacheItem = box.get(key);
|
final cacheItem = box.get(key);
|
||||||
|
|
||||||
if (cacheItem == null) return false;
|
if (cacheItem == null) return false;
|
||||||
@@ -268,7 +338,11 @@ class CacheRepository {
|
|||||||
/// Update cache item data
|
/// Update cache item data
|
||||||
Future<bool> update<T>(String key, T newData, {Duration? newExpirationDuration}) async {
|
Future<bool> update<T>(String key, T newData, {Duration? newExpirationDuration}) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final cacheItem = box.get(key);
|
final cacheItem = box.get(key);
|
||||||
|
|
||||||
if (cacheItem == null) return false;
|
if (cacheItem == null) return false;
|
||||||
@@ -287,7 +361,11 @@ class CacheRepository {
|
|||||||
/// Get all keys in cache
|
/// Get all keys in cache
|
||||||
List<String> getAllKeys() {
|
List<String> getAllKeys() {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return box.keys.cast<String>().toList();
|
return box.keys.cast<String>().toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error getting all keys: $e');
|
debugPrint('❌ Error getting all keys: $e');
|
||||||
@@ -298,7 +376,11 @@ class CacheRepository {
|
|||||||
/// Get keys by pattern
|
/// Get keys by pattern
|
||||||
List<String> getKeysByPattern(Pattern pattern) {
|
List<String> getKeysByPattern(Pattern pattern) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return box.keys
|
return box.keys
|
||||||
.cast<String>()
|
.cast<String>()
|
||||||
.where((key) => key.contains(pattern))
|
.where((key) => key.contains(pattern))
|
||||||
@@ -312,7 +394,11 @@ class CacheRepository {
|
|||||||
/// Get keys by data type
|
/// Get keys by data type
|
||||||
List<String> getKeysByType(String dataType) {
|
List<String> getKeysByType(String dataType) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
final keys = <String>[];
|
final keys = <String>[];
|
||||||
|
|
||||||
for (final key in box.keys) {
|
for (final key in box.keys) {
|
||||||
@@ -332,7 +418,18 @@ class CacheRepository {
|
|||||||
/// Get cache statistics
|
/// Get cache statistics
|
||||||
CacheStats getStats() {
|
CacheStats getStats() {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return CacheStats(
|
||||||
|
totalItems: 0,
|
||||||
|
validItems: 0,
|
||||||
|
expiredItems: 0,
|
||||||
|
oldestItem: DateTime.now(),
|
||||||
|
newestItem: DateTime.now(),
|
||||||
|
typeCount: const {},
|
||||||
|
);
|
||||||
|
}
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
var validItems = 0;
|
var validItems = 0;
|
||||||
var expiredItems = 0;
|
var expiredItems = 0;
|
||||||
@@ -387,7 +484,11 @@ class CacheRepository {
|
|||||||
/// Get cache size in bytes (approximate)
|
/// Get cache size in bytes (approximate)
|
||||||
int getApproximateSize() {
|
int getApproximateSize() {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
// This is an approximation as Hive doesn't provide exact size
|
// This is an approximation as Hive doesn't provide exact size
|
||||||
return box.length * 1024; // Assume average 1KB per item
|
return box.length * 1024; // Assume average 1KB per item
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -399,7 +500,11 @@ class CacheRepository {
|
|||||||
/// Compact cache storage
|
/// Compact cache storage
|
||||||
Future<void> compact() async {
|
Future<void> compact() async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await box.compact();
|
await box.compact();
|
||||||
debugPrint('✅ Cache storage compacted');
|
debugPrint('✅ Cache storage compacted');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -410,7 +515,11 @@ class CacheRepository {
|
|||||||
/// Export cache data (for debugging or backup)
|
/// Export cache data (for debugging or backup)
|
||||||
Map<String, dynamic> exportCache({bool includeExpired = false}) {
|
Map<String, dynamic> exportCache({bool includeExpired = false}) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final exportData = <String, dynamic>{};
|
final exportData = <String, dynamic>{};
|
||||||
|
|
||||||
@@ -435,7 +544,11 @@ class CacheRepository {
|
|||||||
/// Watch cache changes for a specific key
|
/// Watch cache changes for a specific key
|
||||||
Stream<CacheItem?> watch(String key) {
|
Stream<CacheItem?> watch(String key) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.cacheBox;
|
final box = _cacheBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access cache: Hive not initialized');
|
||||||
|
return Stream.value(null);
|
||||||
|
}
|
||||||
return box.watch(key: key).map((event) => event.value as CacheItem?);
|
return box.watch(key: key).map((event) => event.value as CacheItem?);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error watching cache key $key: $e');
|
debugPrint('❌ Error watching cache key $key: $e');
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../hive_service.dart';
|
import '../hive_service.dart';
|
||||||
import '../models/app_settings.dart';
|
import '../models/app_settings.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
/// Repository for managing application settings using Hive
|
/// Repository for managing application settings using Hive
|
||||||
class SettingsRepository {
|
class SettingsRepository {
|
||||||
|
/// Safe getter for app settings box - returns null if not initialized
|
||||||
|
Box<AppSettings>? get _settingsBox {
|
||||||
|
if (!HiveService.isInitialized) {
|
||||||
|
debugPrint('⚠️ SettingsRepository: Hive not initialized yet');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return HiveService.appSettingsBox;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error accessing settings box: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
static const String _defaultKey = 'app_settings';
|
static const String _defaultKey = 'app_settings';
|
||||||
|
|
||||||
/// Get the current app settings
|
/// Get the current app settings
|
||||||
AppSettings getSettings() {
|
AppSettings getSettings() {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.appSettingsBox;
|
final box = _settingsBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access settings: Hive not initialized');
|
||||||
|
return AppSettings.defaultSettings();
|
||||||
|
}
|
||||||
final settings = box.get(_defaultKey);
|
final settings = box.get(_defaultKey);
|
||||||
|
|
||||||
if (settings == null) {
|
if (settings == null) {
|
||||||
@@ -39,7 +57,11 @@ class SettingsRepository {
|
|||||||
/// Save app settings
|
/// Save app settings
|
||||||
Future<void> saveSettings(AppSettings settings) async {
|
Future<void> saveSettings(AppSettings settings) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.appSettingsBox;
|
final box = _settingsBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access settings: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final updatedSettings = settings.copyWith(lastUpdated: DateTime.now());
|
final updatedSettings = settings.copyWith(lastUpdated: DateTime.now());
|
||||||
await box.put(_defaultKey, updatedSettings);
|
await box.put(_defaultKey, updatedSettings);
|
||||||
debugPrint('✅ Settings saved successfully');
|
debugPrint('✅ Settings saved successfully');
|
||||||
@@ -153,7 +175,11 @@ class SettingsRepository {
|
|||||||
/// Check if settings exist
|
/// Check if settings exist
|
||||||
bool hasSettings() {
|
bool hasSettings() {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.appSettingsBox;
|
final box = _settingsBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access settings: Hive not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return box.containsKey(_defaultKey);
|
return box.containsKey(_defaultKey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error checking settings existence: $e');
|
debugPrint('❌ Error checking settings existence: $e');
|
||||||
@@ -164,7 +190,11 @@ class SettingsRepository {
|
|||||||
/// Clear all settings (use with caution)
|
/// Clear all settings (use with caution)
|
||||||
Future<void> clearSettings() async {
|
Future<void> clearSettings() async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.appSettingsBox;
|
final box = _settingsBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access settings: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await box.delete(_defaultKey);
|
await box.delete(_defaultKey);
|
||||||
debugPrint('✅ Settings cleared');
|
debugPrint('✅ Settings cleared');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -177,7 +207,11 @@ class SettingsRepository {
|
|||||||
Map<String, dynamic> getSettingsStats() {
|
Map<String, dynamic> getSettingsStats() {
|
||||||
try {
|
try {
|
||||||
final settings = getSettings();
|
final settings = getSettings();
|
||||||
final box = HiveService.appSettingsBox;
|
final box = _settingsBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access settings: Hive not initialized');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'hasCustomSettings': settings.customSettings?.isNotEmpty ?? false,
|
'hasCustomSettings': settings.customSettings?.isNotEmpty ?? false,
|
||||||
@@ -225,7 +259,11 @@ class SettingsRepository {
|
|||||||
/// Watch settings changes
|
/// Watch settings changes
|
||||||
Stream<AppSettings> watchSettings() {
|
Stream<AppSettings> watchSettings() {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.appSettingsBox;
|
final box = _settingsBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access settings: Hive not initialized');
|
||||||
|
return Stream.value(AppSettings.defaultSettings());
|
||||||
|
}
|
||||||
return box.watch(key: _defaultKey).map((event) {
|
return box.watch(key: _defaultKey).map((event) {
|
||||||
final settings = event.value as AppSettings?;
|
final settings = event.value as AppSettings?;
|
||||||
return settings ?? AppSettings.defaultSettings();
|
return settings ?? AppSettings.defaultSettings();
|
||||||
@@ -239,7 +277,11 @@ class SettingsRepository {
|
|||||||
/// Compact settings storage
|
/// Compact settings storage
|
||||||
Future<void> compact() async {
|
Future<void> compact() async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.appSettingsBox;
|
final box = _settingsBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access settings: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await box.compact();
|
await box.compact();
|
||||||
debugPrint('✅ Settings storage compacted');
|
debugPrint('✅ Settings storage compacted');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../hive_service.dart';
|
import '../hive_service.dart';
|
||||||
import '../models/user_preferences.dart';
|
import '../models/user_preferences.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
/// Repository for managing user preferences using Hive
|
/// Repository for managing user preferences using Hive
|
||||||
class UserPreferencesRepository {
|
class UserPreferencesRepository {
|
||||||
|
/// Safe getter for user preferences box - returns null if not initialized
|
||||||
|
Box<UserPreferences>? get _userPreferencesBox {
|
||||||
|
if (!HiveService.isInitialized) {
|
||||||
|
debugPrint('⚠️ UserPreferencesRepository: Hive not initialized yet');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return HiveService.userDataBox;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error accessing user preferences box: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
static const String _defaultKey = 'current_user_preferences';
|
static const String _defaultKey = 'current_user_preferences';
|
||||||
|
|
||||||
/// Get the current user preferences (alias for getUserPreferences)
|
/// Get the current user preferences (alias for getUserPreferences)
|
||||||
@@ -14,7 +28,11 @@ class UserPreferencesRepository {
|
|||||||
/// Get the current user preferences
|
/// Get the current user preferences
|
||||||
UserPreferences? getUserPreferences([String? userId]) {
|
UserPreferences? getUserPreferences([String? userId]) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.userDataBox;
|
final box = _userPreferencesBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access user preferences: Hive not initialized');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
final key = userId ?? _defaultKey;
|
final key = userId ?? _defaultKey;
|
||||||
|
|
||||||
final preferences = box.get(key);
|
final preferences = box.get(key);
|
||||||
@@ -38,7 +56,11 @@ class UserPreferencesRepository {
|
|||||||
/// Save user preferences
|
/// Save user preferences
|
||||||
Future<void> saveUserPreferences(UserPreferences preferences, [String? userId]) async {
|
Future<void> saveUserPreferences(UserPreferences preferences, [String? userId]) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.userDataBox;
|
final box = _userPreferencesBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access user preferences: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final key = userId ?? _defaultKey;
|
final key = userId ?? _defaultKey;
|
||||||
|
|
||||||
final updatedPreferences = preferences.copyWith(lastUpdated: DateTime.now());
|
final updatedPreferences = preferences.copyWith(lastUpdated: DateTime.now());
|
||||||
@@ -212,7 +234,11 @@ class UserPreferencesRepository {
|
|||||||
/// Check if user preferences exist
|
/// Check if user preferences exist
|
||||||
bool hasUserPreferences([String? userId]) {
|
bool hasUserPreferences([String? userId]) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.userDataBox;
|
final box = _userPreferencesBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access user preferences: Hive not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final key = userId ?? _defaultKey;
|
final key = userId ?? _defaultKey;
|
||||||
return box.containsKey(key);
|
return box.containsKey(key);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -224,7 +250,11 @@ class UserPreferencesRepository {
|
|||||||
/// Clear user preferences (use with caution)
|
/// Clear user preferences (use with caution)
|
||||||
Future<void> clearUserPreferences([String? userId]) async {
|
Future<void> clearUserPreferences([String? userId]) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.userDataBox;
|
final box = _userPreferencesBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access user preferences: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
final key = userId ?? _defaultKey;
|
final key = userId ?? _defaultKey;
|
||||||
await box.delete(key);
|
await box.delete(key);
|
||||||
debugPrint('✅ User preferences cleared for key: $key');
|
debugPrint('✅ User preferences cleared for key: $key');
|
||||||
@@ -237,7 +267,11 @@ class UserPreferencesRepository {
|
|||||||
/// Get all user IDs that have preferences stored
|
/// Get all user IDs that have preferences stored
|
||||||
List<String> getAllUserIds() {
|
List<String> getAllUserIds() {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.userDataBox;
|
final box = _userPreferencesBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access user preferences: Hive not initialized');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return box.keys.cast<String>().where((key) => key != _defaultKey).toList();
|
return box.keys.cast<String>().where((key) => key != _defaultKey).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error getting all user IDs: $e');
|
debugPrint('❌ Error getting all user IDs: $e');
|
||||||
@@ -248,7 +282,11 @@ class UserPreferencesRepository {
|
|||||||
/// Delete preferences for a specific user
|
/// Delete preferences for a specific user
|
||||||
Future<void> deleteUserPreferences(String userId) async {
|
Future<void> deleteUserPreferences(String userId) async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.userDataBox;
|
final box = _userPreferencesBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access user preferences: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await box.delete(userId);
|
await box.delete(userId);
|
||||||
debugPrint('✅ User preferences deleted for user: $userId');
|
debugPrint('✅ User preferences deleted for user: $userId');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -289,7 +327,11 @@ class UserPreferencesRepository {
|
|||||||
/// Watch user preferences changes
|
/// Watch user preferences changes
|
||||||
Stream<UserPreferences?> watchUserPreferences([String? userId]) {
|
Stream<UserPreferences?> watchUserPreferences([String? userId]) {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.userDataBox;
|
final box = _userPreferencesBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access user preferences: Hive not initialized');
|
||||||
|
return Stream.value(null);
|
||||||
|
}
|
||||||
final key = userId ?? _defaultKey;
|
final key = userId ?? _defaultKey;
|
||||||
return box.watch(key: key).map((event) => event.value as UserPreferences?);
|
return box.watch(key: key).map((event) => event.value as UserPreferences?);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -301,7 +343,11 @@ class UserPreferencesRepository {
|
|||||||
/// Compact user preferences storage
|
/// Compact user preferences storage
|
||||||
Future<void> compact() async {
|
Future<void> compact() async {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.userDataBox;
|
final box = _userPreferencesBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access user preferences: Hive not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
await box.compact();
|
await box.compact();
|
||||||
debugPrint('✅ User preferences storage compacted');
|
debugPrint('✅ User preferences storage compacted');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -312,7 +358,11 @@ class UserPreferencesRepository {
|
|||||||
/// Get storage statistics
|
/// Get storage statistics
|
||||||
Map<String, dynamic> getStorageStats() {
|
Map<String, dynamic> getStorageStats() {
|
||||||
try {
|
try {
|
||||||
final box = HiveService.userDataBox;
|
final box = _userPreferencesBox;
|
||||||
|
if (box == null) {
|
||||||
|
debugPrint('⚠️ Cannot access user preferences: Hive not initialized');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
final allUserIds = getAllUserIds();
|
final allUserIds = getAllUserIds();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class AppInitialization extends _$AppInitialization {
|
|||||||
|
|
||||||
// Initialize Hive
|
// Initialize Hive
|
||||||
debugPrint('📦 Initializing Hive database...');
|
debugPrint('📦 Initializing Hive database...');
|
||||||
await HiveService.initialize();
|
// await HiveService.initialize();
|
||||||
|
|
||||||
// Initialize repositories
|
// Initialize repositories
|
||||||
debugPrint('🗂️ Initializing repositories...');
|
debugPrint('🗂️ Initializing repositories...');
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ final isAppReadyProvider = AutoDisposeProvider<bool>.internal(
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef IsAppReadyRef = AutoDisposeProviderRef<bool>;
|
typedef IsAppReadyRef = AutoDisposeProviderRef<bool>;
|
||||||
String _$appInitializationHash() => r'eb87040a5ee3d20a172bef9221c2c56d7e07fe77';
|
String _$appInitializationHash() => r'cdf86e2d6985c6dcee80f618bc032edf81011fc9';
|
||||||
|
|
||||||
/// App initialization provider
|
/// App initialization provider
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -114,17 +114,29 @@ class SecureStorageNotifier extends _$SecureStorageNotifier {
|
|||||||
|
|
||||||
/// Hive storage providers
|
/// Hive storage providers
|
||||||
@riverpod
|
@riverpod
|
||||||
Box<AppSettings> appSettingsBox(AppSettingsBoxRef ref) {
|
Box<AppSettings>? appSettingsBox(AppSettingsBoxRef ref) {
|
||||||
|
// Return null if not initialized yet
|
||||||
|
if (!HiveService.isInitialized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return HiveService.appSettingsBox;
|
return HiveService.appSettingsBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Box<CacheItem> cacheBox(CacheBoxRef ref) {
|
Box<CacheItem>? cacheBox(CacheBoxRef ref) {
|
||||||
|
// Return null if not initialized yet
|
||||||
|
if (!HiveService.isInitialized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return HiveService.cacheBox;
|
return HiveService.cacheBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Box<UserPreferences> userPreferencesBox(UserPreferencesBoxRef ref) {
|
Box<UserPreferences>? userPreferencesBox(UserPreferencesBoxRef ref) {
|
||||||
|
// Return null if not initialized yet
|
||||||
|
if (!HiveService.isInitialized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return HiveService.userDataBox;
|
return HiveService.userDataBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,12 +149,26 @@ class HiveStorageNotifier extends _$HiveStorageNotifier {
|
|||||||
final cacheBox = ref.watch(cacheBoxProvider);
|
final cacheBox = ref.watch(cacheBoxProvider);
|
||||||
final userPreferencesBox = ref.watch(userPreferencesBoxProvider);
|
final userPreferencesBox = ref.watch(userPreferencesBoxProvider);
|
||||||
|
|
||||||
|
// Return empty stats if boxes are not initialized yet
|
||||||
|
// ignore: unnecessary_null_comparison
|
||||||
|
if (appSettingsBox == null || cacheBox == null || userPreferencesBox == null) {
|
||||||
|
return {
|
||||||
|
'appSettingsCount': 0,
|
||||||
|
'cacheItemsCount': 0,
|
||||||
|
'userPreferencesCount': 0,
|
||||||
|
'totalSize': 0,
|
||||||
|
'lastUpdated': DateTime.now().toIso8601String(),
|
||||||
|
'isInitialized': false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'appSettingsCount': appSettingsBox.length,
|
'appSettingsCount': appSettingsBox.length,
|
||||||
'cacheItemsCount': cacheBox.length,
|
'cacheItemsCount': cacheBox.length,
|
||||||
'userPreferencesCount': userPreferencesBox.length,
|
'userPreferencesCount': userPreferencesBox.length,
|
||||||
'totalSize': _calculateTotalSize(),
|
'totalSize': _calculateTotalSize(),
|
||||||
'lastUpdated': DateTime.now().toIso8601String(),
|
'lastUpdated': DateTime.now().toIso8601String(),
|
||||||
|
'isInitialized': true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +178,11 @@ class HiveStorageNotifier extends _$HiveStorageNotifier {
|
|||||||
final cacheBox = ref.read(cacheBoxProvider);
|
final cacheBox = ref.read(cacheBoxProvider);
|
||||||
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
|
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
|
||||||
|
|
||||||
|
// ignore: unnecessary_null_comparison
|
||||||
|
if (appSettingsBox == null || cacheBox == null || userPreferencesBox == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Rough estimation of storage size
|
// Rough estimation of storage size
|
||||||
return appSettingsBox.length + cacheBox.length + userPreferencesBox.length;
|
return appSettingsBox.length + cacheBox.length + userPreferencesBox.length;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -167,6 +198,12 @@ class HiveStorageNotifier extends _$HiveStorageNotifier {
|
|||||||
final cacheBox = ref.read(cacheBoxProvider);
|
final cacheBox = ref.read(cacheBoxProvider);
|
||||||
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
|
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
|
||||||
|
|
||||||
|
// Check if boxes are initialized
|
||||||
|
if (appSettingsBox == null || cacheBox == null || userPreferencesBox == null) {
|
||||||
|
debugPrint('⚠️ Cannot compact storage: boxes not initialized yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
appSettingsBox.compact(),
|
appSettingsBox.compact(),
|
||||||
cacheBox.compact(),
|
cacheBox.compact(),
|
||||||
@@ -184,6 +221,13 @@ class HiveStorageNotifier extends _$HiveStorageNotifier {
|
|||||||
Future<void> clearCache() async {
|
Future<void> clearCache() async {
|
||||||
try {
|
try {
|
||||||
final cacheBox = ref.read(cacheBoxProvider);
|
final cacheBox = ref.read(cacheBoxProvider);
|
||||||
|
|
||||||
|
// Check if cache box is initialized
|
||||||
|
if (cacheBox == null) {
|
||||||
|
debugPrint('⚠️ Cannot clear cache: box not initialized yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await cacheBox.clear();
|
await cacheBox.clear();
|
||||||
|
|
||||||
_updateStats();
|
_updateStats();
|
||||||
@@ -200,6 +244,17 @@ class HiveStorageNotifier extends _$HiveStorageNotifier {
|
|||||||
final cacheBox = ref.read(cacheBoxProvider);
|
final cacheBox = ref.read(cacheBoxProvider);
|
||||||
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
|
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
|
||||||
|
|
||||||
|
// Check if boxes are initialized
|
||||||
|
if (appSettingsBox == null || cacheBox == null || userPreferencesBox == null) {
|
||||||
|
return {
|
||||||
|
'appSettings': {'count': 0, 'keys': <dynamic>[], 'isEmpty': true},
|
||||||
|
'cache': {'count': 0, 'keys': <dynamic>[], 'isEmpty': true},
|
||||||
|
'userPreferences': {'count': 0, 'keys': <dynamic>[], 'isEmpty': true},
|
||||||
|
'total': {'items': 0, 'estimatedSize': 0},
|
||||||
|
'isInitialized': false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'appSettings': {
|
'appSettings': {
|
||||||
'count': appSettingsBox.length,
|
'count': appSettingsBox.length,
|
||||||
@@ -220,6 +275,7 @@ class HiveStorageNotifier extends _$HiveStorageNotifier {
|
|||||||
'items': appSettingsBox.length + cacheBox.length + userPreferencesBox.length,
|
'items': appSettingsBox.length + cacheBox.length + userPreferencesBox.length,
|
||||||
'estimatedSize': _calculateTotalSize(),
|
'estimatedSize': _calculateTotalSize(),
|
||||||
},
|
},
|
||||||
|
'isInitialized': true,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ Error getting storage stats: $e');
|
debugPrint('❌ Error getting storage stats: $e');
|
||||||
@@ -228,14 +284,33 @@ class HiveStorageNotifier extends _$HiveStorageNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateStats() {
|
void _updateStats() {
|
||||||
state = {
|
final appSettingsBox = ref.read(appSettingsBoxProvider);
|
||||||
...state,
|
final cacheBox = ref.read(cacheBoxProvider);
|
||||||
'appSettingsCount': ref.read(appSettingsBoxProvider).length,
|
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
|
||||||
'cacheItemsCount': ref.read(cacheBoxProvider).length,
|
|
||||||
'userPreferencesCount': ref.read(userPreferencesBoxProvider).length,
|
// Only update stats if boxes are initialized
|
||||||
'totalSize': _calculateTotalSize(),
|
// ignore: unnecessary_null_comparison
|
||||||
'lastUpdated': DateTime.now().toIso8601String(),
|
if (appSettingsBox != null && cacheBox != null && userPreferencesBox != null) {
|
||||||
};
|
state = {
|
||||||
|
...state,
|
||||||
|
'appSettingsCount': appSettingsBox.length,
|
||||||
|
'cacheItemsCount': cacheBox.length,
|
||||||
|
'userPreferencesCount': userPreferencesBox.length,
|
||||||
|
'totalSize': _calculateTotalSize(),
|
||||||
|
'lastUpdated': DateTime.now().toIso8601String(),
|
||||||
|
'isInitialized': true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
'appSettingsCount': 0,
|
||||||
|
'cacheItemsCount': 0,
|
||||||
|
'userPreferencesCount': 0,
|
||||||
|
'totalSize': 0,
|
||||||
|
'lastUpdated': DateTime.now().toIso8601String(),
|
||||||
|
'isInitialized': false,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,13 +347,19 @@ class StorageHealthMonitor extends _$StorageHealthMonitor {
|
|||||||
final cacheBox = ref.read(cacheBoxProvider);
|
final cacheBox = ref.read(cacheBoxProvider);
|
||||||
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
|
final userPreferencesBox = ref.read(userPreferencesBoxProvider);
|
||||||
|
|
||||||
if (!appSettingsBox.isOpen) errors.add('App settings box is not open');
|
// Check if boxes are initialized
|
||||||
if (!cacheBox.isOpen) errors.add('Cache box is not open');
|
// ignore: unnecessary_null_comparison
|
||||||
if (!userPreferencesBox.isOpen) errors.add('User preferences box is not open');
|
if (appSettingsBox == null || cacheBox == null || userPreferencesBox == null) {
|
||||||
|
warnings.add('Hive boxes not initialized yet');
|
||||||
|
} else {
|
||||||
|
if (!appSettingsBox.isOpen) errors.add('App settings box is not open');
|
||||||
|
if (!cacheBox.isOpen) errors.add('Cache box is not open');
|
||||||
|
if (!userPreferencesBox.isOpen) errors.add('User preferences box is not open');
|
||||||
|
|
||||||
// Check for large cache
|
// Check for large cache
|
||||||
if (cacheBox.length > 1000) {
|
if (cacheBox.length > 1000) {
|
||||||
warnings.add('Cache has more than 1000 items, consider cleanup');
|
warnings.add('Cache has more than 1000 items, consider cleanup');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.add('Hive storage error: $e');
|
errors.add('Hive storage error: $e');
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ final secureStorageProvider =
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef SecureStorageRef = AutoDisposeProviderRef<FlutterSecureStorage>;
|
typedef SecureStorageRef = AutoDisposeProviderRef<FlutterSecureStorage>;
|
||||||
String _$appSettingsBoxHash() => r'9e348c0084f7f23850f09adb2e6496fdbf8f2bdf';
|
String _$appSettingsBoxHash() => r'34dbc09afd824b056d366fec7d367c5021735bac';
|
||||||
|
|
||||||
/// Hive storage providers
|
/// Hive storage providers
|
||||||
///
|
///
|
||||||
/// Copied from [appSettingsBox].
|
/// Copied from [appSettingsBox].
|
||||||
@ProviderFor(appSettingsBox)
|
@ProviderFor(appSettingsBox)
|
||||||
final appSettingsBoxProvider = AutoDisposeProvider<Box<AppSettings>>.internal(
|
final appSettingsBoxProvider = AutoDisposeProvider<Box<AppSettings>?>.internal(
|
||||||
appSettingsBox,
|
appSettingsBox,
|
||||||
name: r'appSettingsBoxProvider',
|
name: r'appSettingsBoxProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
@@ -40,12 +40,12 @@ final appSettingsBoxProvider = AutoDisposeProvider<Box<AppSettings>>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef AppSettingsBoxRef = AutoDisposeProviderRef<Box<AppSettings>>;
|
typedef AppSettingsBoxRef = AutoDisposeProviderRef<Box<AppSettings>?>;
|
||||||
String _$cacheBoxHash() => r'949b55a2b7423b7fa7182b8e45adf02367ab8c7c';
|
String _$cacheBoxHash() => r'09bd635816f1934066a219a915b7b653d4ccbb22';
|
||||||
|
|
||||||
/// See also [cacheBox].
|
/// See also [cacheBox].
|
||||||
@ProviderFor(cacheBox)
|
@ProviderFor(cacheBox)
|
||||||
final cacheBoxProvider = AutoDisposeProvider<Box<CacheItem>>.internal(
|
final cacheBoxProvider = AutoDisposeProvider<Box<CacheItem>?>.internal(
|
||||||
cacheBox,
|
cacheBox,
|
||||||
name: r'cacheBoxProvider',
|
name: r'cacheBoxProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
@@ -54,14 +54,14 @@ final cacheBoxProvider = AutoDisposeProvider<Box<CacheItem>>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef CacheBoxRef = AutoDisposeProviderRef<Box<CacheItem>>;
|
typedef CacheBoxRef = AutoDisposeProviderRef<Box<CacheItem>?>;
|
||||||
String _$userPreferencesBoxHash() =>
|
String _$userPreferencesBoxHash() =>
|
||||||
r'38e2eab12afb00cca5ad2f48bf1f9ec76cc962c8';
|
r'f2aee9cdfcef7da5c9bb04ddd5044ae80ff8674e';
|
||||||
|
|
||||||
/// See also [userPreferencesBox].
|
/// See also [userPreferencesBox].
|
||||||
@ProviderFor(userPreferencesBox)
|
@ProviderFor(userPreferencesBox)
|
||||||
final userPreferencesBoxProvider =
|
final userPreferencesBoxProvider =
|
||||||
AutoDisposeProvider<Box<UserPreferences>>.internal(
|
AutoDisposeProvider<Box<UserPreferences>?>.internal(
|
||||||
userPreferencesBox,
|
userPreferencesBox,
|
||||||
name: r'userPreferencesBoxProvider',
|
name: r'userPreferencesBoxProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
@@ -71,7 +71,7 @@ final userPreferencesBoxProvider =
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef UserPreferencesBoxRef = AutoDisposeProviderRef<Box<UserPreferences>>;
|
typedef UserPreferencesBoxRef = AutoDisposeProviderRef<Box<UserPreferences>?>;
|
||||||
String _$secureStorageNotifierHash() =>
|
String _$secureStorageNotifierHash() =>
|
||||||
r'08d6cb392865d7483027fde37192c07cb944c45f';
|
r'08d6cb392865d7483027fde37192c07cb944c45f';
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ final secureStorageNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
|||||||
|
|
||||||
typedef _$SecureStorageNotifier = AutoDisposeAsyncNotifier<Map<String, String>>;
|
typedef _$SecureStorageNotifier = AutoDisposeAsyncNotifier<Map<String, String>>;
|
||||||
String _$hiveStorageNotifierHash() =>
|
String _$hiveStorageNotifierHash() =>
|
||||||
r'5d91bf162282fcfbef13aa7296255bb87640af51';
|
r'9f066e5f7959b87cb9955676c2bd1c38c4e04aca';
|
||||||
|
|
||||||
/// Hive storage notifier for managing Hive data
|
/// Hive storage notifier for managing Hive data
|
||||||
///
|
///
|
||||||
@@ -111,7 +111,7 @@ final hiveStorageNotifierProvider = AutoDisposeNotifierProvider<
|
|||||||
|
|
||||||
typedef _$HiveStorageNotifier = AutoDisposeNotifier<Map<String, dynamic>>;
|
typedef _$HiveStorageNotifier = AutoDisposeNotifier<Map<String, dynamic>>;
|
||||||
String _$storageHealthMonitorHash() =>
|
String _$storageHealthMonitorHash() =>
|
||||||
r'1d52e331a84bd59a36055f5e8963eaa996f9c235';
|
r'bea5ed421fcc5775c20692fddbc82fb9183d2e00';
|
||||||
|
|
||||||
/// Storage health monitor
|
/// Storage health monitor
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'route_names.dart';
|
|||||||
import 'route_paths.dart';
|
import 'route_paths.dart';
|
||||||
import 'route_guards.dart';
|
import 'route_guards.dart';
|
||||||
import 'error_page.dart';
|
import 'error_page.dart';
|
||||||
|
import '../../features/auth/presentation/pages/pages.dart';
|
||||||
import '../../features/home/presentation/pages/home_page.dart';
|
import '../../features/home/presentation/pages/home_page.dart';
|
||||||
import '../../features/settings/presentation/pages/settings_page.dart';
|
import '../../features/settings/presentation/pages/settings_page.dart';
|
||||||
import '../../features/todos/presentation/screens/home_screen.dart';
|
import '../../features/todos/presentation/screens/home_screen.dart';
|
||||||
@@ -101,7 +102,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
path: RoutePaths.login,
|
path: RoutePaths.login,
|
||||||
name: RouteNames.login,
|
name: RouteNames.login,
|
||||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||||
child: const _PlaceholderPage(title: 'Login'),
|
child: const LoginPage(),
|
||||||
state: state,
|
state: state,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -109,7 +110,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
path: RoutePaths.register,
|
path: RoutePaths.register,
|
||||||
name: RouteNames.register,
|
name: RouteNames.register,
|
||||||
pageBuilder: (context, state) => _buildPageWithTransition(
|
pageBuilder: (context, state) => _buildPageWithTransition(
|
||||||
child: const _PlaceholderPage(title: 'Register'),
|
child: const RegisterPage(),
|
||||||
state: state,
|
state: state,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,53 +1,26 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'route_paths.dart';
|
import 'route_paths.dart';
|
||||||
|
import '../../features/auth/presentation/providers/auth_providers.dart';
|
||||||
|
|
||||||
/// Authentication state provider
|
/// Legacy auth state provider - redirecting to proper auth provider
|
||||||
final authStateProvider = StateNotifierProvider<AuthStateNotifier, AuthState>(
|
final authStateProvider = Provider<AuthState>((ref) {
|
||||||
(ref) => AuthStateNotifier(),
|
final authState = ref.watch(authNotifierProvider);
|
||||||
);
|
return authState.when(
|
||||||
|
initial: () => AuthState.unknown,
|
||||||
|
loading: () => AuthState.unknown,
|
||||||
|
authenticated: (_) => AuthState.authenticated,
|
||||||
|
unauthenticated: (_) => AuthState.unauthenticated,
|
||||||
|
error: (_) => AuthState.unauthenticated,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/// Authentication state
|
/// Authentication state enum for routing
|
||||||
enum AuthState {
|
enum AuthState {
|
||||||
unknown,
|
unknown,
|
||||||
authenticated,
|
authenticated,
|
||||||
unauthenticated,
|
unauthenticated,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authentication state notifier
|
|
||||||
class AuthStateNotifier extends StateNotifier<AuthState> {
|
|
||||||
AuthStateNotifier() : super(AuthState.unknown) {
|
|
||||||
_checkInitialAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _checkInitialAuth() async {
|
|
||||||
// TODO: Implement actual auth check logic
|
|
||||||
// For now, simulate checking stored auth token
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
|
|
||||||
// Mock authentication check
|
|
||||||
// In a real app, you would check secure storage for auth token
|
|
||||||
state = AuthState.unauthenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> login(String email, String password) async {
|
|
||||||
// TODO: Implement actual login logic
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
state = AuthState.authenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> logout() async {
|
|
||||||
// TODO: Implement actual logout logic
|
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
|
||||||
state = AuthState.unauthenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> register(String email, String password) async {
|
|
||||||
// TODO: Implement actual registration logic
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
state = AuthState.authenticated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Route guard utility class
|
/// Route guard utility class
|
||||||
class RouteGuard {
|
class RouteGuard {
|
||||||
/// Check if user can access the given route
|
/// Check if user can access the given route
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import '../../../../core/errors/exceptions.dart';
|
||||||
|
import '../models/user_model.dart';
|
||||||
|
|
||||||
|
abstract class AuthLocalDataSource {
|
||||||
|
Future<void> cacheUser(UserModel user);
|
||||||
|
Future<UserModel?> getCachedUser();
|
||||||
|
Future<void> clearCache();
|
||||||
|
Future<void> cacheToken(String token);
|
||||||
|
Future<String?> getCachedToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthLocalDataSourceImpl implements AuthLocalDataSource {
|
||||||
|
final FlutterSecureStorage secureStorage;
|
||||||
|
|
||||||
|
static const String userKey = 'CACHED_USER';
|
||||||
|
static const String tokenKey = 'AUTH_TOKEN';
|
||||||
|
|
||||||
|
AuthLocalDataSourceImpl({required this.secureStorage});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> cacheUser(UserModel user) async {
|
||||||
|
try {
|
||||||
|
final userJson = json.encode(user.toJson());
|
||||||
|
await secureStorage.write(key: userKey, value: userJson);
|
||||||
|
} catch (e) {
|
||||||
|
throw CacheException('Failed to cache user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserModel?> getCachedUser() async {
|
||||||
|
try {
|
||||||
|
final userJson = await secureStorage.read(key: userKey);
|
||||||
|
if (userJson != null) {
|
||||||
|
final userMap = json.decode(userJson) as Map<String, dynamic>;
|
||||||
|
return UserModel.fromJson(userMap);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
throw CacheException('Failed to get cached user');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearCache() async {
|
||||||
|
try {
|
||||||
|
await secureStorage.delete(key: userKey);
|
||||||
|
await secureStorage.delete(key: tokenKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw CacheException('Failed to clear cache');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> cacheToken(String token) async {
|
||||||
|
try {
|
||||||
|
await secureStorage.write(key: tokenKey, value: token);
|
||||||
|
} catch (e) {
|
||||||
|
throw CacheException('Failed to cache token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getCachedToken() async {
|
||||||
|
try {
|
||||||
|
return await secureStorage.read(key: tokenKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw CacheException('Failed to get cached token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
232
lib/features/auth/data/datasources/auth_remote_datasource.dart
Normal file
232
lib/features/auth/data/datasources/auth_remote_datasource.dart
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../../../../core/errors/exceptions.dart';
|
||||||
|
import '../../../../core/network/dio_client.dart';
|
||||||
|
import '../models/user_model.dart';
|
||||||
|
|
||||||
|
abstract class AuthRemoteDataSource {
|
||||||
|
Future<UserModel> login({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<UserModel> register({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String name,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> logout();
|
||||||
|
|
||||||
|
Future<UserModel> refreshToken(String token);
|
||||||
|
|
||||||
|
Future<UserModel> updateProfile({
|
||||||
|
required String name,
|
||||||
|
String? avatarUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> changePassword({
|
||||||
|
required String oldPassword,
|
||||||
|
required String newPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> resetPassword({
|
||||||
|
required String email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||||
|
final DioClient dioClient;
|
||||||
|
|
||||||
|
AuthRemoteDataSourceImpl({required this.dioClient});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserModel> login({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Using JSONPlaceholder as a mock API
|
||||||
|
// In real app, this would be your actual auth endpoint
|
||||||
|
final response = await dioClient.dio.post(
|
||||||
|
'https://jsonplaceholder.typicode.com/posts',
|
||||||
|
data: {
|
||||||
|
'email': email,
|
||||||
|
'password': password,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock validation - accept any email/password for demo
|
||||||
|
// In real app, the server would validate credentials
|
||||||
|
if (email.isEmpty || password.isEmpty) {
|
||||||
|
throw const ServerException('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock response for demonstration
|
||||||
|
// In real app, parse actual API response
|
||||||
|
final mockUser = {
|
||||||
|
'id': '1',
|
||||||
|
'email': email,
|
||||||
|
'name': email.split('@').first,
|
||||||
|
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return UserModel.fromJson(mockUser);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 401) {
|
||||||
|
throw const ServerException('Invalid credentials');
|
||||||
|
} else if (e.response?.statusCode == 404) {
|
||||||
|
throw const ServerException('User not found');
|
||||||
|
} else {
|
||||||
|
throw ServerException(e.message ?? 'Login failed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.toString().contains('Invalid credentials')) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserModel> register({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String name,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Mock API call
|
||||||
|
final response = await dioClient.dio.post(
|
||||||
|
'https://jsonplaceholder.typicode.com/users',
|
||||||
|
data: {
|
||||||
|
'email': email,
|
||||||
|
'password': password,
|
||||||
|
'name': name,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock response
|
||||||
|
final mockUser = {
|
||||||
|
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
'email': email,
|
||||||
|
'name': name,
|
||||||
|
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return UserModel.fromJson(mockUser);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 409) {
|
||||||
|
throw const ServerException('Email already exists');
|
||||||
|
} else {
|
||||||
|
throw ServerException(e.message ?? 'Registration failed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> logout() async {
|
||||||
|
try {
|
||||||
|
// Mock API call
|
||||||
|
await dioClient.dio.post('https://jsonplaceholder.typicode.com/posts');
|
||||||
|
// In real app, you might call a logout endpoint to invalidate token
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserModel> refreshToken(String token) async {
|
||||||
|
try {
|
||||||
|
// Mock API call
|
||||||
|
final response = await dioClient.dio.post(
|
||||||
|
'https://jsonplaceholder.typicode.com/users',
|
||||||
|
options: Options(
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock response
|
||||||
|
final mockUser = {
|
||||||
|
'id': '1',
|
||||||
|
'email': 'user@example.com',
|
||||||
|
'name': 'User',
|
||||||
|
'token': 'refreshed_token_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return UserModel.fromJson(mockUser);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserModel> updateProfile({
|
||||||
|
required String name,
|
||||||
|
String? avatarUrl,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Mock API call
|
||||||
|
final response = await dioClient.dio.put(
|
||||||
|
'https://jsonplaceholder.typicode.com/users/1',
|
||||||
|
data: {
|
||||||
|
'name': name,
|
||||||
|
'avatarUrl': avatarUrl,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock response
|
||||||
|
final mockUser = {
|
||||||
|
'id': '1',
|
||||||
|
'email': 'user@example.com',
|
||||||
|
'name': name,
|
||||||
|
'avatarUrl': avatarUrl,
|
||||||
|
'token': 'mock_jwt_token_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
'tokenExpiry': DateTime.now().add(const Duration(days: 7)).toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return UserModel.fromJson(mockUser);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> changePassword({
|
||||||
|
required String oldPassword,
|
||||||
|
required String newPassword,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Mock API call
|
||||||
|
await dioClient.dio.post(
|
||||||
|
'https://jsonplaceholder.typicode.com/posts',
|
||||||
|
data: {
|
||||||
|
'oldPassword': oldPassword,
|
||||||
|
'newPassword': newPassword,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> resetPassword({
|
||||||
|
required String email,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Mock API call
|
||||||
|
await dioClient.dio.post(
|
||||||
|
'https://jsonplaceholder.typicode.com/posts',
|
||||||
|
data: {
|
||||||
|
'email': email,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
lib/features/auth/data/models/user_model.dart
Normal file
42
lib/features/auth/data/models/user_model.dart
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import '../../domain/entities/user.dart';
|
||||||
|
|
||||||
|
part 'user_model.freezed.dart';
|
||||||
|
part 'user_model.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class UserModel with _$UserModel {
|
||||||
|
const factory UserModel({
|
||||||
|
required String id,
|
||||||
|
required String email,
|
||||||
|
required String name,
|
||||||
|
String? avatarUrl,
|
||||||
|
required String token,
|
||||||
|
DateTime? tokenExpiry,
|
||||||
|
}) = _UserModel;
|
||||||
|
|
||||||
|
const UserModel._();
|
||||||
|
|
||||||
|
factory UserModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UserModelFromJson(json);
|
||||||
|
|
||||||
|
/// Convert to domain entity
|
||||||
|
User toEntity() => User(
|
||||||
|
id: id,
|
||||||
|
email: email,
|
||||||
|
name: name,
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
token: token,
|
||||||
|
tokenExpiry: tokenExpiry,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Create from domain entity
|
||||||
|
factory UserModel.fromEntity(User user) => UserModel(
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
avatarUrl: user.avatarUrl,
|
||||||
|
token: user.token,
|
||||||
|
tokenExpiry: user.tokenExpiry,
|
||||||
|
);
|
||||||
|
}
|
||||||
259
lib/features/auth/data/models/user_model.freezed.dart
Normal file
259
lib/features/auth/data/models/user_model.freezed.dart
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'user_model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
UserModel _$UserModelFromJson(Map<String, dynamic> json) {
|
||||||
|
return _UserModel.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$UserModel {
|
||||||
|
String get id => throw _privateConstructorUsedError;
|
||||||
|
String get email => throw _privateConstructorUsedError;
|
||||||
|
String get name => throw _privateConstructorUsedError;
|
||||||
|
String? get avatarUrl => throw _privateConstructorUsedError;
|
||||||
|
String get token => throw _privateConstructorUsedError;
|
||||||
|
DateTime? get tokenExpiry => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$UserModelCopyWith<UserModel> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $UserModelCopyWith<$Res> {
|
||||||
|
factory $UserModelCopyWith(UserModel value, $Res Function(UserModel) then) =
|
||||||
|
_$UserModelCopyWithImpl<$Res, UserModel>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String id,
|
||||||
|
String email,
|
||||||
|
String name,
|
||||||
|
String? avatarUrl,
|
||||||
|
String token,
|
||||||
|
DateTime? tokenExpiry});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$UserModelCopyWithImpl<$Res, $Val extends UserModel>
|
||||||
|
implements $UserModelCopyWith<$Res> {
|
||||||
|
_$UserModelCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? email = null,
|
||||||
|
Object? name = null,
|
||||||
|
Object? avatarUrl = freezed,
|
||||||
|
Object? token = null,
|
||||||
|
Object? tokenExpiry = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
email: null == email
|
||||||
|
? _value.email
|
||||||
|
: email // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
name: null == name
|
||||||
|
? _value.name
|
||||||
|
: name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
avatarUrl: freezed == avatarUrl
|
||||||
|
? _value.avatarUrl
|
||||||
|
: avatarUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
token: null == token
|
||||||
|
? _value.token
|
||||||
|
: token // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
tokenExpiry: freezed == tokenExpiry
|
||||||
|
? _value.tokenExpiry
|
||||||
|
: tokenExpiry // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$UserModelImplCopyWith<$Res>
|
||||||
|
implements $UserModelCopyWith<$Res> {
|
||||||
|
factory _$$UserModelImplCopyWith(
|
||||||
|
_$UserModelImpl value, $Res Function(_$UserModelImpl) then) =
|
||||||
|
__$$UserModelImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String id,
|
||||||
|
String email,
|
||||||
|
String name,
|
||||||
|
String? avatarUrl,
|
||||||
|
String token,
|
||||||
|
DateTime? tokenExpiry});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$UserModelImplCopyWithImpl<$Res>
|
||||||
|
extends _$UserModelCopyWithImpl<$Res, _$UserModelImpl>
|
||||||
|
implements _$$UserModelImplCopyWith<$Res> {
|
||||||
|
__$$UserModelImplCopyWithImpl(
|
||||||
|
_$UserModelImpl _value, $Res Function(_$UserModelImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? email = null,
|
||||||
|
Object? name = null,
|
||||||
|
Object? avatarUrl = freezed,
|
||||||
|
Object? token = null,
|
||||||
|
Object? tokenExpiry = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$UserModelImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
email: null == email
|
||||||
|
? _value.email
|
||||||
|
: email // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
name: null == name
|
||||||
|
? _value.name
|
||||||
|
: name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
avatarUrl: freezed == avatarUrl
|
||||||
|
? _value.avatarUrl
|
||||||
|
: avatarUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
token: null == token
|
||||||
|
? _value.token
|
||||||
|
: token // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
tokenExpiry: freezed == tokenExpiry
|
||||||
|
? _value.tokenExpiry
|
||||||
|
: tokenExpiry // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$UserModelImpl extends _UserModel {
|
||||||
|
const _$UserModelImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.email,
|
||||||
|
required this.name,
|
||||||
|
this.avatarUrl,
|
||||||
|
required this.token,
|
||||||
|
this.tokenExpiry})
|
||||||
|
: super._();
|
||||||
|
|
||||||
|
factory _$UserModelImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$UserModelImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String id;
|
||||||
|
@override
|
||||||
|
final String email;
|
||||||
|
@override
|
||||||
|
final String name;
|
||||||
|
@override
|
||||||
|
final String? avatarUrl;
|
||||||
|
@override
|
||||||
|
final String token;
|
||||||
|
@override
|
||||||
|
final DateTime? tokenExpiry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'UserModel(id: $id, email: $email, name: $name, avatarUrl: $avatarUrl, token: $token, tokenExpiry: $tokenExpiry)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$UserModelImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.email, email) || other.email == email) &&
|
||||||
|
(identical(other.name, name) || other.name == name) &&
|
||||||
|
(identical(other.avatarUrl, avatarUrl) ||
|
||||||
|
other.avatarUrl == avatarUrl) &&
|
||||||
|
(identical(other.token, token) || other.token == token) &&
|
||||||
|
(identical(other.tokenExpiry, tokenExpiry) ||
|
||||||
|
other.tokenExpiry == tokenExpiry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(runtimeType, id, email, name, avatarUrl, token, tokenExpiry);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$UserModelImplCopyWith<_$UserModelImpl> get copyWith =>
|
||||||
|
__$$UserModelImplCopyWithImpl<_$UserModelImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$UserModelImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _UserModel extends UserModel {
|
||||||
|
const factory _UserModel(
|
||||||
|
{required final String id,
|
||||||
|
required final String email,
|
||||||
|
required final String name,
|
||||||
|
final String? avatarUrl,
|
||||||
|
required final String token,
|
||||||
|
final DateTime? tokenExpiry}) = _$UserModelImpl;
|
||||||
|
const _UserModel._() : super._();
|
||||||
|
|
||||||
|
factory _UserModel.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$UserModelImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id;
|
||||||
|
@override
|
||||||
|
String get email;
|
||||||
|
@override
|
||||||
|
String get name;
|
||||||
|
@override
|
||||||
|
String? get avatarUrl;
|
||||||
|
@override
|
||||||
|
String get token;
|
||||||
|
@override
|
||||||
|
DateTime? get tokenExpiry;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$UserModelImplCopyWith<_$UserModelImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
29
lib/features/auth/data/models/user_model.g.dart
Normal file
29
lib/features/auth/data/models/user_model.g.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'user_model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$UserModelImpl _$$UserModelImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UserModelImpl(
|
||||||
|
id: json['id'] as String,
|
||||||
|
email: json['email'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
avatarUrl: json['avatarUrl'] as String?,
|
||||||
|
token: json['token'] as String,
|
||||||
|
tokenExpiry: json['tokenExpiry'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['tokenExpiry'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$UserModelImplToJson(_$UserModelImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'email': instance.email,
|
||||||
|
'name': instance.name,
|
||||||
|
'avatarUrl': instance.avatarUrl,
|
||||||
|
'token': instance.token,
|
||||||
|
'tokenExpiry': instance.tokenExpiry?.toIso8601String(),
|
||||||
|
};
|
||||||
232
lib/features/auth/data/repositories/auth_repository_impl.dart
Normal file
232
lib/features/auth/data/repositories/auth_repository_impl.dart
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import 'package:fpdart/fpdart.dart';
|
||||||
|
import '../../../../core/errors/exceptions.dart';
|
||||||
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../../../../core/network/network_info.dart';
|
||||||
|
import '../../domain/entities/user.dart';
|
||||||
|
import '../../domain/repositories/auth_repository.dart';
|
||||||
|
import '../datasources/auth_local_datasource.dart';
|
||||||
|
import '../datasources/auth_remote_datasource.dart';
|
||||||
|
import '../models/user_model.dart';
|
||||||
|
|
||||||
|
class AuthRepositoryImpl implements AuthRepository {
|
||||||
|
final AuthRemoteDataSource remoteDataSource;
|
||||||
|
final AuthLocalDataSource localDataSource;
|
||||||
|
final NetworkInfo networkInfo;
|
||||||
|
|
||||||
|
AuthRepositoryImpl({
|
||||||
|
required this.remoteDataSource,
|
||||||
|
required this.localDataSource,
|
||||||
|
required this.networkInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, User>> login({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
}) async {
|
||||||
|
if (!await networkInfo.isConnected) {
|
||||||
|
return const Left(NetworkFailure('No internet connection'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final userModel = await remoteDataSource.login(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache user data and token
|
||||||
|
await localDataSource.cacheUser(userModel);
|
||||||
|
await localDataSource.cacheToken(userModel.token);
|
||||||
|
|
||||||
|
return Right(userModel.toEntity());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, User>> register({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String name,
|
||||||
|
}) async {
|
||||||
|
if (!await networkInfo.isConnected) {
|
||||||
|
return const Left(NetworkFailure('No internet connection'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final userModel = await remoteDataSource.register(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
name: name,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache user data and token
|
||||||
|
await localDataSource.cacheUser(userModel);
|
||||||
|
await localDataSource.cacheToken(userModel.token);
|
||||||
|
|
||||||
|
return Right(userModel.toEntity());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> logout() async {
|
||||||
|
try {
|
||||||
|
// Clear local cache first
|
||||||
|
await localDataSource.clearCache();
|
||||||
|
|
||||||
|
// If online, notify server
|
||||||
|
if (await networkInfo.isConnected) {
|
||||||
|
await remoteDataSource.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Right(null);
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} on CacheException catch (e) {
|
||||||
|
return Left(CacheFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, User?>> getCurrentUser() async {
|
||||||
|
try {
|
||||||
|
final cachedUser = await localDataSource.getCachedUser();
|
||||||
|
if (cachedUser != null) {
|
||||||
|
// Check if token is still valid
|
||||||
|
final user = cachedUser.toEntity();
|
||||||
|
if (user.isTokenValid) {
|
||||||
|
return Right(user);
|
||||||
|
} else {
|
||||||
|
// Token expired, try to refresh
|
||||||
|
if (await networkInfo.isConnected) {
|
||||||
|
return await refreshToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const Right(null);
|
||||||
|
} on CacheException catch (e) {
|
||||||
|
return Left(CacheFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(CacheFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isAuthenticated() async {
|
||||||
|
try {
|
||||||
|
final cachedUser = await localDataSource.getCachedUser();
|
||||||
|
if (cachedUser != null) {
|
||||||
|
final user = cachedUser.toEntity();
|
||||||
|
return user.isTokenValid;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, User>> refreshToken() async {
|
||||||
|
if (!await networkInfo.isConnected) {
|
||||||
|
return const Left(NetworkFailure('No internet connection'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final token = await localDataSource.getCachedToken();
|
||||||
|
if (token == null) {
|
||||||
|
return const Left(AuthFailure('No token available'));
|
||||||
|
}
|
||||||
|
|
||||||
|
final userModel = await remoteDataSource.refreshToken(token);
|
||||||
|
|
||||||
|
// Update cached user and token
|
||||||
|
await localDataSource.cacheUser(userModel);
|
||||||
|
await localDataSource.cacheToken(userModel.token);
|
||||||
|
|
||||||
|
return Right(userModel.toEntity());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
// If refresh fails, clear cache
|
||||||
|
await localDataSource.clearCache();
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, User>> updateProfile({
|
||||||
|
required String name,
|
||||||
|
String? avatarUrl,
|
||||||
|
}) async {
|
||||||
|
if (!await networkInfo.isConnected) {
|
||||||
|
return const Left(NetworkFailure('No internet connection'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final userModel = await remoteDataSource.updateProfile(
|
||||||
|
name: name,
|
||||||
|
avatarUrl: avatarUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update cached user
|
||||||
|
await localDataSource.cacheUser(userModel);
|
||||||
|
|
||||||
|
return Right(userModel.toEntity());
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> changePassword({
|
||||||
|
required String oldPassword,
|
||||||
|
required String newPassword,
|
||||||
|
}) async {
|
||||||
|
if (!await networkInfo.isConnected) {
|
||||||
|
return const Left(NetworkFailure('No internet connection'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await remoteDataSource.changePassword(
|
||||||
|
oldPassword: oldPassword,
|
||||||
|
newPassword: newPassword,
|
||||||
|
);
|
||||||
|
|
||||||
|
return const Right(null);
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> resetPassword({
|
||||||
|
required String email,
|
||||||
|
}) async {
|
||||||
|
if (!await networkInfo.isConnected) {
|
||||||
|
return const Left(NetworkFailure('No internet connection'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await remoteDataSource.resetPassword(email: email);
|
||||||
|
|
||||||
|
return const Right(null);
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} catch (e) {
|
||||||
|
return Left(ServerFailure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/features/auth/domain/entities/user.dart
Normal file
28
lib/features/auth/domain/entities/user.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// User entity representing authenticated user
|
||||||
|
class User extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final String email;
|
||||||
|
final String name;
|
||||||
|
final String? avatarUrl;
|
||||||
|
final String token;
|
||||||
|
final DateTime? tokenExpiry;
|
||||||
|
|
||||||
|
const User({
|
||||||
|
required this.id,
|
||||||
|
required this.email,
|
||||||
|
required this.name,
|
||||||
|
this.avatarUrl,
|
||||||
|
required this.token,
|
||||||
|
this.tokenExpiry,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [id, email, name, avatarUrl, token, tokenExpiry];
|
||||||
|
|
||||||
|
bool get isTokenValid {
|
||||||
|
if (tokenExpiry == null) return true;
|
||||||
|
return tokenExpiry!.isAfter(DateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
48
lib/features/auth/domain/repositories/auth_repository.dart
Normal file
48
lib/features/auth/domain/repositories/auth_repository.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:fpdart/fpdart.dart';
|
||||||
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../entities/user.dart';
|
||||||
|
|
||||||
|
/// Auth repository interface
|
||||||
|
abstract class AuthRepository {
|
||||||
|
/// Login with email and password
|
||||||
|
Future<Either<Failure, User>> login({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Register new user
|
||||||
|
Future<Either<Failure, User>> register({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String name,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Logout current user
|
||||||
|
Future<Either<Failure, void>> logout();
|
||||||
|
|
||||||
|
/// Get current user
|
||||||
|
Future<Either<Failure, User?>> getCurrentUser();
|
||||||
|
|
||||||
|
/// Check if user is authenticated
|
||||||
|
Future<bool> isAuthenticated();
|
||||||
|
|
||||||
|
/// Refresh token
|
||||||
|
Future<Either<Failure, User>> refreshToken();
|
||||||
|
|
||||||
|
/// Update user profile
|
||||||
|
Future<Either<Failure, User>> updateProfile({
|
||||||
|
required String name,
|
||||||
|
String? avatarUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Change password
|
||||||
|
Future<Either<Failure, void>> changePassword({
|
||||||
|
required String oldPassword,
|
||||||
|
required String newPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Reset password
|
||||||
|
Future<Either<Failure, void>> resetPassword({
|
||||||
|
required String email,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:fpdart/fpdart.dart';
|
||||||
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../../../../shared/domain/usecases/usecase.dart';
|
||||||
|
import '../entities/user.dart';
|
||||||
|
import '../repositories/auth_repository.dart';
|
||||||
|
|
||||||
|
class GetCurrentUserUseCase implements UseCase<User?, NoParams> {
|
||||||
|
final AuthRepository repository;
|
||||||
|
|
||||||
|
GetCurrentUserUseCase(this.repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, User?>> call(NoParams params) async {
|
||||||
|
return repository.getCurrentUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
43
lib/features/auth/domain/usecases/login_usecase.dart
Normal file
43
lib/features/auth/domain/usecases/login_usecase.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:fpdart/fpdart.dart';
|
||||||
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../../../../shared/domain/usecases/usecase.dart';
|
||||||
|
import '../entities/user.dart';
|
||||||
|
import '../repositories/auth_repository.dart';
|
||||||
|
|
||||||
|
class LoginParams {
|
||||||
|
final String email;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
const LoginParams({
|
||||||
|
required this.email,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginUseCase implements UseCase<User, LoginParams> {
|
||||||
|
final AuthRepository repository;
|
||||||
|
|
||||||
|
LoginUseCase(this.repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, User>> call(LoginParams params) async {
|
||||||
|
// Validate email format
|
||||||
|
if (!_isValidEmail(params.email)) {
|
||||||
|
return Left(ValidationFailure('Invalid email format'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate password
|
||||||
|
if (params.password.length < 6) {
|
||||||
|
return Left(ValidationFailure('Password must be at least 6 characters'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return repository.login(
|
||||||
|
email: params.email,
|
||||||
|
password: params.password,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isValidEmail(String email) {
|
||||||
|
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
lib/features/auth/domain/usecases/logout_usecase.dart
Normal file
15
lib/features/auth/domain/usecases/logout_usecase.dart
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:fpdart/fpdart.dart';
|
||||||
|
import '../../../../core/errors/failures.dart';
|
||||||
|
import '../../../../shared/domain/usecases/usecase.dart';
|
||||||
|
import '../repositories/auth_repository.dart';
|
||||||
|
|
||||||
|
class LogoutUseCase implements UseCase<void, NoParams> {
|
||||||
|
final AuthRepository repository;
|
||||||
|
|
||||||
|
LogoutUseCase(this.repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, void>> call(NoParams params) async {
|
||||||
|
return repository.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
555
lib/features/auth/presentation/pages/login_page.dart
Normal file
555
lib/features/auth/presentation/pages/login_page.dart
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../../../core/theme/app_spacing.dart';
|
||||||
|
import '../../../../core/theme/app_typography.dart';
|
||||||
|
import '../providers/auth_providers.dart';
|
||||||
|
import '../providers/auth_state.dart';
|
||||||
|
import '../widgets/auth_button.dart';
|
||||||
|
import '../widgets/auth_text_field.dart';
|
||||||
|
|
||||||
|
/// Beautiful and functional login page with Material 3 design
|
||||||
|
/// Features responsive design, form validation, and proper state management
|
||||||
|
class LoginPage extends ConsumerStatefulWidget {
|
||||||
|
const LoginPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<LoginPage> createState() => _LoginPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPageState extends ConsumerState<LoginPage>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _emailFocusNode = FocusNode();
|
||||||
|
final _passwordFocusNode = FocusNode();
|
||||||
|
|
||||||
|
bool _isPasswordVisible = false;
|
||||||
|
bool _rememberMe = false;
|
||||||
|
|
||||||
|
late AnimationController _fadeAnimationController;
|
||||||
|
late AnimationController _slideAnimationController;
|
||||||
|
late Animation<double> _fadeAnimation;
|
||||||
|
late Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_setupAnimations();
|
||||||
|
_startAnimations();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupAnimations() {
|
||||||
|
_fadeAnimationController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimationController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_fadeAnimation = Tween<double>(
|
||||||
|
begin: 0.0,
|
||||||
|
end: 1.0,
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: _fadeAnimationController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
));
|
||||||
|
|
||||||
|
_slideAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.3),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: _slideAnimationController,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startAnimations() {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
_fadeAnimationController.forward();
|
||||||
|
_slideAnimationController.forward();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_emailFocusNode.dispose();
|
||||||
|
_passwordFocusNode.dispose();
|
||||||
|
_fadeAnimationController.dispose();
|
||||||
|
_slideAnimationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateEmail(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Email is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
final emailRegExp = RegExp(
|
||||||
|
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||||||
|
);
|
||||||
|
if (!emailRegExp.hasMatch(value)) {
|
||||||
|
return 'Please enter a valid email address';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validatePassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Password is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length < 6) {
|
||||||
|
return 'Password must be at least 6 characters long';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleLogin() async {
|
||||||
|
if (!_formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide haptic feedback
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
|
// Clear any existing errors
|
||||||
|
ref.read(authNotifierProvider.notifier).clearError();
|
||||||
|
|
||||||
|
// Attempt login
|
||||||
|
await ref.read(authNotifierProvider.notifier).login(
|
||||||
|
email: _emailController.text.trim(),
|
||||||
|
password: _passwordController.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleForgotPassword() {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
// TODO: Navigate to forgot password page
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: const Text('Forgot password functionality coming soon'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).size.height * 0.1,
|
||||||
|
left: AppSpacing.lg,
|
||||||
|
right: AppSpacing.lg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSignUp() {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
Navigator.of(context).pushNamed('/auth/register');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorSnackBar(String message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Theme.of(context).colorScheme.onError,
|
||||||
|
),
|
||||||
|
AppSpacing.horizontalSpaceSM,
|
||||||
|
Expanded(child: Text(message)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).size.height * 0.1,
|
||||||
|
left: AppSpacing.lg,
|
||||||
|
right: AppSpacing.lg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final isKeyboardVisible = mediaQuery.viewInsets.bottom > 0;
|
||||||
|
|
||||||
|
// Listen to auth state changes
|
||||||
|
ref.listen<AuthState>(authNotifierProvider, (previous, current) {
|
||||||
|
current.when(
|
||||||
|
initial: () {},
|
||||||
|
loading: () {},
|
||||||
|
authenticated: (user) {
|
||||||
|
// TODO: Navigate to home page
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.check_circle_outline,
|
||||||
|
color: theme.extension<AppColorsExtension>()?.onSuccess ??
|
||||||
|
colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
AppSpacing.horizontalSpaceSM,
|
||||||
|
Text('Welcome back, ${user.name}!'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: theme.extension<AppColorsExtension>()?.success ??
|
||||||
|
colorScheme.primary,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
bottom: mediaQuery.size.height * 0.1,
|
||||||
|
left: AppSpacing.lg,
|
||||||
|
right: AppSpacing.lg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
unauthenticated: (message) {
|
||||||
|
if (message != null) {
|
||||||
|
_showErrorSnackBar(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (message) {
|
||||||
|
_showErrorSnackBar(message);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final authState = ref.watch(authNotifierProvider);
|
||||||
|
final isLoading = authState is AuthStateLoading;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: colorScheme.surface,
|
||||||
|
body: SafeArea(
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _fadeAnimation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: AppSpacing.responsivePadding(context),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: AppSpacing.isMobile(context) ? double.infinity : 400,
|
||||||
|
),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// App Logo and Welcome Section
|
||||||
|
_buildHeaderSection(theme, colorScheme, isKeyboardVisible),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceXXL,
|
||||||
|
|
||||||
|
// Form Fields
|
||||||
|
_buildFormSection(theme, colorScheme, isLoading),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceXXL,
|
||||||
|
|
||||||
|
// Login Button
|
||||||
|
_buildLoginButton(isLoading),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceXL,
|
||||||
|
|
||||||
|
// Footer Links
|
||||||
|
_buildFooterSection(theme, colorScheme),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeaderSection(
|
||||||
|
ThemeData theme,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
bool isKeyboardVisible,
|
||||||
|
) {
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: AppSpacing.animationNormal,
|
||||||
|
height: isKeyboardVisible ? 120 : 180,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// App Icon/Logo
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.primaryContainer.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.primaryContainer.withValues(alpha: 0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.lock_person_outlined,
|
||||||
|
size: isKeyboardVisible ? 48 : 64,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceLG,
|
||||||
|
|
||||||
|
// Welcome Text
|
||||||
|
Text(
|
||||||
|
'Welcome Back',
|
||||||
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceSM,
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Sign in to continue to your account',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFormSection(
|
||||||
|
ThemeData theme,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
bool isLoading,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Email Field
|
||||||
|
AuthTextField(
|
||||||
|
controller: _emailController,
|
||||||
|
focusNode: _emailFocusNode,
|
||||||
|
labelText: 'Email Address',
|
||||||
|
hintText: 'Enter your email',
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
validator: _validateEmail,
|
||||||
|
enabled: !isLoading,
|
||||||
|
autofillHints: const [AutofillHints.email],
|
||||||
|
prefixIcon: const Icon(Icons.email_outlined),
|
||||||
|
onFieldSubmitted: (_) => _passwordFocusNode.requestFocus(),
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceLG,
|
||||||
|
|
||||||
|
// Password Field
|
||||||
|
AuthTextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
focusNode: _passwordFocusNode,
|
||||||
|
labelText: 'Password',
|
||||||
|
hintText: 'Enter your password',
|
||||||
|
obscureText: !_isPasswordVisible,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
validator: _validatePassword,
|
||||||
|
enabled: !isLoading,
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isPasswordVisible
|
||||||
|
? Icons.visibility_off_outlined
|
||||||
|
: Icons.visibility_outlined,
|
||||||
|
),
|
||||||
|
onPressed: isLoading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
_isPasswordVisible = !_isPasswordVisible;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isPasswordVisible ? 'Hide password' : 'Show password',
|
||||||
|
),
|
||||||
|
onFieldSubmitted: (_) => _handleLogin(),
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceMD,
|
||||||
|
|
||||||
|
// Remember Me and Forgot Password Row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: _rememberMe,
|
||||||
|
onChanged: isLoading
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
setState(() {
|
||||||
|
_rememberMe = value ?? false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
AppSpacing.horizontalSpaceSM,
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Remember me',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: isLoading ? null : _handleForgotPassword,
|
||||||
|
child: Text(
|
||||||
|
'Forgot Password?',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isLoading
|
||||||
|
? colorScheme.onSurface.withValues(alpha: 0.38)
|
||||||
|
: colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoginButton(bool isLoading) {
|
||||||
|
return AuthButton(
|
||||||
|
onPressed: isLoading ? null : _handleLogin,
|
||||||
|
text: 'Sign In',
|
||||||
|
isLoading: isLoading,
|
||||||
|
type: AuthButtonType.filled,
|
||||||
|
icon: isLoading ? null : const Icon(Icons.login),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFooterSection(ThemeData theme, ColorScheme colorScheme) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Divider with "or" text
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Divider(
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
thickness: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: AppSpacing.horizontalLG,
|
||||||
|
child: Text(
|
||||||
|
'or',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Divider(
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
thickness: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceXL,
|
||||||
|
|
||||||
|
// Sign Up Link
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Don't have an account? ",
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _handleSignUp,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.sm,
|
||||||
|
vertical: AppSpacing.xs,
|
||||||
|
),
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Sign Up',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceLG,
|
||||||
|
|
||||||
|
// Privacy and Terms
|
||||||
|
Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'By continuing, you agree to our ',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Navigate to terms of service
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Terms of Service',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
' and ',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Navigate to privacy policy
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Privacy Policy',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
7
lib/features/auth/presentation/pages/pages.dart
Normal file
7
lib/features/auth/presentation/pages/pages.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// Auth pages exports
|
||||||
|
//
|
||||||
|
// This file exports all auth-related pages for easy importing
|
||||||
|
// throughout the application.
|
||||||
|
|
||||||
|
export 'login_page.dart';
|
||||||
|
export 'register_page.dart';
|
||||||
685
lib/features/auth/presentation/pages/register_page.dart
Normal file
685
lib/features/auth/presentation/pages/register_page.dart
Normal file
@@ -0,0 +1,685 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../../../core/theme/app_spacing.dart';
|
||||||
|
import '../../../../core/theme/app_typography.dart';
|
||||||
|
import '../providers/auth_providers.dart';
|
||||||
|
import '../widgets/auth_button.dart';
|
||||||
|
import '../widgets/auth_text_field.dart';
|
||||||
|
|
||||||
|
/// Beautiful and functional registration page with Material 3 design
|
||||||
|
/// Features comprehensive form validation, password confirmation, and responsive design
|
||||||
|
class RegisterPage extends ConsumerStatefulWidget {
|
||||||
|
const RegisterPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<RegisterPage> createState() => _RegisterPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegisterPageState extends ConsumerState<RegisterPage>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final _emailController = TextEditingController();
|
||||||
|
final _passwordController = TextEditingController();
|
||||||
|
final _confirmPasswordController = TextEditingController();
|
||||||
|
|
||||||
|
final _nameFocusNode = FocusNode();
|
||||||
|
final _emailFocusNode = FocusNode();
|
||||||
|
final _passwordFocusNode = FocusNode();
|
||||||
|
final _confirmPasswordFocusNode = FocusNode();
|
||||||
|
|
||||||
|
bool _isPasswordVisible = false;
|
||||||
|
bool _isConfirmPasswordVisible = false;
|
||||||
|
bool _agreeToTerms = false;
|
||||||
|
|
||||||
|
late AnimationController _fadeAnimationController;
|
||||||
|
late AnimationController _slideAnimationController;
|
||||||
|
late Animation<double> _fadeAnimation;
|
||||||
|
late Animation<Offset> _slideAnimation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_setupAnimations();
|
||||||
|
_startAnimations();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupAnimations() {
|
||||||
|
_fadeAnimationController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 600),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_slideAnimationController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
_fadeAnimation = Tween<double>(
|
||||||
|
begin: 0.0,
|
||||||
|
end: 1.0,
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: _fadeAnimationController,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
));
|
||||||
|
|
||||||
|
_slideAnimation = Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.3),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: _slideAnimationController,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startAnimations() {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
_fadeAnimationController.forward();
|
||||||
|
_slideAnimationController.forward();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_confirmPasswordController.dispose();
|
||||||
|
_nameFocusNode.dispose();
|
||||||
|
_emailFocusNode.dispose();
|
||||||
|
_passwordFocusNode.dispose();
|
||||||
|
_confirmPasswordFocusNode.dispose();
|
||||||
|
_fadeAnimationController.dispose();
|
||||||
|
_slideAnimationController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateName(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Full name is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.trim().length < 2) {
|
||||||
|
return 'Name must be at least 2 characters long';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.trim().split(' ').length < 2) {
|
||||||
|
return 'Please enter your full name';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateEmail(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Email is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
final emailRegExp = RegExp(
|
||||||
|
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
||||||
|
);
|
||||||
|
if (!emailRegExp.hasMatch(value)) {
|
||||||
|
return 'Please enter a valid email address';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validatePassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Password is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length < 8) {
|
||||||
|
return 'Password must be at least 8 characters long';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) {
|
||||||
|
return 'Password must contain uppercase, lowercase, and number';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _validateConfirmPassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please confirm your password';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != _passwordController.text) {
|
||||||
|
return 'Passwords do not match';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleRegister() async {
|
||||||
|
if (!_formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_agreeToTerms) {
|
||||||
|
_showErrorSnackBar('Please agree to the Terms of Service and Privacy Policy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide haptic feedback
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
|
// Clear any existing errors
|
||||||
|
ref.read(authNotifierProvider.notifier).clearError();
|
||||||
|
|
||||||
|
// TODO: Implement registration logic
|
||||||
|
// For now, we'll simulate the registration process
|
||||||
|
await _simulateRegistration();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _simulateRegistration() async {
|
||||||
|
// Show loading state
|
||||||
|
setState(() {});
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Simulate successful registration
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.check_circle_outline,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
AppSpacing.horizontalSpaceSM,
|
||||||
|
const Expanded(
|
||||||
|
child: Text('Account created successfully! Please sign in.'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).extension<AppColorsExtension>()?.success ??
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).size.height * 0.1,
|
||||||
|
left: AppSpacing.lg,
|
||||||
|
right: AppSpacing.lg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Navigate back to login
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSignIn() {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showErrorSnackBar(String message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Theme.of(context).colorScheme.onError,
|
||||||
|
),
|
||||||
|
AppSpacing.horizontalSpaceSM,
|
||||||
|
Expanded(child: Text(message)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
margin: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).size.height * 0.1,
|
||||||
|
left: AppSpacing.lg,
|
||||||
|
right: AppSpacing.lg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final isKeyboardVisible = mediaQuery.viewInsets.bottom > 0;
|
||||||
|
|
||||||
|
// For simulation, we'll track loading state locally
|
||||||
|
// In real implementation, this would come from auth state
|
||||||
|
final isLoading = false; // Replace with actual auth state when implemented
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: colorScheme.surface,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _fadeAnimation,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: _slideAnimation,
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: AppSpacing.responsivePadding(context),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: AppSpacing.isMobile(context) ? double.infinity : 400,
|
||||||
|
),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Header Section
|
||||||
|
_buildHeaderSection(theme, colorScheme, isKeyboardVisible),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceXXL,
|
||||||
|
|
||||||
|
// Form Fields
|
||||||
|
_buildFormSection(theme, colorScheme, isLoading),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceXL,
|
||||||
|
|
||||||
|
// Terms Agreement
|
||||||
|
_buildTermsAgreement(theme, colorScheme, isLoading),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceXL,
|
||||||
|
|
||||||
|
// Register Button
|
||||||
|
_buildRegisterButton(isLoading),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceXL,
|
||||||
|
|
||||||
|
// Footer Links
|
||||||
|
_buildFooterSection(theme, colorScheme),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeaderSection(
|
||||||
|
ThemeData theme,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
bool isKeyboardVisible,
|
||||||
|
) {
|
||||||
|
return AnimatedContainer(
|
||||||
|
duration: AppSpacing.animationNormal,
|
||||||
|
height: isKeyboardVisible ? 100 : 140,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// App Icon/Logo
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.primaryContainer.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.primaryContainer.withValues(alpha: 0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.person_add_outlined,
|
||||||
|
size: isKeyboardVisible ? 40 : 56,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceLG,
|
||||||
|
|
||||||
|
// Welcome Text
|
||||||
|
Text(
|
||||||
|
'Create Account',
|
||||||
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceSM,
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Sign up to get started',
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFormSection(
|
||||||
|
ThemeData theme,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
bool isLoading,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Full Name Field
|
||||||
|
AuthTextField(
|
||||||
|
controller: _nameController,
|
||||||
|
focusNode: _nameFocusNode,
|
||||||
|
labelText: 'Full Name',
|
||||||
|
hintText: 'Enter your full name',
|
||||||
|
keyboardType: TextInputType.name,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
validator: _validateName,
|
||||||
|
enabled: !isLoading,
|
||||||
|
autofillHints: const [AutofillHints.name],
|
||||||
|
prefixIcon: const Icon(Icons.person_outline),
|
||||||
|
onFieldSubmitted: (_) => _emailFocusNode.requestFocus(),
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceLG,
|
||||||
|
|
||||||
|
// Email Field
|
||||||
|
AuthTextField(
|
||||||
|
controller: _emailController,
|
||||||
|
focusNode: _emailFocusNode,
|
||||||
|
labelText: 'Email Address',
|
||||||
|
hintText: 'Enter your email',
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
validator: _validateEmail,
|
||||||
|
enabled: !isLoading,
|
||||||
|
autofillHints: const [AutofillHints.email],
|
||||||
|
prefixIcon: const Icon(Icons.email_outlined),
|
||||||
|
onFieldSubmitted: (_) => _passwordFocusNode.requestFocus(),
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceLG,
|
||||||
|
|
||||||
|
// Password Field
|
||||||
|
AuthTextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
focusNode: _passwordFocusNode,
|
||||||
|
labelText: 'Password',
|
||||||
|
hintText: 'Create a strong password',
|
||||||
|
obscureText: !_isPasswordVisible,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
validator: _validatePassword,
|
||||||
|
enabled: !isLoading,
|
||||||
|
autofillHints: const [AutofillHints.newPassword],
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isPasswordVisible
|
||||||
|
? Icons.visibility_off_outlined
|
||||||
|
: Icons.visibility_outlined,
|
||||||
|
),
|
||||||
|
onPressed: isLoading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
_isPasswordVisible = !_isPasswordVisible;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isPasswordVisible ? 'Hide password' : 'Show password',
|
||||||
|
),
|
||||||
|
onFieldSubmitted: (_) => _confirmPasswordFocusNode.requestFocus(),
|
||||||
|
),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceSM,
|
||||||
|
|
||||||
|
// Password Requirements
|
||||||
|
_buildPasswordRequirements(theme, colorScheme),
|
||||||
|
|
||||||
|
AppSpacing.verticalSpaceLG,
|
||||||
|
|
||||||
|
// Confirm Password Field
|
||||||
|
AuthTextField(
|
||||||
|
controller: _confirmPasswordController,
|
||||||
|
focusNode: _confirmPasswordFocusNode,
|
||||||
|
labelText: 'Confirm Password',
|
||||||
|
hintText: 'Confirm your password',
|
||||||
|
obscureText: !_isConfirmPasswordVisible,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
validator: _validateConfirmPassword,
|
||||||
|
enabled: !isLoading,
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isConfirmPasswordVisible
|
||||||
|
? Icons.visibility_off_outlined
|
||||||
|
: Icons.visibility_outlined,
|
||||||
|
),
|
||||||
|
onPressed: isLoading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
_isConfirmPasswordVisible = !_isConfirmPasswordVisible;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: _isConfirmPasswordVisible
|
||||||
|
? 'Hide password'
|
||||||
|
: 'Show password',
|
||||||
|
),
|
||||||
|
onFieldSubmitted: (_) => _handleRegister(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPasswordRequirements(ThemeData theme, ColorScheme colorScheme) {
|
||||||
|
final password = _passwordController.text;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||||
|
borderRadius: AppSpacing.radiusSM,
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Password Requirements:',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AppSpacing.verticalSpaceXS,
|
||||||
|
_buildRequirementItem(
|
||||||
|
'At least 8 characters',
|
||||||
|
password.length >= 8,
|
||||||
|
theme,
|
||||||
|
colorScheme,
|
||||||
|
),
|
||||||
|
_buildRequirementItem(
|
||||||
|
'Contains uppercase letter',
|
||||||
|
RegExp(r'[A-Z]').hasMatch(password),
|
||||||
|
theme,
|
||||||
|
colorScheme,
|
||||||
|
),
|
||||||
|
_buildRequirementItem(
|
||||||
|
'Contains lowercase letter',
|
||||||
|
RegExp(r'[a-z]').hasMatch(password),
|
||||||
|
theme,
|
||||||
|
colorScheme,
|
||||||
|
),
|
||||||
|
_buildRequirementItem(
|
||||||
|
'Contains number',
|
||||||
|
RegExp(r'\d').hasMatch(password),
|
||||||
|
theme,
|
||||||
|
colorScheme,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRequirementItem(
|
||||||
|
String text,
|
||||||
|
bool isValid,
|
||||||
|
ThemeData theme,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isValid ? Icons.check_circle : Icons.radio_button_unchecked,
|
||||||
|
size: 16,
|
||||||
|
color: isValid
|
||||||
|
? (theme.extension<AppColorsExtension>()?.success ?? colorScheme.primary)
|
||||||
|
: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
AppSpacing.horizontalSpaceXS,
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: isValid
|
||||||
|
? (theme.extension<AppColorsExtension>()?.success ?? colorScheme.primary)
|
||||||
|
: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTermsAgreement(
|
||||||
|
ThemeData theme,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
bool isLoading,
|
||||||
|
) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: _agreeToTerms,
|
||||||
|
onChanged: isLoading
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
setState(() {
|
||||||
|
_agreeToTerms = value ?? false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
AppSpacing.horizontalSpaceSM,
|
||||||
|
Expanded(
|
||||||
|
child: Wrap(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'I agree to the ',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Navigate to terms of service
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Terms of Service',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
' and ',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Navigate to privacy policy
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Privacy Policy',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRegisterButton(bool isLoading) {
|
||||||
|
return AuthButton(
|
||||||
|
onPressed: isLoading ? null : _handleRegister,
|
||||||
|
text: 'Create Account',
|
||||||
|
isLoading: isLoading,
|
||||||
|
type: AuthButtonType.filled,
|
||||||
|
icon: isLoading ? null : const Icon(Icons.person_add),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFooterSection(ThemeData theme, ColorScheme colorScheme) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Sign In Link
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Already have an account? ',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _handleSignIn,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.sm,
|
||||||
|
vertical: AppSpacing.xs,
|
||||||
|
),
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Sign In',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
145
lib/features/auth/presentation/providers/auth_providers.dart
Normal file
145
lib/features/auth/presentation/providers/auth_providers.dart
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import '../../../../core/network/dio_client.dart';
|
||||||
|
import '../../../../core/network/network_info.dart';
|
||||||
|
import '../../../../core/providers/network_providers.dart';
|
||||||
|
import '../../../../shared/presentation/providers/app_providers.dart' hide secureStorageProvider;
|
||||||
|
import '../../../../shared/domain/usecases/usecase.dart';
|
||||||
|
import '../../data/datasources/auth_local_datasource.dart';
|
||||||
|
import '../../data/datasources/auth_remote_datasource.dart';
|
||||||
|
import '../../data/repositories/auth_repository_impl.dart';
|
||||||
|
import '../../domain/entities/user.dart';
|
||||||
|
import '../../domain/repositories/auth_repository.dart';
|
||||||
|
import '../../domain/usecases/get_current_user_usecase.dart';
|
||||||
|
import '../../domain/usecases/login_usecase.dart';
|
||||||
|
import '../../domain/usecases/logout_usecase.dart';
|
||||||
|
import 'auth_state.dart';
|
||||||
|
|
||||||
|
part 'auth_providers.g.dart';
|
||||||
|
|
||||||
|
// Data sources
|
||||||
|
@riverpod
|
||||||
|
AuthRemoteDataSource authRemoteDataSource(Ref ref) {
|
||||||
|
final dioClient = ref.watch(dioClientProvider);
|
||||||
|
return AuthRemoteDataSourceImpl(dioClient: dioClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
AuthLocalDataSource authLocalDataSource(Ref ref) {
|
||||||
|
final secureStorage = ref.watch(secureStorageProvider);
|
||||||
|
return AuthLocalDataSourceImpl(secureStorage: secureStorage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository
|
||||||
|
@riverpod
|
||||||
|
AuthRepository authRepository(Ref ref) {
|
||||||
|
final remoteDataSource = ref.watch(authRemoteDataSourceProvider);
|
||||||
|
final localDataSource = ref.watch(authLocalDataSourceProvider);
|
||||||
|
final networkInfo = ref.watch(networkInfoProvider);
|
||||||
|
|
||||||
|
return AuthRepositoryImpl(
|
||||||
|
remoteDataSource: remoteDataSource,
|
||||||
|
localDataSource: localDataSource,
|
||||||
|
networkInfo: networkInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cases
|
||||||
|
@riverpod
|
||||||
|
LoginUseCase loginUseCase(Ref ref) {
|
||||||
|
final repository = ref.watch(authRepositoryProvider);
|
||||||
|
return LoginUseCase(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
LogoutUseCase logoutUseCase(Ref ref) {
|
||||||
|
final repository = ref.watch(authRepositoryProvider);
|
||||||
|
return LogoutUseCase(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
GetCurrentUserUseCase getCurrentUserUseCase(Ref ref) {
|
||||||
|
final repository = ref.watch(authRepositoryProvider);
|
||||||
|
return GetCurrentUserUseCase(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth state notifier
|
||||||
|
@riverpod
|
||||||
|
class AuthNotifier extends _$AuthNotifier {
|
||||||
|
@override
|
||||||
|
AuthState build() {
|
||||||
|
// Check for cached user on startup
|
||||||
|
_checkAuthStatus();
|
||||||
|
return const AuthState.initial();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkAuthStatus() async {
|
||||||
|
final getCurrentUser = ref.read(getCurrentUserUseCaseProvider);
|
||||||
|
final result = await getCurrentUser(const NoParams());
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => state = AuthState.unauthenticated(failure.message),
|
||||||
|
(user) {
|
||||||
|
if (user != null) {
|
||||||
|
state = AuthState.authenticated(user);
|
||||||
|
} else {
|
||||||
|
state = const AuthState.unauthenticated();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> login({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
}) async {
|
||||||
|
state = const AuthState.loading();
|
||||||
|
|
||||||
|
final loginUseCase = ref.read(loginUseCaseProvider);
|
||||||
|
final params = LoginParams(email: email, password: password);
|
||||||
|
final result = await loginUseCase(params);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => state = AuthState.error(failure.message),
|
||||||
|
(user) => state = AuthState.authenticated(user),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
state = const AuthState.loading();
|
||||||
|
|
||||||
|
final logoutUseCase = ref.read(logoutUseCaseProvider);
|
||||||
|
final result = await logoutUseCase(const NoParams());
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => state = AuthState.error(failure.message),
|
||||||
|
(_) => state = const AuthState.unauthenticated(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearError() {
|
||||||
|
if (state is AuthStateError) {
|
||||||
|
state = const AuthState.unauthenticated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current user provider
|
||||||
|
@riverpod
|
||||||
|
User? currentUser(Ref ref) {
|
||||||
|
final authState = ref.watch(authNotifierProvider);
|
||||||
|
return authState.maybeWhen(
|
||||||
|
authenticated: (user) => user,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is authenticated provider
|
||||||
|
@riverpod
|
||||||
|
bool isAuthenticated(Ref ref) {
|
||||||
|
final authState = ref.watch(authNotifierProvider);
|
||||||
|
return authState.maybeWhen(
|
||||||
|
authenticated: (_) => true,
|
||||||
|
orElse: () => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
150
lib/features/auth/presentation/providers/auth_providers.g.dart
Normal file
150
lib/features/auth/presentation/providers/auth_providers.g.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'auth_providers.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$authRemoteDataSourceHash() =>
|
||||||
|
r'e1e2164defcfc3905e9fb8e75e346817a6e0bf73';
|
||||||
|
|
||||||
|
/// See also [authRemoteDataSource].
|
||||||
|
@ProviderFor(authRemoteDataSource)
|
||||||
|
final authRemoteDataSourceProvider =
|
||||||
|
AutoDisposeProvider<AuthRemoteDataSource>.internal(
|
||||||
|
authRemoteDataSource,
|
||||||
|
name: r'authRemoteDataSourceProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$authRemoteDataSourceHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef AuthRemoteDataSourceRef = AutoDisposeProviderRef<AuthRemoteDataSource>;
|
||||||
|
String _$authLocalDataSourceHash() =>
|
||||||
|
r'dfab2fdd71de815f93c16ab9e234bd2d0885d2f4';
|
||||||
|
|
||||||
|
/// See also [authLocalDataSource].
|
||||||
|
@ProviderFor(authLocalDataSource)
|
||||||
|
final authLocalDataSourceProvider =
|
||||||
|
AutoDisposeProvider<AuthLocalDataSource>.internal(
|
||||||
|
authLocalDataSource,
|
||||||
|
name: r'authLocalDataSourceProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$authLocalDataSourceHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef AuthLocalDataSourceRef = AutoDisposeProviderRef<AuthLocalDataSource>;
|
||||||
|
String _$authRepositoryHash() => r'8ce22ed16336f42a50e8266fbafbdbd7db71d613';
|
||||||
|
|
||||||
|
/// See also [authRepository].
|
||||||
|
@ProviderFor(authRepository)
|
||||||
|
final authRepositoryProvider = AutoDisposeProvider<AuthRepository>.internal(
|
||||||
|
authRepository,
|
||||||
|
name: r'authRepositoryProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$authRepositoryHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef AuthRepositoryRef = AutoDisposeProviderRef<AuthRepository>;
|
||||||
|
String _$loginUseCaseHash() => r'cbfd4200f40c132516f20f942ae9d825a31e2515';
|
||||||
|
|
||||||
|
/// See also [loginUseCase].
|
||||||
|
@ProviderFor(loginUseCase)
|
||||||
|
final loginUseCaseProvider = AutoDisposeProvider<LoginUseCase>.internal(
|
||||||
|
loginUseCase,
|
||||||
|
name: r'loginUseCaseProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$loginUseCaseHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef LoginUseCaseRef = AutoDisposeProviderRef<LoginUseCase>;
|
||||||
|
String _$logoutUseCaseHash() => r'67224f00aebb158eab2aba2c4398e98150dd958c';
|
||||||
|
|
||||||
|
/// See also [logoutUseCase].
|
||||||
|
@ProviderFor(logoutUseCase)
|
||||||
|
final logoutUseCaseProvider = AutoDisposeProvider<LogoutUseCase>.internal(
|
||||||
|
logoutUseCase,
|
||||||
|
name: r'logoutUseCaseProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$logoutUseCaseHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef LogoutUseCaseRef = AutoDisposeProviderRef<LogoutUseCase>;
|
||||||
|
String _$getCurrentUserUseCaseHash() =>
|
||||||
|
r'1e9d6222283b80c2b6fc6ed8c89f4130614c0a11';
|
||||||
|
|
||||||
|
/// See also [getCurrentUserUseCase].
|
||||||
|
@ProviderFor(getCurrentUserUseCase)
|
||||||
|
final getCurrentUserUseCaseProvider =
|
||||||
|
AutoDisposeProvider<GetCurrentUserUseCase>.internal(
|
||||||
|
getCurrentUserUseCase,
|
||||||
|
name: r'getCurrentUserUseCaseProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$getCurrentUserUseCaseHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef GetCurrentUserUseCaseRef
|
||||||
|
= AutoDisposeProviderRef<GetCurrentUserUseCase>;
|
||||||
|
String _$currentUserHash() => r'a5dbfda090aa4a2784b934352ff00cf3c751332b';
|
||||||
|
|
||||||
|
/// See also [currentUser].
|
||||||
|
@ProviderFor(currentUser)
|
||||||
|
final currentUserProvider = AutoDisposeProvider<User?>.internal(
|
||||||
|
currentUser,
|
||||||
|
name: r'currentUserProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$currentUserHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef CurrentUserRef = AutoDisposeProviderRef<User?>;
|
||||||
|
String _$isAuthenticatedHash() => r'0f8559d2c47c9554b3c1b9643ed0c2bf1cb18727';
|
||||||
|
|
||||||
|
/// See also [isAuthenticated].
|
||||||
|
@ProviderFor(isAuthenticated)
|
||||||
|
final isAuthenticatedProvider = AutoDisposeProvider<bool>.internal(
|
||||||
|
isAuthenticated,
|
||||||
|
name: r'isAuthenticatedProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$isAuthenticatedHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef IsAuthenticatedRef = AutoDisposeProviderRef<bool>;
|
||||||
|
String _$authNotifierHash() => r'e97041b6776589adb6e6d424d2ebbb7bc837cb5b';
|
||||||
|
|
||||||
|
/// See also [AuthNotifier].
|
||||||
|
@ProviderFor(AuthNotifier)
|
||||||
|
final authNotifierProvider =
|
||||||
|
AutoDisposeNotifierProvider<AuthNotifier, AuthState>.internal(
|
||||||
|
AuthNotifier.new,
|
||||||
|
name: r'authNotifierProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$authNotifierHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$AuthNotifier = AutoDisposeNotifier<AuthState>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
13
lib/features/auth/presentation/providers/auth_state.dart
Normal file
13
lib/features/auth/presentation/providers/auth_state.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import '../../domain/entities/user.dart';
|
||||||
|
|
||||||
|
part 'auth_state.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class AuthState with _$AuthState {
|
||||||
|
const factory AuthState.initial() = AuthStateInitial;
|
||||||
|
const factory AuthState.loading() = AuthStateLoading;
|
||||||
|
const factory AuthState.authenticated(User user) = AuthStateAuthenticated;
|
||||||
|
const factory AuthState.unauthenticated([String? message]) = AuthStateUnauthenticated;
|
||||||
|
const factory AuthState.error(String message) = AuthStateError;
|
||||||
|
}
|
||||||
794
lib/features/auth/presentation/providers/auth_state.freezed.dart
Normal file
794
lib/features/auth/presentation/providers/auth_state.freezed.dart
Normal file
@@ -0,0 +1,794 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'auth_state.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$AuthState {
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function() initial,
|
||||||
|
required TResult Function() loading,
|
||||||
|
required TResult Function(User user) authenticated,
|
||||||
|
required TResult Function(String? message) unauthenticated,
|
||||||
|
required TResult Function(String message) error,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function()? initial,
|
||||||
|
TResult? Function()? loading,
|
||||||
|
TResult? Function(User user)? authenticated,
|
||||||
|
TResult? Function(String? message)? unauthenticated,
|
||||||
|
TResult? Function(String message)? error,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function()? initial,
|
||||||
|
TResult Function()? loading,
|
||||||
|
TResult Function(User user)? authenticated,
|
||||||
|
TResult Function(String? message)? unauthenticated,
|
||||||
|
TResult Function(String message)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(AuthStateInitial value) initial,
|
||||||
|
required TResult Function(AuthStateLoading value) loading,
|
||||||
|
required TResult Function(AuthStateAuthenticated value) authenticated,
|
||||||
|
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
|
||||||
|
required TResult Function(AuthStateError value) error,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(AuthStateInitial value)? initial,
|
||||||
|
TResult? Function(AuthStateLoading value)? loading,
|
||||||
|
TResult? Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult? Function(AuthStateError value)? error,
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(AuthStateInitial value)? initial,
|
||||||
|
TResult Function(AuthStateLoading value)? loading,
|
||||||
|
TResult Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult Function(AuthStateError value)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $AuthStateCopyWith<$Res> {
|
||||||
|
factory $AuthStateCopyWith(AuthState value, $Res Function(AuthState) then) =
|
||||||
|
_$AuthStateCopyWithImpl<$Res, AuthState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$AuthStateCopyWithImpl<$Res, $Val extends AuthState>
|
||||||
|
implements $AuthStateCopyWith<$Res> {
|
||||||
|
_$AuthStateCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$AuthStateInitialImplCopyWith<$Res> {
|
||||||
|
factory _$$AuthStateInitialImplCopyWith(_$AuthStateInitialImpl value,
|
||||||
|
$Res Function(_$AuthStateInitialImpl) then) =
|
||||||
|
__$$AuthStateInitialImplCopyWithImpl<$Res>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$AuthStateInitialImplCopyWithImpl<$Res>
|
||||||
|
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateInitialImpl>
|
||||||
|
implements _$$AuthStateInitialImplCopyWith<$Res> {
|
||||||
|
__$$AuthStateInitialImplCopyWithImpl(_$AuthStateInitialImpl _value,
|
||||||
|
$Res Function(_$AuthStateInitialImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$AuthStateInitialImpl implements AuthStateInitial {
|
||||||
|
const _$AuthStateInitialImpl();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AuthState.initial()';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType && other is _$AuthStateInitialImpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => runtimeType.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function() initial,
|
||||||
|
required TResult Function() loading,
|
||||||
|
required TResult Function(User user) authenticated,
|
||||||
|
required TResult Function(String? message) unauthenticated,
|
||||||
|
required TResult Function(String message) error,
|
||||||
|
}) {
|
||||||
|
return initial();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function()? initial,
|
||||||
|
TResult? Function()? loading,
|
||||||
|
TResult? Function(User user)? authenticated,
|
||||||
|
TResult? Function(String? message)? unauthenticated,
|
||||||
|
TResult? Function(String message)? error,
|
||||||
|
}) {
|
||||||
|
return initial?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function()? initial,
|
||||||
|
TResult Function()? loading,
|
||||||
|
TResult Function(User user)? authenticated,
|
||||||
|
TResult Function(String? message)? unauthenticated,
|
||||||
|
TResult Function(String message)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (initial != null) {
|
||||||
|
return initial();
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(AuthStateInitial value) initial,
|
||||||
|
required TResult Function(AuthStateLoading value) loading,
|
||||||
|
required TResult Function(AuthStateAuthenticated value) authenticated,
|
||||||
|
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
|
||||||
|
required TResult Function(AuthStateError value) error,
|
||||||
|
}) {
|
||||||
|
return initial(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(AuthStateInitial value)? initial,
|
||||||
|
TResult? Function(AuthStateLoading value)? loading,
|
||||||
|
TResult? Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult? Function(AuthStateError value)? error,
|
||||||
|
}) {
|
||||||
|
return initial?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(AuthStateInitial value)? initial,
|
||||||
|
TResult Function(AuthStateLoading value)? loading,
|
||||||
|
TResult Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult Function(AuthStateError value)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (initial != null) {
|
||||||
|
return initial(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AuthStateInitial implements AuthState {
|
||||||
|
const factory AuthStateInitial() = _$AuthStateInitialImpl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$AuthStateLoadingImplCopyWith<$Res> {
|
||||||
|
factory _$$AuthStateLoadingImplCopyWith(_$AuthStateLoadingImpl value,
|
||||||
|
$Res Function(_$AuthStateLoadingImpl) then) =
|
||||||
|
__$$AuthStateLoadingImplCopyWithImpl<$Res>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$AuthStateLoadingImplCopyWithImpl<$Res>
|
||||||
|
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateLoadingImpl>
|
||||||
|
implements _$$AuthStateLoadingImplCopyWith<$Res> {
|
||||||
|
__$$AuthStateLoadingImplCopyWithImpl(_$AuthStateLoadingImpl _value,
|
||||||
|
$Res Function(_$AuthStateLoadingImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$AuthStateLoadingImpl implements AuthStateLoading {
|
||||||
|
const _$AuthStateLoadingImpl();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AuthState.loading()';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType && other is _$AuthStateLoadingImpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => runtimeType.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function() initial,
|
||||||
|
required TResult Function() loading,
|
||||||
|
required TResult Function(User user) authenticated,
|
||||||
|
required TResult Function(String? message) unauthenticated,
|
||||||
|
required TResult Function(String message) error,
|
||||||
|
}) {
|
||||||
|
return loading();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function()? initial,
|
||||||
|
TResult? Function()? loading,
|
||||||
|
TResult? Function(User user)? authenticated,
|
||||||
|
TResult? Function(String? message)? unauthenticated,
|
||||||
|
TResult? Function(String message)? error,
|
||||||
|
}) {
|
||||||
|
return loading?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function()? initial,
|
||||||
|
TResult Function()? loading,
|
||||||
|
TResult Function(User user)? authenticated,
|
||||||
|
TResult Function(String? message)? unauthenticated,
|
||||||
|
TResult Function(String message)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (loading != null) {
|
||||||
|
return loading();
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(AuthStateInitial value) initial,
|
||||||
|
required TResult Function(AuthStateLoading value) loading,
|
||||||
|
required TResult Function(AuthStateAuthenticated value) authenticated,
|
||||||
|
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
|
||||||
|
required TResult Function(AuthStateError value) error,
|
||||||
|
}) {
|
||||||
|
return loading(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(AuthStateInitial value)? initial,
|
||||||
|
TResult? Function(AuthStateLoading value)? loading,
|
||||||
|
TResult? Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult? Function(AuthStateError value)? error,
|
||||||
|
}) {
|
||||||
|
return loading?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(AuthStateInitial value)? initial,
|
||||||
|
TResult Function(AuthStateLoading value)? loading,
|
||||||
|
TResult Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult Function(AuthStateError value)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (loading != null) {
|
||||||
|
return loading(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AuthStateLoading implements AuthState {
|
||||||
|
const factory AuthStateLoading() = _$AuthStateLoadingImpl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$AuthStateAuthenticatedImplCopyWith<$Res> {
|
||||||
|
factory _$$AuthStateAuthenticatedImplCopyWith(
|
||||||
|
_$AuthStateAuthenticatedImpl value,
|
||||||
|
$Res Function(_$AuthStateAuthenticatedImpl) then) =
|
||||||
|
__$$AuthStateAuthenticatedImplCopyWithImpl<$Res>;
|
||||||
|
@useResult
|
||||||
|
$Res call({User user});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$AuthStateAuthenticatedImplCopyWithImpl<$Res>
|
||||||
|
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateAuthenticatedImpl>
|
||||||
|
implements _$$AuthStateAuthenticatedImplCopyWith<$Res> {
|
||||||
|
__$$AuthStateAuthenticatedImplCopyWithImpl(
|
||||||
|
_$AuthStateAuthenticatedImpl _value,
|
||||||
|
$Res Function(_$AuthStateAuthenticatedImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? user = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$AuthStateAuthenticatedImpl(
|
||||||
|
null == user
|
||||||
|
? _value.user
|
||||||
|
: user // ignore: cast_nullable_to_non_nullable
|
||||||
|
as User,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$AuthStateAuthenticatedImpl implements AuthStateAuthenticated {
|
||||||
|
const _$AuthStateAuthenticatedImpl(this.user);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AuthState.authenticated(user: $user)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$AuthStateAuthenticatedImpl &&
|
||||||
|
(identical(other.user, user) || other.user == user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, user);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$AuthStateAuthenticatedImplCopyWith<_$AuthStateAuthenticatedImpl>
|
||||||
|
get copyWith => __$$AuthStateAuthenticatedImplCopyWithImpl<
|
||||||
|
_$AuthStateAuthenticatedImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function() initial,
|
||||||
|
required TResult Function() loading,
|
||||||
|
required TResult Function(User user) authenticated,
|
||||||
|
required TResult Function(String? message) unauthenticated,
|
||||||
|
required TResult Function(String message) error,
|
||||||
|
}) {
|
||||||
|
return authenticated(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function()? initial,
|
||||||
|
TResult? Function()? loading,
|
||||||
|
TResult? Function(User user)? authenticated,
|
||||||
|
TResult? Function(String? message)? unauthenticated,
|
||||||
|
TResult? Function(String message)? error,
|
||||||
|
}) {
|
||||||
|
return authenticated?.call(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function()? initial,
|
||||||
|
TResult Function()? loading,
|
||||||
|
TResult Function(User user)? authenticated,
|
||||||
|
TResult Function(String? message)? unauthenticated,
|
||||||
|
TResult Function(String message)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (authenticated != null) {
|
||||||
|
return authenticated(user);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(AuthStateInitial value) initial,
|
||||||
|
required TResult Function(AuthStateLoading value) loading,
|
||||||
|
required TResult Function(AuthStateAuthenticated value) authenticated,
|
||||||
|
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
|
||||||
|
required TResult Function(AuthStateError value) error,
|
||||||
|
}) {
|
||||||
|
return authenticated(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(AuthStateInitial value)? initial,
|
||||||
|
TResult? Function(AuthStateLoading value)? loading,
|
||||||
|
TResult? Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult? Function(AuthStateError value)? error,
|
||||||
|
}) {
|
||||||
|
return authenticated?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(AuthStateInitial value)? initial,
|
||||||
|
TResult Function(AuthStateLoading value)? loading,
|
||||||
|
TResult Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult Function(AuthStateError value)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (authenticated != null) {
|
||||||
|
return authenticated(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AuthStateAuthenticated implements AuthState {
|
||||||
|
const factory AuthStateAuthenticated(final User user) =
|
||||||
|
_$AuthStateAuthenticatedImpl;
|
||||||
|
|
||||||
|
User get user;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$AuthStateAuthenticatedImplCopyWith<_$AuthStateAuthenticatedImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$AuthStateUnauthenticatedImplCopyWith<$Res> {
|
||||||
|
factory _$$AuthStateUnauthenticatedImplCopyWith(
|
||||||
|
_$AuthStateUnauthenticatedImpl value,
|
||||||
|
$Res Function(_$AuthStateUnauthenticatedImpl) then) =
|
||||||
|
__$$AuthStateUnauthenticatedImplCopyWithImpl<$Res>;
|
||||||
|
@useResult
|
||||||
|
$Res call({String? message});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$AuthStateUnauthenticatedImplCopyWithImpl<$Res>
|
||||||
|
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateUnauthenticatedImpl>
|
||||||
|
implements _$$AuthStateUnauthenticatedImplCopyWith<$Res> {
|
||||||
|
__$$AuthStateUnauthenticatedImplCopyWithImpl(
|
||||||
|
_$AuthStateUnauthenticatedImpl _value,
|
||||||
|
$Res Function(_$AuthStateUnauthenticatedImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? message = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$AuthStateUnauthenticatedImpl(
|
||||||
|
freezed == message
|
||||||
|
? _value.message
|
||||||
|
: message // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$AuthStateUnauthenticatedImpl implements AuthStateUnauthenticated {
|
||||||
|
const _$AuthStateUnauthenticatedImpl([this.message]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AuthState.unauthenticated(message: $message)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$AuthStateUnauthenticatedImpl &&
|
||||||
|
(identical(other.message, message) || other.message == message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, message);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$AuthStateUnauthenticatedImplCopyWith<_$AuthStateUnauthenticatedImpl>
|
||||||
|
get copyWith => __$$AuthStateUnauthenticatedImplCopyWithImpl<
|
||||||
|
_$AuthStateUnauthenticatedImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function() initial,
|
||||||
|
required TResult Function() loading,
|
||||||
|
required TResult Function(User user) authenticated,
|
||||||
|
required TResult Function(String? message) unauthenticated,
|
||||||
|
required TResult Function(String message) error,
|
||||||
|
}) {
|
||||||
|
return unauthenticated(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function()? initial,
|
||||||
|
TResult? Function()? loading,
|
||||||
|
TResult? Function(User user)? authenticated,
|
||||||
|
TResult? Function(String? message)? unauthenticated,
|
||||||
|
TResult? Function(String message)? error,
|
||||||
|
}) {
|
||||||
|
return unauthenticated?.call(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function()? initial,
|
||||||
|
TResult Function()? loading,
|
||||||
|
TResult Function(User user)? authenticated,
|
||||||
|
TResult Function(String? message)? unauthenticated,
|
||||||
|
TResult Function(String message)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (unauthenticated != null) {
|
||||||
|
return unauthenticated(message);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(AuthStateInitial value) initial,
|
||||||
|
required TResult Function(AuthStateLoading value) loading,
|
||||||
|
required TResult Function(AuthStateAuthenticated value) authenticated,
|
||||||
|
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
|
||||||
|
required TResult Function(AuthStateError value) error,
|
||||||
|
}) {
|
||||||
|
return unauthenticated(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(AuthStateInitial value)? initial,
|
||||||
|
TResult? Function(AuthStateLoading value)? loading,
|
||||||
|
TResult? Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult? Function(AuthStateError value)? error,
|
||||||
|
}) {
|
||||||
|
return unauthenticated?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(AuthStateInitial value)? initial,
|
||||||
|
TResult Function(AuthStateLoading value)? loading,
|
||||||
|
TResult Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult Function(AuthStateError value)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (unauthenticated != null) {
|
||||||
|
return unauthenticated(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AuthStateUnauthenticated implements AuthState {
|
||||||
|
const factory AuthStateUnauthenticated([final String? message]) =
|
||||||
|
_$AuthStateUnauthenticatedImpl;
|
||||||
|
|
||||||
|
String? get message;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$AuthStateUnauthenticatedImplCopyWith<_$AuthStateUnauthenticatedImpl>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$AuthStateErrorImplCopyWith<$Res> {
|
||||||
|
factory _$$AuthStateErrorImplCopyWith(_$AuthStateErrorImpl value,
|
||||||
|
$Res Function(_$AuthStateErrorImpl) then) =
|
||||||
|
__$$AuthStateErrorImplCopyWithImpl<$Res>;
|
||||||
|
@useResult
|
||||||
|
$Res call({String message});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$AuthStateErrorImplCopyWithImpl<$Res>
|
||||||
|
extends _$AuthStateCopyWithImpl<$Res, _$AuthStateErrorImpl>
|
||||||
|
implements _$$AuthStateErrorImplCopyWith<$Res> {
|
||||||
|
__$$AuthStateErrorImplCopyWithImpl(
|
||||||
|
_$AuthStateErrorImpl _value, $Res Function(_$AuthStateErrorImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? message = null,
|
||||||
|
}) {
|
||||||
|
return _then(_$AuthStateErrorImpl(
|
||||||
|
null == message
|
||||||
|
? _value.message
|
||||||
|
: message // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$AuthStateErrorImpl implements AuthStateError {
|
||||||
|
const _$AuthStateErrorImpl(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AuthState.error(message: $message)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$AuthStateErrorImpl &&
|
||||||
|
(identical(other.message, message) || other.message == message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, message);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$AuthStateErrorImplCopyWith<_$AuthStateErrorImpl> get copyWith =>
|
||||||
|
__$$AuthStateErrorImplCopyWithImpl<_$AuthStateErrorImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult when<TResult extends Object?>({
|
||||||
|
required TResult Function() initial,
|
||||||
|
required TResult Function() loading,
|
||||||
|
required TResult Function(User user) authenticated,
|
||||||
|
required TResult Function(String? message) unauthenticated,
|
||||||
|
required TResult Function(String message) error,
|
||||||
|
}) {
|
||||||
|
return error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function()? initial,
|
||||||
|
TResult? Function()? loading,
|
||||||
|
TResult? Function(User user)? authenticated,
|
||||||
|
TResult? Function(String? message)? unauthenticated,
|
||||||
|
TResult? Function(String message)? error,
|
||||||
|
}) {
|
||||||
|
return error?.call(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
|
TResult Function()? initial,
|
||||||
|
TResult Function()? loading,
|
||||||
|
TResult Function(User user)? authenticated,
|
||||||
|
TResult Function(String? message)? unauthenticated,
|
||||||
|
TResult Function(String message)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (error != null) {
|
||||||
|
return error(message);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult map<TResult extends Object?>({
|
||||||
|
required TResult Function(AuthStateInitial value) initial,
|
||||||
|
required TResult Function(AuthStateLoading value) loading,
|
||||||
|
required TResult Function(AuthStateAuthenticated value) authenticated,
|
||||||
|
required TResult Function(AuthStateUnauthenticated value) unauthenticated,
|
||||||
|
required TResult Function(AuthStateError value) error,
|
||||||
|
}) {
|
||||||
|
return error(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult? mapOrNull<TResult extends Object?>({
|
||||||
|
TResult? Function(AuthStateInitial value)? initial,
|
||||||
|
TResult? Function(AuthStateLoading value)? loading,
|
||||||
|
TResult? Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult? Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult? Function(AuthStateError value)? error,
|
||||||
|
}) {
|
||||||
|
return error?.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@optionalTypeArgs
|
||||||
|
TResult maybeMap<TResult extends Object?>({
|
||||||
|
TResult Function(AuthStateInitial value)? initial,
|
||||||
|
TResult Function(AuthStateLoading value)? loading,
|
||||||
|
TResult Function(AuthStateAuthenticated value)? authenticated,
|
||||||
|
TResult Function(AuthStateUnauthenticated value)? unauthenticated,
|
||||||
|
TResult Function(AuthStateError value)? error,
|
||||||
|
required TResult orElse(),
|
||||||
|
}) {
|
||||||
|
if (error != null) {
|
||||||
|
return error(this);
|
||||||
|
}
|
||||||
|
return orElse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AuthStateError implements AuthState {
|
||||||
|
const factory AuthStateError(final String message) = _$AuthStateErrorImpl;
|
||||||
|
|
||||||
|
String get message;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$AuthStateErrorImplCopyWith<_$AuthStateErrorImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
308
lib/features/auth/presentation/widgets/auth_button.dart
Normal file
308
lib/features/auth/presentation/widgets/auth_button.dart
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../../../core/theme/app_spacing.dart';
|
||||||
|
|
||||||
|
/// Reusable primary button specifically designed for authentication actions
|
||||||
|
/// Provides consistent styling, loading states, and accessibility features
|
||||||
|
class AuthButton extends StatelessWidget {
|
||||||
|
const AuthButton({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.text,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.isEnabled = true,
|
||||||
|
this.type = AuthButtonType.filled,
|
||||||
|
this.icon,
|
||||||
|
this.width = double.infinity,
|
||||||
|
this.height = AppSpacing.buttonHeightLarge,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final String text;
|
||||||
|
final bool isLoading;
|
||||||
|
final bool isEnabled;
|
||||||
|
final AuthButtonType type;
|
||||||
|
final Widget? icon;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
final isButtonEnabled = isEnabled && !isLoading && onPressed != null;
|
||||||
|
|
||||||
|
Widget child = _buildButtonChild(theme, colorScheme);
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: AppSpacing.animationNormal,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: _buildButtonByType(context, child, isButtonEnabled),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButtonChild(ThemeData theme, ColorScheme colorScheme) {
|
||||||
|
if (isLoading) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: AppSpacing.iconSM,
|
||||||
|
height: AppSpacing.iconSM,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: _getLoadingIndicatorColor(colorScheme),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AppSpacing.horizontalSpaceSM,
|
||||||
|
Text(
|
||||||
|
'Please wait...',
|
||||||
|
style: _getTextStyle(theme, colorScheme),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icon != null) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconTheme(
|
||||||
|
data: IconThemeData(
|
||||||
|
color: _getIconColor(colorScheme),
|
||||||
|
size: AppSpacing.iconSM,
|
||||||
|
),
|
||||||
|
child: icon!,
|
||||||
|
),
|
||||||
|
AppSpacing.horizontalSpaceSM,
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: _getTextStyle(theme, colorScheme),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
style: _getTextStyle(theme, colorScheme),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButtonByType(BuildContext context, Widget child, bool enabled) {
|
||||||
|
switch (type) {
|
||||||
|
case AuthButtonType.filled:
|
||||||
|
return FilledButton(
|
||||||
|
onPressed: enabled ? onPressed : null,
|
||||||
|
style: _getFilledButtonStyle(context),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
case AuthButtonType.outlined:
|
||||||
|
return OutlinedButton(
|
||||||
|
onPressed: enabled ? onPressed : null,
|
||||||
|
style: _getOutlinedButtonStyle(context),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
case AuthButtonType.text:
|
||||||
|
return TextButton(
|
||||||
|
onPressed: enabled ? onPressed : null,
|
||||||
|
style: _getTextButtonStyle(context),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonStyle _getFilledButtonStyle(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return FilledButton.styleFrom(
|
||||||
|
backgroundColor: colorScheme.primary,
|
||||||
|
foregroundColor: colorScheme.onPrimary,
|
||||||
|
disabledBackgroundColor: colorScheme.onSurface.withValues(alpha: 0.12),
|
||||||
|
disabledForegroundColor: colorScheme.onSurface.withValues(alpha: 0.38),
|
||||||
|
elevation: AppSpacing.elevationNone,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: AppSpacing.buttonRadius,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.buttonPaddingHorizontal,
|
||||||
|
vertical: AppSpacing.buttonPaddingVertical,
|
||||||
|
),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.padded,
|
||||||
|
).copyWith(
|
||||||
|
overlayColor: WidgetStateProperty.resolveWith<Color?>(
|
||||||
|
(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.pressed)) {
|
||||||
|
return colorScheme.onPrimary.withValues(alpha: 0.1);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.hovered)) {
|
||||||
|
return colorScheme.onPrimary.withValues(alpha: 0.08);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.focused)) {
|
||||||
|
return colorScheme.onPrimary.withValues(alpha: 0.1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonStyle _getOutlinedButtonStyle(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: colorScheme.primary,
|
||||||
|
disabledForegroundColor: colorScheme.onSurface.withValues(alpha: 0.38),
|
||||||
|
side: BorderSide(
|
||||||
|
color: colorScheme.outline,
|
||||||
|
width: AppSpacing.borderWidth,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: AppSpacing.buttonRadius,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.buttonPaddingHorizontal,
|
||||||
|
vertical: AppSpacing.buttonPaddingVertical,
|
||||||
|
),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.padded,
|
||||||
|
).copyWith(
|
||||||
|
side: WidgetStateProperty.resolveWith<BorderSide?>(
|
||||||
|
(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.disabled)) {
|
||||||
|
return BorderSide(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.12),
|
||||||
|
width: AppSpacing.borderWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.focused)) {
|
||||||
|
return BorderSide(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
width: AppSpacing.borderWidthThick,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return BorderSide(
|
||||||
|
color: colorScheme.outline,
|
||||||
|
width: AppSpacing.borderWidth,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
overlayColor: WidgetStateProperty.resolveWith<Color?>(
|
||||||
|
(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.pressed)) {
|
||||||
|
return colorScheme.primary.withValues(alpha: 0.1);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.hovered)) {
|
||||||
|
return colorScheme.primary.withValues(alpha: 0.08);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.focused)) {
|
||||||
|
return colorScheme.primary.withValues(alpha: 0.1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonStyle _getTextButtonStyle(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return TextButton.styleFrom(
|
||||||
|
foregroundColor: colorScheme.primary,
|
||||||
|
disabledForegroundColor: colorScheme.onSurface.withValues(alpha: 0.38),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: AppSpacing.buttonRadius,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.buttonPaddingHorizontal,
|
||||||
|
vertical: AppSpacing.buttonPaddingVertical,
|
||||||
|
),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.padded,
|
||||||
|
).copyWith(
|
||||||
|
overlayColor: WidgetStateProperty.resolveWith<Color?>(
|
||||||
|
(Set<WidgetState> states) {
|
||||||
|
if (states.contains(WidgetState.pressed)) {
|
||||||
|
return colorScheme.primary.withValues(alpha: 0.1);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.hovered)) {
|
||||||
|
return colorScheme.primary.withValues(alpha: 0.08);
|
||||||
|
}
|
||||||
|
if (states.contains(WidgetState.focused)) {
|
||||||
|
return colorScheme.primary.withValues(alpha: 0.1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle _getTextStyle(ThemeData theme, ColorScheme colorScheme) {
|
||||||
|
final baseStyle = theme.textTheme.labelLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
) ??
|
||||||
|
const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isEnabled || isLoading) {
|
||||||
|
return baseStyle.copyWith(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.38),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case AuthButtonType.filled:
|
||||||
|
return baseStyle.copyWith(color: colorScheme.onPrimary);
|
||||||
|
case AuthButtonType.outlined:
|
||||||
|
case AuthButtonType.text:
|
||||||
|
return baseStyle.copyWith(color: colorScheme.primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getLoadingIndicatorColor(ColorScheme colorScheme) {
|
||||||
|
switch (type) {
|
||||||
|
case AuthButtonType.filled:
|
||||||
|
return colorScheme.onPrimary;
|
||||||
|
case AuthButtonType.outlined:
|
||||||
|
case AuthButtonType.text:
|
||||||
|
return colorScheme.primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getIconColor(ColorScheme colorScheme) {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return colorScheme.onSurface.withValues(alpha: 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case AuthButtonType.filled:
|
||||||
|
return colorScheme.onPrimary;
|
||||||
|
case AuthButtonType.outlined:
|
||||||
|
case AuthButtonType.text:
|
||||||
|
return colorScheme.primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Types of auth buttons available
|
||||||
|
enum AuthButtonType {
|
||||||
|
/// Filled button with primary color background
|
||||||
|
filled,
|
||||||
|
|
||||||
|
/// Outlined button with transparent background and border
|
||||||
|
outlined,
|
||||||
|
|
||||||
|
/// Text button with no background or border
|
||||||
|
text,
|
||||||
|
}
|
||||||
209
lib/features/auth/presentation/widgets/auth_text_field.dart
Normal file
209
lib/features/auth/presentation/widgets/auth_text_field.dart
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import '../../../../core/theme/app_spacing.dart';
|
||||||
|
|
||||||
|
/// Reusable styled text field specifically designed for authentication forms
|
||||||
|
/// Follows Material 3 design guidelines with consistent theming
|
||||||
|
class AuthTextField extends StatefulWidget {
|
||||||
|
const AuthTextField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.labelText,
|
||||||
|
this.hintText,
|
||||||
|
this.prefixIcon,
|
||||||
|
this.suffixIcon,
|
||||||
|
this.validator,
|
||||||
|
this.keyboardType,
|
||||||
|
this.textInputAction,
|
||||||
|
this.onFieldSubmitted,
|
||||||
|
this.onChanged,
|
||||||
|
this.obscureText = false,
|
||||||
|
this.enabled = true,
|
||||||
|
this.autofillHints,
|
||||||
|
this.inputFormatters,
|
||||||
|
this.maxLength,
|
||||||
|
this.focusNode,
|
||||||
|
this.autofocus = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String labelText;
|
||||||
|
final String? hintText;
|
||||||
|
final Widget? prefixIcon;
|
||||||
|
final Widget? suffixIcon;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
final TextInputType? keyboardType;
|
||||||
|
final TextInputAction? textInputAction;
|
||||||
|
final void Function(String)? onFieldSubmitted;
|
||||||
|
final void Function(String)? onChanged;
|
||||||
|
final bool obscureText;
|
||||||
|
final bool enabled;
|
||||||
|
final List<String>? autofillHints;
|
||||||
|
final List<TextInputFormatter>? inputFormatters;
|
||||||
|
final int? maxLength;
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AuthTextField> createState() => _AuthTextFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AuthTextFieldState extends State<AuthTextField> {
|
||||||
|
bool _isFocused = false;
|
||||||
|
late FocusNode _focusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_focusNode = widget.focusNode ?? FocusNode();
|
||||||
|
_focusNode.addListener(_onFocusChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (widget.focusNode == null) {
|
||||||
|
_focusNode.dispose();
|
||||||
|
} else {
|
||||||
|
_focusNode.removeListener(_onFocusChanged);
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFocusChanged() {
|
||||||
|
setState(() {
|
||||||
|
_isFocused = _focusNode.hasFocus;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: widget.controller,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
validator: widget.validator,
|
||||||
|
keyboardType: widget.keyboardType,
|
||||||
|
textInputAction: widget.textInputAction,
|
||||||
|
onFieldSubmitted: widget.onFieldSubmitted,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
obscureText: widget.obscureText,
|
||||||
|
enabled: widget.enabled,
|
||||||
|
autofillHints: widget.autofillHints,
|
||||||
|
inputFormatters: widget.inputFormatters,
|
||||||
|
maxLength: widget.maxLength,
|
||||||
|
autofocus: widget.autofocus,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: widget.enabled ? colorScheme.onSurface : colorScheme.onSurface.withValues(alpha: 0.38),
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: widget.labelText,
|
||||||
|
hintText: widget.hintText,
|
||||||
|
prefixIcon: widget.prefixIcon != null
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(left: AppSpacing.md, right: AppSpacing.sm),
|
||||||
|
child: IconTheme.merge(
|
||||||
|
data: IconThemeData(
|
||||||
|
color: _isFocused
|
||||||
|
? colorScheme.primary
|
||||||
|
: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||||
|
size: AppSpacing.iconMD,
|
||||||
|
),
|
||||||
|
child: widget.prefixIcon!,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
suffixIcon: widget.suffixIcon != null
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(right: AppSpacing.md),
|
||||||
|
child: IconTheme.merge(
|
||||||
|
data: IconThemeData(
|
||||||
|
color: _isFocused
|
||||||
|
? colorScheme.primary
|
||||||
|
: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||||
|
size: AppSpacing.iconMD,
|
||||||
|
),
|
||||||
|
child: widget.suffixIcon!,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
filled: true,
|
||||||
|
fillColor: widget.enabled
|
||||||
|
? (_isFocused
|
||||||
|
? colorScheme.primaryContainer.withValues(alpha: 0.08)
|
||||||
|
: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5))
|
||||||
|
: colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.lg,
|
||||||
|
vertical: AppSpacing.lg,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: AppSpacing.fieldRadius,
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: colorScheme.outline,
|
||||||
|
width: AppSpacing.borderWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: AppSpacing.fieldRadius,
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: colorScheme.outline.withValues(alpha: 0.6),
|
||||||
|
width: AppSpacing.borderWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: AppSpacing.fieldRadius,
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
width: AppSpacing.borderWidthThick,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: AppSpacing.fieldRadius,
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: colorScheme.error,
|
||||||
|
width: AppSpacing.borderWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: AppSpacing.fieldRadius,
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: colorScheme.error,
|
||||||
|
width: AppSpacing.borderWidthThick,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: AppSpacing.fieldRadius,
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: colorScheme.onSurface.withValues(alpha: 0.12),
|
||||||
|
width: AppSpacing.borderWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
labelStyle: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: _isFocused
|
||||||
|
? colorScheme.primary
|
||||||
|
: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
hintStyle: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
errorStyle: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.error,
|
||||||
|
),
|
||||||
|
counterStyle: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Enhanced visual feedback with subtle animations
|
||||||
|
cursorColor: colorScheme.primary,
|
||||||
|
cursorHeight: 24,
|
||||||
|
cursorWidth: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
lib/features/auth/presentation/widgets/widgets.dart
Normal file
7
lib/features/auth/presentation/widgets/widgets.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// Auth widgets exports
|
||||||
|
//
|
||||||
|
// This file exports all auth-related widgets for easy importing
|
||||||
|
// throughout the auth feature module.
|
||||||
|
|
||||||
|
export 'auth_button.dart';
|
||||||
|
export 'auth_text_field.dart';
|
||||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import '../../../../core/routing/route_paths.dart';
|
import '../../../../core/routing/route_paths.dart';
|
||||||
import '../../../../core/routing/route_guards.dart';
|
import '../../../../core/routing/route_guards.dart';
|
||||||
import '../../../../shared/presentation/providers/app_providers.dart';
|
import '../../../../shared/presentation/providers/app_providers.dart';
|
||||||
|
import '../../../auth/presentation/providers/auth_providers.dart';
|
||||||
|
|
||||||
/// Main settings page with theme switcher and navigation to other settings
|
/// Main settings page with theme switcher and navigation to other settings
|
||||||
class SettingsPage extends ConsumerWidget {
|
class SettingsPage extends ConsumerWidget {
|
||||||
@@ -96,9 +97,22 @@ class _ThemeSection extends StatelessWidget {
|
|||||||
Icons.palette_outlined,
|
Icons.palette_outlined,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
title: const Text('Theme'),
|
title: Text(
|
||||||
subtitle: Text(_getThemeModeText(themeMode)),
|
'Theme',
|
||||||
trailing: const Icon(Icons.chevron_right),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
_getThemeModeText(themeMode),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () => context.push(RoutePaths.settingsTheme),
|
onTap: () => context.push(RoutePaths.settingsTheme),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -110,8 +124,18 @@ class _ThemeSection extends StatelessWidget {
|
|||||||
: Icons.brightness_auto,
|
: Icons.brightness_auto,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
title: const Text('Quick Theme Toggle'),
|
title: Text(
|
||||||
subtitle: const Text('Switch between light and dark mode'),
|
'Quick Theme Toggle',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Switch between light and dark mode',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
trailing: Switch(
|
trailing: Switch(
|
||||||
value: themeMode == ThemeMode.dark,
|
value: themeMode == ThemeMode.dark,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -152,32 +176,96 @@ class _AccountSection extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (authState == AuthState.authenticated) ...[
|
if (authState == AuthState.authenticated) ...[
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.person_outline),
|
leading: Icon(
|
||||||
title: const Text('Profile'),
|
Icons.person_outline,
|
||||||
subtitle: const Text('Manage your profile information'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Profile',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Manage your profile information',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () => context.push(RoutePaths.profile),
|
onTap: () => context.push(RoutePaths.profile),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.logout),
|
leading: Icon(
|
||||||
title: const Text('Sign Out'),
|
Icons.logout,
|
||||||
subtitle: const Text('Sign out of your account'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Sign Out',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Sign out of your account',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () => _showSignOutDialog(context, ref),
|
onTap: () => _showSignOutDialog(context, ref),
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.login),
|
leading: Icon(
|
||||||
title: const Text('Sign In'),
|
Icons.login,
|
||||||
subtitle: const Text('Sign in to your account'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Sign In',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Sign in to your account',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () => context.push(RoutePaths.login),
|
onTap: () => context.push(RoutePaths.login),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.person_add_outlined),
|
leading: Icon(
|
||||||
title: const Text('Create Account'),
|
Icons.person_add_outlined,
|
||||||
subtitle: const Text('Sign up for a new account'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Create Account',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Sign up for a new account',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () => context.push(RoutePaths.register),
|
onTap: () => context.push(RoutePaths.register),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -200,7 +288,7 @@ class _AccountSection extends StatelessWidget {
|
|||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ref.read(authStateProvider.notifier).logout();
|
ref.read(authNotifierProvider.notifier).logout();
|
||||||
},
|
},
|
||||||
child: const Text('Sign Out'),
|
child: const Text('Sign Out'),
|
||||||
),
|
),
|
||||||
@@ -217,17 +305,49 @@ class _AppSettingsSection extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.notifications_outlined),
|
leading: Icon(
|
||||||
title: const Text('Notifications'),
|
Icons.notifications_outlined,
|
||||||
subtitle: const Text('Manage notification preferences'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Notifications',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Manage notification preferences',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () => context.push(RoutePaths.settingsNotifications),
|
onTap: () => context.push(RoutePaths.settingsNotifications),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.language),
|
leading: Icon(
|
||||||
title: const Text('Language'),
|
Icons.language,
|
||||||
subtitle: const Text('English (United States)'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Language',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'English (United States)',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Language settings coming soon!')),
|
const SnackBar(content: Text('Language settings coming soon!')),
|
||||||
@@ -235,10 +355,26 @@ class _AppSettingsSection extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.storage_outlined),
|
leading: Icon(
|
||||||
title: const Text('Storage'),
|
Icons.storage_outlined,
|
||||||
subtitle: const Text('Manage local data and cache'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Storage',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Manage local data and cache',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Storage settings coming soon!')),
|
const SnackBar(content: Text('Storage settings coming soon!')),
|
||||||
@@ -256,17 +392,49 @@ class _PrivacySection extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.privacy_tip_outlined),
|
leading: Icon(
|
||||||
title: const Text('Privacy'),
|
Icons.privacy_tip_outlined,
|
||||||
subtitle: const Text('Privacy settings and data protection'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Privacy',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Privacy settings and data protection',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () => context.push(RoutePaths.settingsPrivacy),
|
onTap: () => context.push(RoutePaths.settingsPrivacy),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.security),
|
leading: Icon(
|
||||||
title: const Text('Security'),
|
Icons.security,
|
||||||
subtitle: const Text('App security and permissions'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Security',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'App security and permissions',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Security settings coming soon!')),
|
const SnackBar(content: Text('Security settings coming soon!')),
|
||||||
@@ -284,17 +452,49 @@ class _AboutSection extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.info_outlined),
|
leading: Icon(
|
||||||
title: const Text('About'),
|
Icons.info_outlined,
|
||||||
subtitle: const Text('App version and information'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'About',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'App version and information',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () => context.push(RoutePaths.about),
|
onTap: () => context.push(RoutePaths.about),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.help_outline),
|
leading: Icon(
|
||||||
title: const Text('Help & Support'),
|
Icons.help_outline,
|
||||||
subtitle: const Text('Get help and contact support'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Help & Support',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Get help and contact support',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Help & Support coming soon!')),
|
const SnackBar(content: Text('Help & Support coming soon!')),
|
||||||
@@ -302,10 +502,26 @@ class _AboutSection extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.article_outlined),
|
leading: Icon(
|
||||||
title: const Text('Terms of Service'),
|
Icons.article_outlined,
|
||||||
subtitle: const Text('View terms and conditions'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Terms of Service',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'View terms and conditions',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Terms of Service coming soon!')),
|
const SnackBar(content: Text('Terms of Service coming soon!')),
|
||||||
@@ -313,10 +529,26 @@ class _AboutSection extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.policy_outlined),
|
leading: Icon(
|
||||||
title: const Text('Privacy Policy'),
|
Icons.policy_outlined,
|
||||||
subtitle: const Text('View privacy policy'),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
trailing: const Icon(Icons.chevron_right),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Privacy Policy',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'View privacy policy',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Icons.chevron_right,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Privacy Policy coming soon!')),
|
const SnackBar(content: Text('Privacy Policy coming soon!')),
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import '../../../core/constants/storage_constants.dart';
|
import '../../../core/constants/storage_constants.dart';
|
||||||
|
import '../../../core/database/models/app_settings.dart';
|
||||||
import '../../../core/network/dio_client.dart';
|
import '../../../core/network/dio_client.dart';
|
||||||
import '../../../core/providers/network_providers.dart';
|
import '../../../core/providers/network_providers.dart';
|
||||||
|
import '../../../core/providers/storage_providers.dart' as storage;
|
||||||
|
|
||||||
/// Secure storage provider
|
/// Secure storage provider
|
||||||
final secureStorageProvider = Provider<FlutterSecureStorage>(
|
final secureStorageProvider = Provider<FlutterSecureStorage>(
|
||||||
@@ -29,19 +32,19 @@ final httpClientProvider = Provider<DioClient>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// App settings Hive box provider
|
/// App settings Hive box provider - uses safe provider from storage_providers.dart
|
||||||
final appSettingsBoxProvider = Provider<Box>(
|
final appSettingsBoxProvider = Provider<Box?>(
|
||||||
(ref) => Hive.box(StorageConstants.appSettingsBox),
|
(ref) => ref.watch(storage.appSettingsBoxProvider),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Cache Hive box provider
|
/// Cache Hive box provider - uses safe provider from storage_providers.dart
|
||||||
final cacheBoxProvider = Provider<Box>(
|
final cacheBoxProvider = Provider<Box?>(
|
||||||
(ref) => Hive.box(StorageConstants.cacheBox),
|
(ref) => ref.watch(storage.cacheBoxProvider),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// User data Hive box provider
|
/// User data Hive box provider - uses safe provider from storage_providers.dart
|
||||||
final userDataBoxProvider = Provider<Box>(
|
final userDataBoxProvider = Provider<Box?>(
|
||||||
(ref) => Hive.box(StorageConstants.userDataBox),
|
(ref) => ref.watch(storage.userPreferencesBoxProvider),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Theme mode provider
|
/// Theme mode provider
|
||||||
@@ -51,33 +54,58 @@ final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
|
|||||||
|
|
||||||
/// Theme mode notifier
|
/// Theme mode notifier
|
||||||
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
||||||
final Box _box;
|
final Box? _box;
|
||||||
|
static const String _settingsKey = 'app_settings';
|
||||||
|
|
||||||
ThemeModeNotifier(this._box) : super(ThemeMode.system) {
|
ThemeModeNotifier(this._box) : super(ThemeMode.system) {
|
||||||
_loadThemeMode();
|
_loadThemeMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _loadThemeMode() {
|
void _loadThemeMode() {
|
||||||
final isDarkMode = _box.get(StorageConstants.isDarkModeKey, defaultValue: null);
|
if (_box == null || !_box.isOpen) {
|
||||||
if (isDarkMode == null) {
|
// Default to system theme if box is not ready
|
||||||
|
state = ThemeMode.system;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get AppSettings from box
|
||||||
|
final settings = _box.get(_settingsKey) as AppSettings?;
|
||||||
|
if (settings != null) {
|
||||||
|
state = _themeModeFromString(settings.themeMode);
|
||||||
|
} else {
|
||||||
|
state = ThemeMode.system;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to system theme on any error
|
||||||
|
debugPrint('Error loading theme mode: $e');
|
||||||
state = ThemeMode.system;
|
state = ThemeMode.system;
|
||||||
} else {
|
|
||||||
state = isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setThemeMode(ThemeMode mode) async {
|
Future<void> setThemeMode(ThemeMode mode) async {
|
||||||
state = mode;
|
state = mode;
|
||||||
switch (mode) {
|
|
||||||
case ThemeMode.system:
|
// Only persist if box is available
|
||||||
await _box.delete(StorageConstants.isDarkModeKey);
|
if (_box == null || !_box.isOpen) {
|
||||||
break;
|
return;
|
||||||
case ThemeMode.light:
|
}
|
||||||
await _box.put(StorageConstants.isDarkModeKey, false);
|
|
||||||
break;
|
try {
|
||||||
case ThemeMode.dark:
|
// Get current settings or create default
|
||||||
await _box.put(StorageConstants.isDarkModeKey, true);
|
var settings = _box.get(_settingsKey) as AppSettings?;
|
||||||
break;
|
settings ??= AppSettings.defaultSettings();
|
||||||
|
|
||||||
|
// Update theme mode
|
||||||
|
final updatedSettings = settings.copyWith(
|
||||||
|
themeMode: _themeModeToString(mode),
|
||||||
|
lastUpdated: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save to box
|
||||||
|
await _box.put(_settingsKey, updatedSettings);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error saving theme mode: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,4 +120,26 @@ class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThemeMode _themeModeFromString(String mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case 'light':
|
||||||
|
return ThemeMode.light;
|
||||||
|
case 'dark':
|
||||||
|
return ThemeMode.dark;
|
||||||
|
default:
|
||||||
|
return ThemeMode.system;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _themeModeToString(ThemeMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case ThemeMode.light:
|
||||||
|
return 'light';
|
||||||
|
case ThemeMode.dark:
|
||||||
|
return 'dark';
|
||||||
|
case ThemeMode.system:
|
||||||
|
return 'system';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user