add auth, format
This commit is contained in:
122
lib/features/auth/data/datasources/auth_local_datasource.dart
Normal file
122
lib/features/auth/data/datasources/auth_local_datasource.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
/// Authentication Local Data Source
|
||||
///
|
||||
/// Handles secure local storage of authentication session data.
|
||||
/// Uses flutter_secure_storage for SID and CSRF token (encrypted).
|
||||
library;
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:worker/features/auth/data/models/auth_session_model.dart';
|
||||
|
||||
/// Authentication Local Data Source
|
||||
///
|
||||
/// Manages session data (SID, CSRF token) using secure storage.
|
||||
/// Session tokens are stored encrypted on device.
|
||||
class AuthLocalDataSource {
|
||||
final FlutterSecureStorage _secureStorage;
|
||||
|
||||
/// Secure storage keys
|
||||
static const String _sidKey = 'auth_session_sid';
|
||||
static const String _csrfTokenKey = 'auth_session_csrf_token';
|
||||
static const String _fullNameKey = 'auth_session_full_name';
|
||||
static const String _createdAtKey = 'auth_session_created_at';
|
||||
static const String _appsKey = 'auth_session_apps';
|
||||
|
||||
AuthLocalDataSource(this._secureStorage);
|
||||
|
||||
/// Save session data securely
|
||||
///
|
||||
/// Stores SID, CSRF token, and user info in encrypted storage.
|
||||
Future<void> saveSession(SessionData session) async {
|
||||
await _secureStorage.write(key: _sidKey, value: session.sid);
|
||||
await _secureStorage.write(key: _csrfTokenKey, value: session.csrfToken);
|
||||
await _secureStorage.write(key: _fullNameKey, value: session.fullName);
|
||||
await _secureStorage.write(
|
||||
key: _createdAtKey,
|
||||
value: session.createdAt.toIso8601String(),
|
||||
);
|
||||
|
||||
// Store apps as JSON string if available
|
||||
if (session.apps != null && session.apps!.isNotEmpty) {
|
||||
final appsJson = session.apps!.map((app) => app.toJson()).toList();
|
||||
// Convert to JSON string for storage
|
||||
await _secureStorage.write(key: _appsKey, value: appsJson.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stored session data
|
||||
///
|
||||
/// Returns null if no session is stored.
|
||||
Future<SessionData?> getSession() async {
|
||||
final sid = await _secureStorage.read(key: _sidKey);
|
||||
final csrfToken = await _secureStorage.read(key: _csrfTokenKey);
|
||||
final fullName = await _secureStorage.read(key: _fullNameKey);
|
||||
final createdAtStr = await _secureStorage.read(key: _createdAtKey);
|
||||
|
||||
if (sid == null || csrfToken == null || fullName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final createdAt = createdAtStr != null
|
||||
? DateTime.tryParse(createdAtStr) ?? DateTime.now()
|
||||
: DateTime.now();
|
||||
|
||||
// TODO: Parse apps from JSON string if needed
|
||||
// For now, apps are optional
|
||||
|
||||
return SessionData(
|
||||
sid: sid,
|
||||
csrfToken: csrfToken,
|
||||
fullName: fullName,
|
||||
createdAt: createdAt,
|
||||
apps: null, // TODO: Parse from stored JSON if needed
|
||||
);
|
||||
}
|
||||
|
||||
/// Get SID (Session ID)
|
||||
///
|
||||
/// Returns null if not logged in.
|
||||
Future<String?> getSid() async {
|
||||
return await _secureStorage.read(key: _sidKey);
|
||||
}
|
||||
|
||||
/// Get CSRF Token
|
||||
///
|
||||
/// Returns null if not logged in.
|
||||
Future<String?> getCsrfToken() async {
|
||||
return await _secureStorage.read(key: _csrfTokenKey);
|
||||
}
|
||||
|
||||
/// Get Full Name
|
||||
///
|
||||
/// Returns null if not logged in.
|
||||
Future<String?> getFullName() async {
|
||||
return await _secureStorage.read(key: _fullNameKey);
|
||||
}
|
||||
|
||||
/// Check if user has valid session
|
||||
///
|
||||
/// Returns true if SID and CSRF token are present.
|
||||
Future<bool> hasValidSession() async {
|
||||
final sid = await getSid();
|
||||
final csrfToken = await getCsrfToken();
|
||||
return sid != null && csrfToken != null;
|
||||
}
|
||||
|
||||
/// Clear session data
|
||||
///
|
||||
/// Called during logout to remove all session information.
|
||||
Future<void> clearSession() async {
|
||||
await _secureStorage.delete(key: _sidKey);
|
||||
await _secureStorage.delete(key: _csrfTokenKey);
|
||||
await _secureStorage.delete(key: _fullNameKey);
|
||||
await _secureStorage.delete(key: _createdAtKey);
|
||||
await _secureStorage.delete(key: _appsKey);
|
||||
}
|
||||
|
||||
/// Clear all authentication data
|
||||
///
|
||||
/// Complete cleanup of all stored auth data.
|
||||
Future<void> clearAll() async {
|
||||
await _secureStorage.deleteAll();
|
||||
}
|
||||
}
|
||||
86
lib/features/auth/data/models/auth_session_model.dart
Normal file
86
lib/features/auth/data/models/auth_session_model.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
/// Authentication Session Model
|
||||
///
|
||||
/// Models for API authentication response structure.
|
||||
/// Matches the ERPNext login API response format.
|
||||
library;
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'auth_session_model.freezed.dart';
|
||||
part 'auth_session_model.g.dart';
|
||||
|
||||
/// App Information
|
||||
///
|
||||
/// Represents an available app in the system.
|
||||
@freezed
|
||||
sealed class AppInfo with _$AppInfo {
|
||||
const factory AppInfo({
|
||||
@JsonKey(name: 'app_title') required String appTitle,
|
||||
@JsonKey(name: 'app_endpoint') required String appEndpoint,
|
||||
@JsonKey(name: 'app_logo') required String appLogo,
|
||||
}) = _AppInfo;
|
||||
|
||||
factory AppInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$AppInfoFromJson(json);
|
||||
}
|
||||
|
||||
/// Login Response Message
|
||||
///
|
||||
/// Contains the core authentication data from login response.
|
||||
@freezed
|
||||
sealed class LoginMessage with _$LoginMessage {
|
||||
const factory LoginMessage({
|
||||
required bool success,
|
||||
required String message,
|
||||
required String sid,
|
||||
@JsonKey(name: 'csrf_token') required String csrfToken,
|
||||
@Default([]) List<AppInfo> apps,
|
||||
}) = _LoginMessage;
|
||||
|
||||
factory LoginMessage.fromJson(Map<String, dynamic> json) =>
|
||||
_$LoginMessageFromJson(json);
|
||||
}
|
||||
|
||||
/// Authentication Session Response
|
||||
///
|
||||
/// Complete authentication response from ERPNext login API.
|
||||
@freezed
|
||||
sealed class AuthSessionResponse with _$AuthSessionResponse {
|
||||
const factory AuthSessionResponse({
|
||||
@JsonKey(name: 'session_expired') required int sessionExpired,
|
||||
required LoginMessage message,
|
||||
@JsonKey(name: 'home_page') required String homePage,
|
||||
@JsonKey(name: 'full_name') required String fullName,
|
||||
}) = _AuthSessionResponse;
|
||||
|
||||
factory AuthSessionResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$AuthSessionResponseFromJson(json);
|
||||
}
|
||||
|
||||
/// Session Storage Model
|
||||
///
|
||||
/// Simplified model for storing session data in Hive.
|
||||
@freezed
|
||||
sealed class SessionData with _$SessionData {
|
||||
const factory SessionData({
|
||||
required String sid,
|
||||
required String csrfToken,
|
||||
required String fullName,
|
||||
required DateTime createdAt,
|
||||
List<AppInfo>? apps,
|
||||
}) = _SessionData;
|
||||
|
||||
factory SessionData.fromJson(Map<String, dynamic> json) =>
|
||||
_$SessionDataFromJson(json);
|
||||
|
||||
/// Create from API response
|
||||
factory SessionData.fromAuthResponse(AuthSessionResponse response) {
|
||||
return SessionData(
|
||||
sid: response.message.sid,
|
||||
csrfToken: response.message.csrfToken,
|
||||
fullName: response.fullName,
|
||||
createdAt: DateTime.now(),
|
||||
apps: response.message.apps,
|
||||
);
|
||||
}
|
||||
}
|
||||
1113
lib/features/auth/data/models/auth_session_model.freezed.dart
Normal file
1113
lib/features/auth/data/models/auth_session_model.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
131
lib/features/auth/data/models/auth_session_model.g.dart
Normal file
131
lib/features/auth/data/models/auth_session_model.g.dart
Normal file
@@ -0,0 +1,131 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auth_session_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_AppInfo _$AppInfoFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'_AppInfo',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = _AppInfo(
|
||||
appTitle: $checkedConvert('app_title', (v) => v as String),
|
||||
appEndpoint: $checkedConvert('app_endpoint', (v) => v as String),
|
||||
appLogo: $checkedConvert('app_logo', (v) => v as String),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
fieldKeyMap: const {
|
||||
'appTitle': 'app_title',
|
||||
'appEndpoint': 'app_endpoint',
|
||||
'appLogo': 'app_logo',
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AppInfoToJson(_AppInfo instance) => <String, dynamic>{
|
||||
'app_title': instance.appTitle,
|
||||
'app_endpoint': instance.appEndpoint,
|
||||
'app_logo': instance.appLogo,
|
||||
};
|
||||
|
||||
_LoginMessage _$LoginMessageFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate('_LoginMessage', json, ($checkedConvert) {
|
||||
final val = _LoginMessage(
|
||||
success: $checkedConvert('success', (v) => v as bool),
|
||||
message: $checkedConvert('message', (v) => v as String),
|
||||
sid: $checkedConvert('sid', (v) => v as String),
|
||||
csrfToken: $checkedConvert('csrf_token', (v) => v as String),
|
||||
apps: $checkedConvert(
|
||||
'apps',
|
||||
(v) =>
|
||||
(v as List<dynamic>?)
|
||||
?.map((e) => AppInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
),
|
||||
);
|
||||
return val;
|
||||
}, fieldKeyMap: const {'csrfToken': 'csrf_token'});
|
||||
|
||||
Map<String, dynamic> _$LoginMessageToJson(_LoginMessage instance) =>
|
||||
<String, dynamic>{
|
||||
'success': instance.success,
|
||||
'message': instance.message,
|
||||
'sid': instance.sid,
|
||||
'csrf_token': instance.csrfToken,
|
||||
'apps': instance.apps.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
_AuthSessionResponse _$AuthSessionResponseFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'_AuthSessionResponse',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = _AuthSessionResponse(
|
||||
sessionExpired: $checkedConvert(
|
||||
'session_expired',
|
||||
(v) => (v as num).toInt(),
|
||||
),
|
||||
message: $checkedConvert(
|
||||
'message',
|
||||
(v) => LoginMessage.fromJson(v as Map<String, dynamic>),
|
||||
),
|
||||
homePage: $checkedConvert('home_page', (v) => v as String),
|
||||
fullName: $checkedConvert('full_name', (v) => v as String),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
fieldKeyMap: const {
|
||||
'sessionExpired': 'session_expired',
|
||||
'homePage': 'home_page',
|
||||
'fullName': 'full_name',
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AuthSessionResponseToJson(
|
||||
_AuthSessionResponse instance,
|
||||
) => <String, dynamic>{
|
||||
'session_expired': instance.sessionExpired,
|
||||
'message': instance.message.toJson(),
|
||||
'home_page': instance.homePage,
|
||||
'full_name': instance.fullName,
|
||||
};
|
||||
|
||||
_SessionData _$SessionDataFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'_SessionData',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = _SessionData(
|
||||
sid: $checkedConvert('sid', (v) => v as String),
|
||||
csrfToken: $checkedConvert('csrf_token', (v) => v as String),
|
||||
fullName: $checkedConvert('full_name', (v) => v as String),
|
||||
createdAt: $checkedConvert(
|
||||
'created_at',
|
||||
(v) => DateTime.parse(v as String),
|
||||
),
|
||||
apps: $checkedConvert(
|
||||
'apps',
|
||||
(v) => (v as List<dynamic>?)
|
||||
?.map((e) => AppInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
fieldKeyMap: const {
|
||||
'csrfToken': 'csrf_token',
|
||||
'fullName': 'full_name',
|
||||
'createdAt': 'created_at',
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SessionDataToJson(_SessionData instance) =>
|
||||
<String, dynamic>{
|
||||
'sid': instance.sid,
|
||||
'csrf_token': instance.csrfToken,
|
||||
'full_name': instance.fullName,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'apps': ?instance.apps?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
@@ -19,7 +19,7 @@ enum UserRole {
|
||||
accountant,
|
||||
|
||||
/// Designer
|
||||
designer;
|
||||
designer,
|
||||
}
|
||||
|
||||
/// User status enum
|
||||
@@ -34,7 +34,7 @@ enum UserStatus {
|
||||
suspended,
|
||||
|
||||
/// Rejected account
|
||||
rejected;
|
||||
rejected,
|
||||
}
|
||||
|
||||
/// Loyalty tier enum
|
||||
|
||||
492
lib/features/auth/presentation/pages/login_page.dart
Normal file
492
lib/features/auth/presentation/pages/login_page.dart
Normal file
@@ -0,0 +1,492 @@
|
||||
/// Login Page
|
||||
///
|
||||
/// Main authentication page for the Worker app.
|
||||
/// Allows users to login with phone number and password.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/core/utils/validators.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/password_visibility_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/widgets/phone_input_field.dart';
|
||||
|
||||
/// Login Page
|
||||
///
|
||||
/// Provides phone and password authentication.
|
||||
/// On successful login, navigates to home page.
|
||||
/// Links to registration page for new users.
|
||||
///
|
||||
/// Features:
|
||||
/// - Phone number input with Vietnamese format validation
|
||||
/// - Password input with visibility toggle
|
||||
/// - Form validation
|
||||
/// - Loading states
|
||||
/// - Error handling with snackbar
|
||||
/// - Link to registration
|
||||
/// - Customer support link
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
// Form key for validation
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Controllers
|
||||
final _phoneController = TextEditingController(text: "0988111111");
|
||||
final _passwordController = TextEditingController(text: "123456");
|
||||
|
||||
// Focus nodes
|
||||
final _phoneFocusNode = FocusNode();
|
||||
final _passwordFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneController.dispose();
|
||||
_passwordController.dispose();
|
||||
_phoneFocusNode.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Handle login button press
|
||||
Future<void> _handleLogin() async {
|
||||
// Validate form
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unfocus keyboard
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
// Call login method
|
||||
await ref
|
||||
.read(authProvider.notifier)
|
||||
.login(
|
||||
phoneNumber: _phoneController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
);
|
||||
|
||||
// Check if login was successful
|
||||
final authState = ref.read(authProvider);
|
||||
authState.when(
|
||||
data: (user) {
|
||||
if (user != null && mounted) {
|
||||
// Navigate to home on success
|
||||
context.goHome();
|
||||
}
|
||||
},
|
||||
loading: () {},
|
||||
error: (error, stack) {
|
||||
// Show error snackbar
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.toString()),
|
||||
backgroundColor: AppColors.danger,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Show error snackbar
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Đăng nhập thất bại: ${e.toString()}'),
|
||||
backgroundColor: AppColors.danger,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigate to register page
|
||||
void _navigateToRegister() {
|
||||
// TODO: Navigate to register page when route is set up
|
||||
// context.go('/register');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Chức năng đăng ký đang được phát triển'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show support dialog
|
||||
void _showSupport() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Hỗ trợ khách hàng'),
|
||||
content: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Hotline: 1900 xxxx'),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text('Email: support@eurotile.vn'),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text('Giờ làm việc: 8:00 - 17:00 (T2-T6)'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Đóng'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Watch auth state for loading indicator
|
||||
final authState = ref.watch(authProvider);
|
||||
final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Logo Section
|
||||
_buildLogo(),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Welcome Message
|
||||
_buildWelcomeMessage(),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Login Form Card
|
||||
_buildLoginForm(authState, isPasswordVisible),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Register Link
|
||||
_buildRegisterLink(),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Support Link
|
||||
_buildSupportLink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build logo section
|
||||
Widget _buildLogo() {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 20.0),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.primaryBlue, AppColors.lightBlue],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
Text(
|
||||
'EUROTILE',
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.0),
|
||||
Text(
|
||||
'Worker App',
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 12.0,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build welcome message
|
||||
Widget _buildWelcomeMessage() {
|
||||
return const Column(
|
||||
children: [
|
||||
Text(
|
||||
'Xin chào!',
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'Đăng nhập để tiếp tục',
|
||||
style: TextStyle(fontSize: 16.0, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Build login form card
|
||||
Widget _buildLoginForm(
|
||||
AsyncValue<dynamic> authState,
|
||||
bool isPasswordVisible,
|
||||
) {
|
||||
final isLoading = authState.isLoading;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10.0,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Phone Input
|
||||
PhoneInputField(
|
||||
controller: _phoneController,
|
||||
focusNode: _phoneFocusNode,
|
||||
validator: Validators.phone,
|
||||
enabled: !isLoading,
|
||||
onFieldSubmitted: (_) {
|
||||
// Move focus to password field
|
||||
FocusScope.of(context).requestFocus(_passwordFocusNode);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Password Input
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocusNode,
|
||||
enabled: !isLoading,
|
||||
obscureText: !isPasswordVisible,
|
||||
textInputAction: TextInputAction.done,
|
||||
style: const TextStyle(
|
||||
fontSize: InputFieldSpecs.fontSize,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Mật khẩu',
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.labelFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
hintText: 'Nhập mật khẩu',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.hintFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.lock,
|
||||
color: AppColors.primaryBlue,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
isPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
color: AppColors.grey500,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(passwordVisibilityProvider.notifier).toggle();
|
||||
},
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.danger,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.danger,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
errorStyle: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
Validators.passwordSimple(value, minLength: 6),
|
||||
onFieldSubmitted: (_) {
|
||||
if (!isLoading) {
|
||||
_handleLogin();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Login Button
|
||||
SizedBox(
|
||||
height: ButtonSpecs.height,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : _handleLogin,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
elevation: ButtonSpecs.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
AppColors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Đăng nhập',
|
||||
style: TextStyle(
|
||||
fontSize: ButtonSpecs.fontSize,
|
||||
fontWeight: ButtonSpecs.fontWeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build register link
|
||||
Widget _buildRegisterLink() {
|
||||
return Center(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: 'Chưa có tài khoản? ',
|
||||
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: GestureDetector(
|
||||
onTap: _navigateToRegister,
|
||||
child: const Text(
|
||||
'Đăng ký ngay',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build support link
|
||||
Widget _buildSupportLink() {
|
||||
return Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: _showSupport,
|
||||
icon: const Icon(
|
||||
Icons.headset_mic,
|
||||
size: AppIconSize.sm,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
label: const Text(
|
||||
'Hỗ trợ khách hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
790
lib/features/auth/presentation/pages/register_page.dart
Normal file
790
lib/features/auth/presentation/pages/register_page.dart
Normal file
@@ -0,0 +1,790 @@
|
||||
/// Registration Page
|
||||
///
|
||||
/// User registration form with role-based verification requirements.
|
||||
/// Matches design from html/register.html
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/core/utils/validators.dart';
|
||||
import 'package:worker/features/auth/presentation/widgets/phone_input_field.dart';
|
||||
import 'package:worker/features/auth/presentation/widgets/file_upload_card.dart';
|
||||
import 'package:worker/features/auth/presentation/widgets/role_dropdown.dart';
|
||||
|
||||
/// Registration Page
|
||||
///
|
||||
/// Features:
|
||||
/// - Full name, phone, email, password fields
|
||||
/// - Role selection (dealer/worker/broker/other)
|
||||
/// - Conditional verification section for workers/dealers
|
||||
/// - File upload for ID card and certificate
|
||||
/// - Company name and city selection
|
||||
/// - Terms and conditions checkbox
|
||||
///
|
||||
/// Navigation:
|
||||
/// - From: Login page
|
||||
/// - To: OTP verification (broker/other) or pending approval (worker/dealer)
|
||||
class RegisterPage extends ConsumerStatefulWidget {
|
||||
const RegisterPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<RegisterPage> createState() => _RegisterPageState();
|
||||
}
|
||||
|
||||
class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
// Form key
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Text controllers
|
||||
final _fullNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _idNumberController = TextEditingController();
|
||||
final _taxCodeController = TextEditingController();
|
||||
final _companyController = TextEditingController();
|
||||
|
||||
// Focus nodes
|
||||
final _fullNameFocus = FocusNode();
|
||||
final _phoneFocus = FocusNode();
|
||||
final _emailFocus = FocusNode();
|
||||
final _passwordFocus = FocusNode();
|
||||
final _idNumberFocus = FocusNode();
|
||||
final _taxCodeFocus = FocusNode();
|
||||
final _companyFocus = FocusNode();
|
||||
|
||||
// State
|
||||
String? _selectedRole;
|
||||
String? _selectedCity;
|
||||
File? _idCardFile;
|
||||
File? _certificateFile;
|
||||
bool _termsAccepted = false;
|
||||
bool _passwordVisible = false;
|
||||
bool _isLoading = false;
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fullNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_idNumberController.dispose();
|
||||
_taxCodeController.dispose();
|
||||
_companyController.dispose();
|
||||
_fullNameFocus.dispose();
|
||||
_phoneFocus.dispose();
|
||||
_emailFocus.dispose();
|
||||
_passwordFocus.dispose();
|
||||
_idNumberFocus.dispose();
|
||||
_taxCodeFocus.dispose();
|
||||
_companyFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Check if verification section should be shown
|
||||
bool get _shouldShowVerification {
|
||||
return _selectedRole == 'worker' || _selectedRole == 'dealer';
|
||||
}
|
||||
|
||||
/// Pick image from gallery or camera
|
||||
Future<void> _pickImage(bool isIdCard) async {
|
||||
try {
|
||||
// Show bottom sheet to select source
|
||||
final source = await showModalBottomSheet<ImageSource>(
|
||||
context: context,
|
||||
builder: (context) => SafeArea(
|
||||
child: Wrap(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Chụp ảnh'),
|
||||
onTap: () => Navigator.pop(context, ImageSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('Chọn từ thư viện'),
|
||||
onTap: () => Navigator.pop(context, ImageSource.gallery),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (source == null) return;
|
||||
|
||||
final pickedFile = await _imagePicker.pickImage(
|
||||
source: source,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
imageQuality: 85,
|
||||
);
|
||||
|
||||
if (pickedFile == null) return;
|
||||
|
||||
final file = File(pickedFile.path);
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
final fileSize = await file.length();
|
||||
if (fileSize > 5 * 1024 * 1024) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('File không được vượt quá 5MB'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
if (isIdCard) {
|
||||
_idCardFile = file;
|
||||
} else {
|
||||
_certificateFile = file;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi chọn ảnh: $e'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove selected image
|
||||
void _removeImage(bool isIdCard) {
|
||||
setState(() {
|
||||
if (isIdCard) {
|
||||
_idCardFile = null;
|
||||
} else {
|
||||
_certificateFile = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Validate form and submit
|
||||
Future<void> _handleRegister() async {
|
||||
// Validate form
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check terms acceptance
|
||||
if (!_termsAccepted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Vui lòng đồng ý với Điều khoản sử dụng và Chính sách bảo mật',
|
||||
),
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate verification requirements for workers/dealers
|
||||
if (_shouldShowVerification) {
|
||||
if (_idNumberController.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Vui lòng nhập số CCCD/CMND'),
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_idCardFile == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Vui lòng tải lên ảnh CCCD/CMND'),
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_certificateFile == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Vui lòng tải lên ảnh chứng chỉ hành nghề hoặc GPKD'),
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Implement actual registration API call
|
||||
// For now, simulate API delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
if (mounted) {
|
||||
// Navigate based on role
|
||||
if (_shouldShowVerification) {
|
||||
// For workers/dealers with verification, show pending page
|
||||
// TODO: Navigate to pending approval page
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Đăng ký thành công! Tài khoản đang chờ xét duyệt.',
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
context.pop();
|
||||
} else {
|
||||
// For other roles, navigate to OTP verification
|
||||
// TODO: Navigate to OTP verification page
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Đăng ký thành công! Vui lòng xác thực OTP.'),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
// context.push('/otp-verification');
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Đăng ký thất bại: $e'),
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.white,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: const Text(
|
||||
'Đăng ký tài khoản',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Welcome section
|
||||
const Text(
|
||||
'Tạo tài khoản mới',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
const Text(
|
||||
'Điền thông tin để bắt đầu',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Form card
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Full Name
|
||||
_buildLabel('Họ và tên *'),
|
||||
TextFormField(
|
||||
controller: _fullNameController,
|
||||
focusNode: _fullNameFocus,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Nhập họ và tên',
|
||||
prefixIcon: Icons.person,
|
||||
),
|
||||
validator: (value) => Validators.minLength(
|
||||
value,
|
||||
3,
|
||||
fieldName: 'Họ và tên',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Phone Number
|
||||
_buildLabel('Số điện thoại *'),
|
||||
PhoneInputField(
|
||||
controller: _phoneController,
|
||||
focusNode: _phoneFocus,
|
||||
validator: Validators.phone,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Email
|
||||
_buildLabel('Email *'),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
focusNode: _emailFocus,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Nhập email',
|
||||
prefixIcon: Icons.email,
|
||||
),
|
||||
validator: Validators.email,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Password
|
||||
_buildLabel('Mật khẩu *'),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocus,
|
||||
obscureText: !_passwordVisible,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Tạo mật khẩu mới',
|
||||
prefixIcon: Icons.lock,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
Validators.passwordSimple(value, minLength: 6),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
const Text(
|
||||
'Mật khẩu tối thiểu 6 ký tự',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Role Selection
|
||||
_buildLabel('Vai trò *'),
|
||||
RoleDropdown(
|
||||
value: _selectedRole,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedRole = value;
|
||||
// Clear verification fields when role changes
|
||||
if (!_shouldShowVerification) {
|
||||
_idNumberController.clear();
|
||||
_taxCodeController.clear();
|
||||
_idCardFile = null;
|
||||
_certificateFile = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Vui lòng chọn vai trò';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Verification Section (conditional)
|
||||
if (_shouldShowVerification) ...[
|
||||
_buildVerificationSection(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
],
|
||||
|
||||
// Company Name (optional)
|
||||
_buildLabel('Tên công ty/Cửa hàng'),
|
||||
TextFormField(
|
||||
controller: _companyController,
|
||||
focusNode: _companyFocus,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Nhập tên công ty (không bắt buộc)',
|
||||
prefixIcon: Icons.business,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// City/Province
|
||||
_buildLabel('Tỉnh/Thành phố *'),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedCity,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Chọn tỉnh/thành phố',
|
||||
prefixIcon: Icons.location_city,
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'hanoi',
|
||||
child: Text('Hà Nội'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'hcm',
|
||||
child: Text('TP. Hồ Chí Minh'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'danang',
|
||||
child: Text('Đà Nẵng'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'haiphong',
|
||||
child: Text('Hải Phòng'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'cantho',
|
||||
child: Text('Cần Thơ'),
|
||||
),
|
||||
DropdownMenuItem(value: 'other', child: Text('Khác')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedCity = value;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Vui lòng chọn tỉnh/thành phố';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Terms and Conditions
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _termsAccepted,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_termsAccepted = value ?? false;
|
||||
});
|
||||
},
|
||||
activeColor: AppColors.primaryBlue,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_termsAccepted = !_termsAccepted;
|
||||
});
|
||||
},
|
||||
child: const Text.rich(
|
||||
TextSpan(
|
||||
text: 'Tôi đồng ý với ',
|
||||
style: TextStyle(fontSize: 13),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Điều khoản sử dụng',
|
||||
style: TextStyle(
|
||||
color: AppColors.primaryBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
TextSpan(text: ' và '),
|
||||
TextSpan(
|
||||
text: 'Chính sách bảo mật',
|
||||
style: TextStyle(
|
||||
color: AppColors.primaryBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Register Button
|
||||
SizedBox(
|
||||
height: ButtonSpecs.height,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleRegister,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
ButtonSpecs.borderRadius,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
AppColors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Đăng ký',
|
||||
style: TextStyle(
|
||||
fontSize: ButtonSpecs.fontSize,
|
||||
fontWeight: ButtonSpecs.fontWeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Login Link
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Đã có tài khoản? ',
|
||||
style: TextStyle(fontSize: 13, color: AppColors.grey500),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => context.pop(),
|
||||
child: const Text(
|
||||
'Đăng nhập',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.primaryBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build label widget
|
||||
Widget _buildLabel(String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build input decoration
|
||||
InputDecoration _buildInputDecoration({
|
||||
required String hintText,
|
||||
required IconData prefixIcon,
|
||||
Widget? suffixIcon,
|
||||
}) {
|
||||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.hintFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
prefixIcon,
|
||||
color: AppColors.primaryBlue,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
suffixIcon: suffixIcon,
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2.0),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 1.0),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 2.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build verification section
|
||||
Widget _buildVerificationSection() {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0), width: 2),
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
),
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.shield, color: AppColors.primaryBlue, size: 20),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
const Text(
|
||||
'Thông tin xác thực',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
const Text(
|
||||
'Thông tin này sẽ được dùng để xác minh tư cách chuyên môn của bạn',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.grey500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// ID Number
|
||||
_buildLabel('Số CCCD/CMND'),
|
||||
TextFormField(
|
||||
controller: _idNumberController,
|
||||
focusNode: _idNumberFocus,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Nhập số CCCD/CMND',
|
||||
prefixIcon: Icons.badge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Tax Code
|
||||
_buildLabel('Mã số thuế cá nhân/Công ty'),
|
||||
TextFormField(
|
||||
controller: _taxCodeController,
|
||||
focusNode: _taxCodeFocus,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Nhập mã số thuế (không bắt buộc)',
|
||||
prefixIcon: Icons.receipt_long,
|
||||
),
|
||||
validator: Validators.taxIdOptional,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// ID Card Upload
|
||||
_buildLabel('Ảnh mặt trước CCCD/CMND'),
|
||||
FileUploadCard(
|
||||
file: _idCardFile,
|
||||
onTap: () => _pickImage(true),
|
||||
onRemove: () => _removeImage(true),
|
||||
icon: Icons.camera_alt,
|
||||
title: 'Chụp ảnh hoặc chọn file',
|
||||
subtitle: 'JPG, PNG tối đa 5MB',
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Certificate Upload
|
||||
_buildLabel('Ảnh chứng chỉ hành nghề hoặc GPKD'),
|
||||
FileUploadCard(
|
||||
file: _certificateFile,
|
||||
onTap: () => _pickImage(false),
|
||||
onRemove: () => _removeImage(false),
|
||||
icon: Icons.file_present,
|
||||
title: 'Chụp ảnh hoặc chọn file',
|
||||
subtitle: 'JPG, PNG tối đa 5MB',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
279
lib/features/auth/presentation/providers/auth_provider.dart
Normal file
279
lib/features/auth/presentation/providers/auth_provider.dart
Normal file
@@ -0,0 +1,279 @@
|
||||
/// Authentication State Provider
|
||||
///
|
||||
/// Manages authentication state for the Worker application.
|
||||
/// Handles login, logout, and user session management.
|
||||
///
|
||||
/// Uses Riverpod 3.0 with code generation for type-safe state management.
|
||||
library;
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart';
|
||||
import 'package:worker/features/auth/data/models/auth_session_model.dart';
|
||||
import 'package:worker/features/auth/domain/entities/user.dart';
|
||||
|
||||
part 'auth_provider.g.dart';
|
||||
|
||||
/// Provide FlutterSecureStorage instance
|
||||
@riverpod
|
||||
FlutterSecureStorage secureStorage(Ref ref) {
|
||||
return const FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||
);
|
||||
}
|
||||
|
||||
/// Provide AuthLocalDataSource instance
|
||||
@riverpod
|
||||
AuthLocalDataSource authLocalDataSource(Ref ref) {
|
||||
final secureStorage = ref.watch(secureStorageProvider);
|
||||
return AuthLocalDataSource(secureStorage);
|
||||
}
|
||||
|
||||
/// Authentication state result
|
||||
///
|
||||
/// Represents the result of authentication operations.
|
||||
/// Contains either the authenticated user or null if logged out.
|
||||
typedef AuthState = AsyncValue<User?>;
|
||||
|
||||
/// Authentication Provider
|
||||
///
|
||||
/// Main provider for authentication state management.
|
||||
/// Provides login and logout functionality with async state handling.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final authState = ref.watch(authProvider);
|
||||
/// authState.when(
|
||||
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
@riverpod
|
||||
class Auth extends _$Auth {
|
||||
/// Get auth local data source
|
||||
AuthLocalDataSource get _localDataSource =>
|
||||
ref.read(authLocalDataSourceProvider);
|
||||
|
||||
/// Initialize with saved session if available
|
||||
@override
|
||||
Future<User?> build() async {
|
||||
// Check for saved session in secure storage
|
||||
final session = await _localDataSource.getSession();
|
||||
if (session != null) {
|
||||
// User has saved session, create User entity
|
||||
final now = DateTime.now();
|
||||
return User(
|
||||
userId: 'user_saved', // TODO: Get from API
|
||||
phoneNumber: '', // TODO: Get from saved user data
|
||||
fullName: session.fullName,
|
||||
email: '', // TODO: Get from saved user data
|
||||
role: UserRole.customer,
|
||||
status: UserStatus.active,
|
||||
loyaltyTier: LoyaltyTier.gold,
|
||||
totalPoints: 0,
|
||||
companyInfo: null,
|
||||
cccd: null,
|
||||
attachments: [],
|
||||
address: null,
|
||||
avatarUrl: null,
|
||||
referralCode: null,
|
||||
referredBy: null,
|
||||
erpnextCustomerId: null,
|
||||
createdAt: session.createdAt,
|
||||
updatedAt: now,
|
||||
lastLoginAt: now,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Login with phone number and password
|
||||
///
|
||||
/// Simulates ERPNext API authentication with mock response.
|
||||
/// Stores session data (SID, CSRF token) in Hive.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - [phoneNumber]: User's phone number (Vietnamese format)
|
||||
/// - [password]: User's password
|
||||
///
|
||||
/// Returns: Authenticated User object on success
|
||||
///
|
||||
/// Throws: Exception on authentication failure
|
||||
Future<void> login({
|
||||
required String phoneNumber,
|
||||
required String password,
|
||||
}) async {
|
||||
// Set loading state
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
// Simulate API call delay
|
||||
state = await AsyncValue.guard(() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
|
||||
// Mock validation
|
||||
if (phoneNumber.isEmpty || password.isEmpty) {
|
||||
throw Exception('Số điện thoại và mật khẩu không được để trống');
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
throw Exception('Mật khẩu phải có ít nhất 6 ký tự');
|
||||
}
|
||||
|
||||
// Simulate API response matching ERPNext format
|
||||
final mockApiResponse = AuthSessionResponse(
|
||||
sessionExpired: 1,
|
||||
message: const LoginMessage(
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
sid: 'df7fd4e7ef1041aa3422b0ee861315ba8c28d4fe008a7d7e0e7e0e01',
|
||||
csrfToken: '6b6e37563854e951c36a7af4177956bb15ca469ca4f498b742648d70',
|
||||
apps: [
|
||||
AppInfo(
|
||||
appTitle: 'App nhân viên kinh doanh',
|
||||
appEndpoint: '/ecommerce/app-sales',
|
||||
appLogo:
|
||||
'https://assets.digitalbiz.com.vn/DBIZ_Internal/Logo/logo_app_sales.png',
|
||||
),
|
||||
],
|
||||
),
|
||||
homePage: '/apps',
|
||||
fullName: 'Tân Duy Nguyễn',
|
||||
);
|
||||
|
||||
// Save session data to Hive
|
||||
final sessionData = SessionData.fromAuthResponse(mockApiResponse);
|
||||
await _localDataSource.saveSession(sessionData);
|
||||
|
||||
// Create and return User entity
|
||||
final now = DateTime.now();
|
||||
return User(
|
||||
userId: 'user_${phoneNumber.replaceAll('+84', '')}',
|
||||
phoneNumber: phoneNumber,
|
||||
fullName: mockApiResponse.fullName,
|
||||
email: 'user@eurotile.vn',
|
||||
role: UserRole.customer,
|
||||
status: UserStatus.active,
|
||||
loyaltyTier: LoyaltyTier.gold,
|
||||
totalPoints: 1500,
|
||||
companyInfo: const CompanyInfo(
|
||||
name: 'Công ty TNHH XYZ',
|
||||
taxId: '0123456789',
|
||||
businessType: 'Xây dựng',
|
||||
),
|
||||
cccd: '001234567890',
|
||||
attachments: [],
|
||||
address: '123 Đường ABC, Quận 1, TP.HCM',
|
||||
avatarUrl: null,
|
||||
referralCode: 'REF${phoneNumber.replaceAll('+84', '').substring(0, 6)}',
|
||||
referredBy: null,
|
||||
erpnextCustomerId: null,
|
||||
createdAt: now.subtract(const Duration(days: 30)),
|
||||
updatedAt: now,
|
||||
lastLoginAt: now,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Logout current user
|
||||
///
|
||||
/// Clears authentication state and removes saved session from Hive.
|
||||
Future<void> logout() async {
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Clear saved session from Hive
|
||||
await _localDataSource.clearSession();
|
||||
|
||||
// TODO: Call logout API to invalidate token on server
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Return null to indicate logged out
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/// Get current authenticated user
|
||||
///
|
||||
/// Returns the current user if logged in, null otherwise.
|
||||
User? get currentUser => state.value;
|
||||
|
||||
/// Check if user is authenticated
|
||||
///
|
||||
/// Returns true if there is a logged-in user.
|
||||
bool get isAuthenticated => currentUser != null;
|
||||
|
||||
/// Check if authentication is in progress
|
||||
///
|
||||
/// Returns true during login/logout operations.
|
||||
bool get isLoading => state.isLoading;
|
||||
|
||||
/// Get authentication error if any
|
||||
///
|
||||
/// Returns error message or null if no error.
|
||||
Object? get error => state.error;
|
||||
}
|
||||
|
||||
/// Convenience provider for checking if user is authenticated
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final isLoggedIn = ref.watch(isAuthenticatedProvider);
|
||||
/// if (isLoggedIn) {
|
||||
/// // Show home screen
|
||||
/// }
|
||||
/// ```
|
||||
@riverpod
|
||||
bool isAuthenticated(Ref ref) {
|
||||
final authState = ref.watch(authProvider);
|
||||
return authState.value != null;
|
||||
}
|
||||
|
||||
/// Convenience provider for getting current user
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final user = ref.watch(currentUserProvider);
|
||||
/// if (user != null) {
|
||||
/// Text('Welcome ${user.fullName}');
|
||||
/// }
|
||||
/// ```
|
||||
@riverpod
|
||||
User? currentUser(Ref ref) {
|
||||
final authState = ref.watch(authProvider);
|
||||
return authState.value;
|
||||
}
|
||||
|
||||
/// Convenience provider for user's loyalty tier
|
||||
///
|
||||
/// Returns the current user's loyalty tier or null if not logged in.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final tier = ref.watch(userLoyaltyTierProvider);
|
||||
/// if (tier != null) {
|
||||
/// Text('Tier: ${tier.displayName}');
|
||||
/// }
|
||||
/// ```
|
||||
@riverpod
|
||||
LoyaltyTier? userLoyaltyTier(Ref ref) {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
return user?.loyaltyTier;
|
||||
}
|
||||
|
||||
/// Convenience provider for user's total points
|
||||
///
|
||||
/// Returns the current user's total loyalty points or 0 if not logged in.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final points = ref.watch(userTotalPointsProvider);
|
||||
/// Text('Points: $points');
|
||||
/// ```
|
||||
@riverpod
|
||||
int userTotalPoints(Ref ref) {
|
||||
final user = ref.watch(currentUserProvider);
|
||||
return user?.totalPoints ?? 0;
|
||||
}
|
||||
500
lib/features/auth/presentation/providers/auth_provider.g.dart
Normal file
500
lib/features/auth/presentation/providers/auth_provider.g.dart
Normal file
@@ -0,0 +1,500 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auth_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provide FlutterSecureStorage instance
|
||||
|
||||
@ProviderFor(secureStorage)
|
||||
const secureStorageProvider = SecureStorageProvider._();
|
||||
|
||||
/// Provide FlutterSecureStorage instance
|
||||
|
||||
final class SecureStorageProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
FlutterSecureStorage,
|
||||
FlutterSecureStorage,
|
||||
FlutterSecureStorage
|
||||
>
|
||||
with $Provider<FlutterSecureStorage> {
|
||||
/// Provide FlutterSecureStorage instance
|
||||
const SecureStorageProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'secureStorageProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$secureStorageHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<FlutterSecureStorage> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FlutterSecureStorage create(Ref ref) {
|
||||
return secureStorage(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(FlutterSecureStorage value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<FlutterSecureStorage>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$secureStorageHash() => r'c3d90388f6d1bb7c95a29ceeda2e56c57deb1ecb';
|
||||
|
||||
/// Provide AuthLocalDataSource instance
|
||||
|
||||
@ProviderFor(authLocalDataSource)
|
||||
const authLocalDataSourceProvider = AuthLocalDataSourceProvider._();
|
||||
|
||||
/// Provide AuthLocalDataSource instance
|
||||
|
||||
final class AuthLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AuthLocalDataSource,
|
||||
AuthLocalDataSource,
|
||||
AuthLocalDataSource
|
||||
>
|
||||
with $Provider<AuthLocalDataSource> {
|
||||
/// Provide AuthLocalDataSource instance
|
||||
const AuthLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'authLocalDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$authLocalDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<AuthLocalDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
AuthLocalDataSource create(Ref ref) {
|
||||
return authLocalDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AuthLocalDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AuthLocalDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$authLocalDataSourceHash() =>
|
||||
r'f104de00a8ab431f6736387fb499c2b6e0ab4924';
|
||||
|
||||
/// Authentication Provider
|
||||
///
|
||||
/// Main provider for authentication state management.
|
||||
/// Provides login and logout functionality with async state handling.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final authState = ref.watch(authProvider);
|
||||
/// authState.when(
|
||||
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
@ProviderFor(Auth)
|
||||
const authProvider = AuthProvider._();
|
||||
|
||||
/// Authentication Provider
|
||||
///
|
||||
/// Main provider for authentication state management.
|
||||
/// Provides login and logout functionality with async state handling.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final authState = ref.watch(authProvider);
|
||||
/// authState.when(
|
||||
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
final class AuthProvider extends $AsyncNotifierProvider<Auth, User?> {
|
||||
/// Authentication Provider
|
||||
///
|
||||
/// Main provider for authentication state management.
|
||||
/// Provides login and logout functionality with async state handling.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final authState = ref.watch(authProvider);
|
||||
/// authState.when(
|
||||
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
const AuthProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'authProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$authHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Auth create() => Auth();
|
||||
}
|
||||
|
||||
String _$authHash() => r'6f410d1abe6c53a6cbfa52fde7ea7a2d22a7f78d';
|
||||
|
||||
/// Authentication Provider
|
||||
///
|
||||
/// Main provider for authentication state management.
|
||||
/// Provides login and logout functionality with async state handling.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final authState = ref.watch(authProvider);
|
||||
/// authState.when(
|
||||
/// data: (user) => user != null ? HomeScreen() : LoginScreen(),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
abstract class _$Auth extends $AsyncNotifier<User?> {
|
||||
FutureOr<User?> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<User?>, User?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<User?>, User?>,
|
||||
AsyncValue<User?>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience provider for checking if user is authenticated
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final isLoggedIn = ref.watch(isAuthenticatedProvider);
|
||||
/// if (isLoggedIn) {
|
||||
/// // Show home screen
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@ProviderFor(isAuthenticated)
|
||||
const isAuthenticatedProvider = IsAuthenticatedProvider._();
|
||||
|
||||
/// Convenience provider for checking if user is authenticated
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final isLoggedIn = ref.watch(isAuthenticatedProvider);
|
||||
/// if (isLoggedIn) {
|
||||
/// // Show home screen
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
final class IsAuthenticatedProvider
|
||||
extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Convenience provider for checking if user is authenticated
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final isLoggedIn = ref.watch(isAuthenticatedProvider);
|
||||
/// if (isLoggedIn) {
|
||||
/// // Show home screen
|
||||
/// }
|
||||
/// ```
|
||||
const IsAuthenticatedProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'isAuthenticatedProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$isAuthenticatedHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return isAuthenticated(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$isAuthenticatedHash() => r'dc783f052ad2ddb7fa18c58e5dc6d212e6c32a96';
|
||||
|
||||
/// Convenience provider for getting current user
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final user = ref.watch(currentUserProvider);
|
||||
/// if (user != null) {
|
||||
/// Text('Welcome ${user.fullName}');
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@ProviderFor(currentUser)
|
||||
const currentUserProvider = CurrentUserProvider._();
|
||||
|
||||
/// Convenience provider for getting current user
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final user = ref.watch(currentUserProvider);
|
||||
/// if (user != null) {
|
||||
/// Text('Welcome ${user.fullName}');
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
final class CurrentUserProvider extends $FunctionalProvider<User?, User?, User?>
|
||||
with $Provider<User?> {
|
||||
/// Convenience provider for getting current user
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final user = ref.watch(currentUserProvider);
|
||||
/// if (user != null) {
|
||||
/// Text('Welcome ${user.fullName}');
|
||||
/// }
|
||||
/// ```
|
||||
const CurrentUserProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'currentUserProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$currentUserHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<User?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
User? create(Ref ref) {
|
||||
return currentUser(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(User? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<User?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$currentUserHash() => r'f3c1da551f4a4c2bf158782ea37a4749a718128a';
|
||||
|
||||
/// Convenience provider for user's loyalty tier
|
||||
///
|
||||
/// Returns the current user's loyalty tier or null if not logged in.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final tier = ref.watch(userLoyaltyTierProvider);
|
||||
/// if (tier != null) {
|
||||
/// Text('Tier: ${tier.displayName}');
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@ProviderFor(userLoyaltyTier)
|
||||
const userLoyaltyTierProvider = UserLoyaltyTierProvider._();
|
||||
|
||||
/// Convenience provider for user's loyalty tier
|
||||
///
|
||||
/// Returns the current user's loyalty tier or null if not logged in.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final tier = ref.watch(userLoyaltyTierProvider);
|
||||
/// if (tier != null) {
|
||||
/// Text('Tier: ${tier.displayName}');
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
final class UserLoyaltyTierProvider
|
||||
extends $FunctionalProvider<LoyaltyTier?, LoyaltyTier?, LoyaltyTier?>
|
||||
with $Provider<LoyaltyTier?> {
|
||||
/// Convenience provider for user's loyalty tier
|
||||
///
|
||||
/// Returns the current user's loyalty tier or null if not logged in.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final tier = ref.watch(userLoyaltyTierProvider);
|
||||
/// if (tier != null) {
|
||||
/// Text('Tier: ${tier.displayName}');
|
||||
/// }
|
||||
/// ```
|
||||
const UserLoyaltyTierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userLoyaltyTierProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userLoyaltyTierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<LoyaltyTier?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
LoyaltyTier? create(Ref ref) {
|
||||
return userLoyaltyTier(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(LoyaltyTier? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<LoyaltyTier?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userLoyaltyTierHash() => r'f1a157486b8bdd2cf64bc2201207f2ac71ea6a69';
|
||||
|
||||
/// Convenience provider for user's total points
|
||||
///
|
||||
/// Returns the current user's total loyalty points or 0 if not logged in.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final points = ref.watch(userTotalPointsProvider);
|
||||
/// Text('Points: $points');
|
||||
/// ```
|
||||
|
||||
@ProviderFor(userTotalPoints)
|
||||
const userTotalPointsProvider = UserTotalPointsProvider._();
|
||||
|
||||
/// Convenience provider for user's total points
|
||||
///
|
||||
/// Returns the current user's total loyalty points or 0 if not logged in.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final points = ref.watch(userTotalPointsProvider);
|
||||
/// Text('Points: $points');
|
||||
/// ```
|
||||
|
||||
final class UserTotalPointsProvider extends $FunctionalProvider<int, int, int>
|
||||
with $Provider<int> {
|
||||
/// Convenience provider for user's total points
|
||||
///
|
||||
/// Returns the current user's total loyalty points or 0 if not logged in.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final points = ref.watch(userTotalPointsProvider);
|
||||
/// Text('Points: $points');
|
||||
/// ```
|
||||
const UserTotalPointsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userTotalPointsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userTotalPointsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
int create(Ref ref) {
|
||||
return userTotalPoints(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userTotalPointsHash() => r'9ccebb48a8641c3c0624b1649303b436e82602bd';
|
||||
@@ -0,0 +1,112 @@
|
||||
/// Password Visibility Provider
|
||||
///
|
||||
/// Simple state provider for toggling password visibility in login/register forms.
|
||||
///
|
||||
/// Uses Riverpod 3.0 with code generation for type-safe state management.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'password_visibility_provider.g.dart';
|
||||
|
||||
/// Password Visibility State Provider
|
||||
///
|
||||
/// Manages the visibility state of password input fields.
|
||||
/// Default state is false (password hidden).
|
||||
///
|
||||
/// Usage in login/register pages:
|
||||
/// ```dart
|
||||
/// class LoginPage extends ConsumerWidget {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context, WidgetRef ref) {
|
||||
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||
///
|
||||
/// return TextField(
|
||||
/// obscureText: !isPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(passwordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@riverpod
|
||||
class PasswordVisibility extends _$PasswordVisibility {
|
||||
/// Initialize with password hidden (false)
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
/// Toggle password visibility
|
||||
///
|
||||
/// Switches between showing and hiding the password.
|
||||
void toggle() {
|
||||
state = !state;
|
||||
}
|
||||
|
||||
/// Show password
|
||||
///
|
||||
/// Sets visibility to true (password visible).
|
||||
void show() {
|
||||
state = true;
|
||||
}
|
||||
|
||||
/// Hide password
|
||||
///
|
||||
/// Sets visibility to false (password hidden).
|
||||
void hide() {
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirm Password Visibility State Provider
|
||||
///
|
||||
/// Separate provider for confirm password field in registration forms.
|
||||
/// This allows independent control of password and confirm password visibility.
|
||||
///
|
||||
/// Usage in registration page:
|
||||
/// ```dart
|
||||
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
|
||||
///
|
||||
/// TextField(
|
||||
/// obscureText: !isConfirmPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// labelText: 'Xác nhận mật khẩu',
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
@riverpod
|
||||
class ConfirmPasswordVisibility extends _$ConfirmPasswordVisibility {
|
||||
/// Initialize with password hidden (false)
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
/// Toggle confirm password visibility
|
||||
void toggle() {
|
||||
state = !state;
|
||||
}
|
||||
|
||||
/// Show confirm password
|
||||
void show() {
|
||||
state = true;
|
||||
}
|
||||
|
||||
/// Hide confirm password
|
||||
void hide() {
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'password_visibility_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Password Visibility State Provider
|
||||
///
|
||||
/// Manages the visibility state of password input fields.
|
||||
/// Default state is false (password hidden).
|
||||
///
|
||||
/// Usage in login/register pages:
|
||||
/// ```dart
|
||||
/// class LoginPage extends ConsumerWidget {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context, WidgetRef ref) {
|
||||
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||
///
|
||||
/// return TextField(
|
||||
/// obscureText: !isPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(passwordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@ProviderFor(PasswordVisibility)
|
||||
const passwordVisibilityProvider = PasswordVisibilityProvider._();
|
||||
|
||||
/// Password Visibility State Provider
|
||||
///
|
||||
/// Manages the visibility state of password input fields.
|
||||
/// Default state is false (password hidden).
|
||||
///
|
||||
/// Usage in login/register pages:
|
||||
/// ```dart
|
||||
/// class LoginPage extends ConsumerWidget {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context, WidgetRef ref) {
|
||||
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||
///
|
||||
/// return TextField(
|
||||
/// obscureText: !isPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(passwordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
final class PasswordVisibilityProvider
|
||||
extends $NotifierProvider<PasswordVisibility, bool> {
|
||||
/// Password Visibility State Provider
|
||||
///
|
||||
/// Manages the visibility state of password input fields.
|
||||
/// Default state is false (password hidden).
|
||||
///
|
||||
/// Usage in login/register pages:
|
||||
/// ```dart
|
||||
/// class LoginPage extends ConsumerWidget {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context, WidgetRef ref) {
|
||||
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||
///
|
||||
/// return TextField(
|
||||
/// obscureText: !isPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(passwordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
const PasswordVisibilityProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'passwordVisibilityProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$passwordVisibilityHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PasswordVisibility create() => PasswordVisibility();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$passwordVisibilityHash() =>
|
||||
r'25b6fa914e42dd83c8443aecbeb1d608cccd00ab';
|
||||
|
||||
/// Password Visibility State Provider
|
||||
///
|
||||
/// Manages the visibility state of password input fields.
|
||||
/// Default state is false (password hidden).
|
||||
///
|
||||
/// Usage in login/register pages:
|
||||
/// ```dart
|
||||
/// class LoginPage extends ConsumerWidget {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context, WidgetRef ref) {
|
||||
/// final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||
///
|
||||
/// return TextField(
|
||||
/// obscureText: !isPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(passwordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
abstract class _$PasswordVisibility extends $Notifier<bool> {
|
||||
bool build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<bool, bool>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<bool, bool>,
|
||||
bool,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirm Password Visibility State Provider
|
||||
///
|
||||
/// Separate provider for confirm password field in registration forms.
|
||||
/// This allows independent control of password and confirm password visibility.
|
||||
///
|
||||
/// Usage in registration page:
|
||||
/// ```dart
|
||||
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
|
||||
///
|
||||
/// TextField(
|
||||
/// obscureText: !isConfirmPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// labelText: 'Xác nhận mật khẩu',
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
@ProviderFor(ConfirmPasswordVisibility)
|
||||
const confirmPasswordVisibilityProvider = ConfirmPasswordVisibilityProvider._();
|
||||
|
||||
/// Confirm Password Visibility State Provider
|
||||
///
|
||||
/// Separate provider for confirm password field in registration forms.
|
||||
/// This allows independent control of password and confirm password visibility.
|
||||
///
|
||||
/// Usage in registration page:
|
||||
/// ```dart
|
||||
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
|
||||
///
|
||||
/// TextField(
|
||||
/// obscureText: !isConfirmPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// labelText: 'Xác nhận mật khẩu',
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
final class ConfirmPasswordVisibilityProvider
|
||||
extends $NotifierProvider<ConfirmPasswordVisibility, bool> {
|
||||
/// Confirm Password Visibility State Provider
|
||||
///
|
||||
/// Separate provider for confirm password field in registration forms.
|
||||
/// This allows independent control of password and confirm password visibility.
|
||||
///
|
||||
/// Usage in registration page:
|
||||
/// ```dart
|
||||
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
|
||||
///
|
||||
/// TextField(
|
||||
/// obscureText: !isConfirmPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// labelText: 'Xác nhận mật khẩu',
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
const ConfirmPasswordVisibilityProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'confirmPasswordVisibilityProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$confirmPasswordVisibilityHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
ConfirmPasswordVisibility create() => ConfirmPasswordVisibility();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$confirmPasswordVisibilityHash() =>
|
||||
r'8408bba9db1e8deba425f98015a4e2fa76d75eb8';
|
||||
|
||||
/// Confirm Password Visibility State Provider
|
||||
///
|
||||
/// Separate provider for confirm password field in registration forms.
|
||||
/// This allows independent control of password and confirm password visibility.
|
||||
///
|
||||
/// Usage in registration page:
|
||||
/// ```dart
|
||||
/// final isConfirmPasswordVisible = ref.watch(confirmPasswordVisibilityProvider);
|
||||
///
|
||||
/// TextField(
|
||||
/// obscureText: !isConfirmPasswordVisible,
|
||||
/// decoration: InputDecoration(
|
||||
/// labelText: 'Xác nhận mật khẩu',
|
||||
/// suffixIcon: IconButton(
|
||||
/// icon: Icon(
|
||||
/// isConfirmPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
/// ),
|
||||
/// onPressed: () {
|
||||
/// ref.read(confirmPasswordVisibilityProvider.notifier).toggle();
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
abstract class _$ConfirmPasswordVisibility extends $Notifier<bool> {
|
||||
bool build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<bool, bool>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<bool, bool>,
|
||||
bool,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
305
lib/features/auth/presentation/providers/register_provider.dart
Normal file
305
lib/features/auth/presentation/providers/register_provider.dart
Normal file
@@ -0,0 +1,305 @@
|
||||
/// Registration State Provider
|
||||
///
|
||||
/// Manages registration state for the Worker application.
|
||||
/// Handles user registration with role-based validation and verification.
|
||||
///
|
||||
/// Uses Riverpod 3.0 with code generation for type-safe state management.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/auth/domain/entities/user.dart';
|
||||
|
||||
part 'register_provider.g.dart';
|
||||
|
||||
/// Registration Form Data
|
||||
///
|
||||
/// Contains all data needed for user registration.
|
||||
/// Optional fields are used based on selected role.
|
||||
class RegistrationData {
|
||||
/// Required: Full name of the user
|
||||
final String fullName;
|
||||
|
||||
/// Required: Phone number (Vietnamese format)
|
||||
final String phoneNumber;
|
||||
|
||||
/// Required: Email address
|
||||
final String email;
|
||||
|
||||
/// Required: Password (minimum 6 characters)
|
||||
final String password;
|
||||
|
||||
/// Required: User role
|
||||
final UserRole role;
|
||||
|
||||
/// Optional: CCCD/ID card number (required for dealer/worker roles)
|
||||
final String? cccd;
|
||||
|
||||
/// Optional: Tax code (personal or company)
|
||||
final String? taxCode;
|
||||
|
||||
/// Optional: Company/store name
|
||||
final String? companyName;
|
||||
|
||||
/// Required: Province/city
|
||||
final String? city;
|
||||
|
||||
/// Optional: Attachment file paths (ID card, certificate, license)
|
||||
final List<String>? attachments;
|
||||
|
||||
const RegistrationData({
|
||||
required this.fullName,
|
||||
required this.phoneNumber,
|
||||
required this.email,
|
||||
required this.password,
|
||||
required this.role,
|
||||
this.cccd,
|
||||
this.taxCode,
|
||||
this.companyName,
|
||||
this.city,
|
||||
this.attachments,
|
||||
});
|
||||
|
||||
/// Copy with method for immutability
|
||||
RegistrationData copyWith({
|
||||
String? fullName,
|
||||
String? phoneNumber,
|
||||
String? email,
|
||||
String? password,
|
||||
UserRole? role,
|
||||
String? cccd,
|
||||
String? taxCode,
|
||||
String? companyName,
|
||||
String? city,
|
||||
List<String>? attachments,
|
||||
}) {
|
||||
return RegistrationData(
|
||||
fullName: fullName ?? this.fullName,
|
||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||
email: email ?? this.email,
|
||||
password: password ?? this.password,
|
||||
role: role ?? this.role,
|
||||
cccd: cccd ?? this.cccd,
|
||||
taxCode: taxCode ?? this.taxCode,
|
||||
companyName: companyName ?? this.companyName,
|
||||
city: city ?? this.city,
|
||||
attachments: attachments ?? this.attachments,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Registration State Provider
|
||||
///
|
||||
/// Main provider for user registration state management.
|
||||
/// Handles registration process with role-based validation.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final registerState = ref.watch(registerProvider);
|
||||
/// registerState.when(
|
||||
/// data: (user) => SuccessScreen(user),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
@riverpod
|
||||
class Register extends _$Register {
|
||||
/// Initialize with no registration result
|
||||
@override
|
||||
Future<User?> build() async {
|
||||
// No initial registration
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Register a new user
|
||||
///
|
||||
/// Performs user registration with role-based validation.
|
||||
/// For dealer/worker roles, requires additional verification documents.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - [data]: Registration form data containing all required fields
|
||||
///
|
||||
/// Returns: Newly created User object on success
|
||||
///
|
||||
/// Throws: Exception on validation failure or registration error
|
||||
///
|
||||
/// Error messages (Vietnamese):
|
||||
/// - "Vui lòng điền đầy đủ thông tin bắt buộc"
|
||||
/// - "Số điện thoại không hợp lệ"
|
||||
/// - "Email không hợp lệ"
|
||||
/// - "Mật khẩu phải có ít nhất 6 ký tự"
|
||||
/// - "Vui lòng nhập số CCCD/CMND" (for dealer/worker)
|
||||
/// - "Vui lòng tải lên ảnh CCCD/CMND" (for dealer/worker)
|
||||
/// - "Vui lòng tải lên ảnh chứng chỉ hành nghề hoặc GPKD" (for dealer/worker)
|
||||
/// - "Số điện thoại đã được đăng ký"
|
||||
/// - "Email đã được đăng ký"
|
||||
Future<void> register(RegistrationData data) async {
|
||||
// Set loading state
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
// Perform registration with error handling
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Validate required fields
|
||||
if (data.fullName.isEmpty ||
|
||||
data.phoneNumber.isEmpty ||
|
||||
data.email.isEmpty ||
|
||||
data.password.isEmpty ||
|
||||
data.city == null ||
|
||||
data.city!.isEmpty) {
|
||||
throw Exception('Vui lòng điền đầy đủ thông tin bắt buộc');
|
||||
}
|
||||
|
||||
// Validate phone number (Vietnamese format: 10 digits starting with 0)
|
||||
final phoneRegex = RegExp(r'^0[0-9]{9}$');
|
||||
if (!phoneRegex.hasMatch(data.phoneNumber)) {
|
||||
throw Exception('Số điện thoại không hợp lệ');
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
if (!emailRegex.hasMatch(data.email)) {
|
||||
throw Exception('Email không hợp lệ');
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if (data.password.length < 6) {
|
||||
throw Exception('Mật khẩu phải có ít nhất 6 ký tự');
|
||||
}
|
||||
|
||||
// Role-based validation for dealer/worker (requires verification)
|
||||
if (data.role == UserRole.customer) {
|
||||
// For dealer/worker roles, CCCD and attachments are required
|
||||
if (data.cccd == null || data.cccd!.isEmpty) {
|
||||
throw Exception('Vui lòng nhập số CCCD/CMND');
|
||||
}
|
||||
|
||||
// Validate CCCD format (9 or 12 digits)
|
||||
final cccdRegex = RegExp(r'^[0-9]{9}$|^[0-9]{12}$');
|
||||
if (!cccdRegex.hasMatch(data.cccd!)) {
|
||||
throw Exception('Số CCCD/CMND không hợp lệ (phải có 9 hoặc 12 số)');
|
||||
}
|
||||
|
||||
// Validate attachments
|
||||
if (data.attachments == null || data.attachments!.isEmpty) {
|
||||
throw Exception('Vui lòng tải lên ảnh CCCD/CMND');
|
||||
}
|
||||
|
||||
if (data.attachments!.length < 2) {
|
||||
throw Exception('Vui lòng tải lên ảnh chứng chỉ hành nghề hoặc GPKD');
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate API call delay (2 seconds)
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
|
||||
// TODO: In production, call the registration API here
|
||||
// final response = await ref.read(authRepositoryProvider).register(data);
|
||||
|
||||
// Mock: Simulate registration success
|
||||
final now = DateTime.now();
|
||||
|
||||
// Determine initial status based on role
|
||||
// Dealer/Worker require admin approval (pending status)
|
||||
// Other roles are immediately active
|
||||
final initialStatus = data.role == UserRole.customer
|
||||
? UserStatus.pending
|
||||
: UserStatus.active;
|
||||
|
||||
// Create new user entity
|
||||
final newUser = User(
|
||||
userId: 'user_${DateTime.now().millisecondsSinceEpoch}',
|
||||
phoneNumber: data.phoneNumber,
|
||||
fullName: data.fullName,
|
||||
email: data.email,
|
||||
role: data.role,
|
||||
status: initialStatus,
|
||||
loyaltyTier: LoyaltyTier.gold, // Default tier for new users
|
||||
totalPoints: 0, // New users start with 0 points
|
||||
companyInfo: data.companyName != null || data.taxCode != null
|
||||
? CompanyInfo(
|
||||
name: data.companyName,
|
||||
taxId: data.taxCode,
|
||||
businessType: _getBusinessType(data.role),
|
||||
)
|
||||
: null,
|
||||
cccd: data.cccd,
|
||||
attachments: data.attachments ?? [],
|
||||
address: data.city,
|
||||
avatarUrl: null,
|
||||
referralCode: 'REF${data.phoneNumber.substring(0, 6)}',
|
||||
referredBy: null,
|
||||
erpnextCustomerId: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
lastLoginAt: null, // Not logged in yet
|
||||
);
|
||||
|
||||
return newUser;
|
||||
});
|
||||
}
|
||||
|
||||
/// Reset registration state
|
||||
///
|
||||
/// Clears the registration result. Useful when navigating away
|
||||
/// from success screen or starting a new registration.
|
||||
Future<void> reset() async {
|
||||
state = const AsyncValue.data(null);
|
||||
}
|
||||
|
||||
/// Get business type based on user role
|
||||
String _getBusinessType(UserRole role) {
|
||||
switch (role) {
|
||||
case UserRole.customer:
|
||||
return 'Đại lý/Thầu thợ/Kiến trúc sư';
|
||||
case UserRole.sales:
|
||||
return 'Nhân viên kinh doanh';
|
||||
case UserRole.admin:
|
||||
return 'Quản trị viên';
|
||||
case UserRole.accountant:
|
||||
return 'Kế toán';
|
||||
case UserRole.designer:
|
||||
return 'Thiết kế';
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if registration is in progress
|
||||
bool get isLoading => state.isLoading;
|
||||
|
||||
/// Get registration error if any
|
||||
Object? get error => state.error;
|
||||
|
||||
/// Get registered user if successful
|
||||
User? get registeredUser => state.value;
|
||||
|
||||
/// Check if registration was successful
|
||||
bool get isSuccess => state.hasValue && state.value != null;
|
||||
}
|
||||
|
||||
/// Convenience provider for checking if registration is in progress
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final isRegistering = ref.watch(isRegisteringProvider);
|
||||
/// if (isRegistering) {
|
||||
/// // Show loading indicator
|
||||
/// }
|
||||
/// ```
|
||||
@riverpod
|
||||
bool isRegistering(Ref ref) {
|
||||
final registerState = ref.watch(registerProvider);
|
||||
return registerState.isLoading;
|
||||
}
|
||||
|
||||
/// Convenience provider for checking if registration was successful
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final success = ref.watch(registrationSuccessProvider);
|
||||
/// if (success) {
|
||||
/// // Navigate to pending approval or OTP screen
|
||||
/// }
|
||||
/// ```
|
||||
@riverpod
|
||||
bool registrationSuccess(Ref ref) {
|
||||
final registerState = ref.watch(registerProvider);
|
||||
return registerState.hasValue && registerState.value != null;
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'register_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Registration State Provider
|
||||
///
|
||||
/// Main provider for user registration state management.
|
||||
/// Handles registration process with role-based validation.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final registerState = ref.watch(registerProvider);
|
||||
/// registerState.when(
|
||||
/// data: (user) => SuccessScreen(user),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
@ProviderFor(Register)
|
||||
const registerProvider = RegisterProvider._();
|
||||
|
||||
/// Registration State Provider
|
||||
///
|
||||
/// Main provider for user registration state management.
|
||||
/// Handles registration process with role-based validation.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final registerState = ref.watch(registerProvider);
|
||||
/// registerState.when(
|
||||
/// data: (user) => SuccessScreen(user),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
final class RegisterProvider extends $AsyncNotifierProvider<Register, User?> {
|
||||
/// Registration State Provider
|
||||
///
|
||||
/// Main provider for user registration state management.
|
||||
/// Handles registration process with role-based validation.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final registerState = ref.watch(registerProvider);
|
||||
/// registerState.when(
|
||||
/// data: (user) => SuccessScreen(user),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
const RegisterProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'registerProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$registerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Register create() => Register();
|
||||
}
|
||||
|
||||
String _$registerHash() => r'a073b5c5958b74c63a3cddfec7f6f018e14a5088';
|
||||
|
||||
/// Registration State Provider
|
||||
///
|
||||
/// Main provider for user registration state management.
|
||||
/// Handles registration process with role-based validation.
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// final registerState = ref.watch(registerProvider);
|
||||
/// registerState.when(
|
||||
/// data: (user) => SuccessScreen(user),
|
||||
/// loading: () => LoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
abstract class _$Register extends $AsyncNotifier<User?> {
|
||||
FutureOr<User?> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<User?>, User?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<User?>, User?>,
|
||||
AsyncValue<User?>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience provider for checking if registration is in progress
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final isRegistering = ref.watch(isRegisteringProvider);
|
||||
/// if (isRegistering) {
|
||||
/// // Show loading indicator
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@ProviderFor(isRegistering)
|
||||
const isRegisteringProvider = IsRegisteringProvider._();
|
||||
|
||||
/// Convenience provider for checking if registration is in progress
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final isRegistering = ref.watch(isRegisteringProvider);
|
||||
/// if (isRegistering) {
|
||||
/// // Show loading indicator
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
final class IsRegisteringProvider extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Convenience provider for checking if registration is in progress
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final isRegistering = ref.watch(isRegisteringProvider);
|
||||
/// if (isRegistering) {
|
||||
/// // Show loading indicator
|
||||
/// }
|
||||
/// ```
|
||||
const IsRegisteringProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'isRegisteringProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$isRegisteringHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return isRegistering(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$isRegisteringHash() => r'2108b87b37451de9aaf799f9b8b380924bed2c87';
|
||||
|
||||
/// Convenience provider for checking if registration was successful
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final success = ref.watch(registrationSuccessProvider);
|
||||
/// if (success) {
|
||||
/// // Navigate to pending approval or OTP screen
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@ProviderFor(registrationSuccess)
|
||||
const registrationSuccessProvider = RegistrationSuccessProvider._();
|
||||
|
||||
/// Convenience provider for checking if registration was successful
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final success = ref.watch(registrationSuccessProvider);
|
||||
/// if (success) {
|
||||
/// // Navigate to pending approval or OTP screen
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
final class RegistrationSuccessProvider
|
||||
extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Convenience provider for checking if registration was successful
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final success = ref.watch(registrationSuccessProvider);
|
||||
/// if (success) {
|
||||
/// // Navigate to pending approval or OTP screen
|
||||
/// }
|
||||
/// ```
|
||||
const RegistrationSuccessProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'registrationSuccessProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$registrationSuccessHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return registrationSuccess(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$registrationSuccessHash() =>
|
||||
r'6435b9ca4bf4c287497a39077a5d4558e0515ddc';
|
||||
@@ -0,0 +1,175 @@
|
||||
/// Selected Role State Provider
|
||||
///
|
||||
/// Manages the selected user role during registration.
|
||||
/// Simple state provider for role selection in the registration form.
|
||||
///
|
||||
/// Uses Riverpod 3.0 with code generation for type-safe state management.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/auth/domain/entities/user.dart';
|
||||
|
||||
part 'selected_role_provider.g.dart';
|
||||
|
||||
/// Selected Role Provider
|
||||
///
|
||||
/// Manages the currently selected user role in the registration form.
|
||||
/// Provides methods to select and clear role selection.
|
||||
///
|
||||
/// This provider is used to:
|
||||
/// - Track which role the user has selected
|
||||
/// - Conditionally show/hide verification fields based on role
|
||||
/// - Validate required documents for dealer/worker roles
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// // Watch the selected role
|
||||
/// final selectedRole = ref.watch(selectedRoleProvider);
|
||||
///
|
||||
/// // Select a role
|
||||
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
|
||||
///
|
||||
/// // Clear selection
|
||||
/// ref.read(selectedRoleProvider.notifier).clearRole();
|
||||
///
|
||||
/// // Show verification section conditionally
|
||||
/// if (selectedRole == UserRole.customer) {
|
||||
/// VerificationSection(),
|
||||
/// }
|
||||
/// ```
|
||||
@riverpod
|
||||
class SelectedRole extends _$SelectedRole {
|
||||
/// Initialize with no role selected
|
||||
@override
|
||||
UserRole? build() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Select a user role
|
||||
///
|
||||
/// Updates the state with the newly selected role.
|
||||
/// This triggers UI updates that depend on role selection.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - [role]: The user role to select
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// // User selects "Đại lý hệ thống" (dealer)
|
||||
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
|
||||
/// // This will show verification fields
|
||||
/// ```
|
||||
void selectRole(UserRole role) {
|
||||
state = role;
|
||||
}
|
||||
|
||||
/// Clear the role selection
|
||||
///
|
||||
/// Resets the state to null (no role selected).
|
||||
/// Useful when resetting the form or canceling registration.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// // User clicks "Cancel" or goes back
|
||||
/// ref.read(selectedRoleProvider.notifier).clearRole();
|
||||
/// // This will hide verification fields
|
||||
/// ```
|
||||
void clearRole() {
|
||||
state = null;
|
||||
}
|
||||
|
||||
/// Check if a role is currently selected
|
||||
///
|
||||
/// Returns true if any role has been selected, false otherwise.
|
||||
bool get hasSelection => state != null;
|
||||
|
||||
/// Check if verification is required for current role
|
||||
///
|
||||
/// Returns true if the selected role requires verification documents
|
||||
/// (CCCD, certificates, etc.). Currently only customer role requires this.
|
||||
///
|
||||
/// This is used to conditionally show the verification section:
|
||||
/// ```dart
|
||||
/// if (ref.read(selectedRoleProvider.notifier).requiresVerification) {
|
||||
/// // Show CCCD input, file uploads, etc.
|
||||
/// }
|
||||
/// ```
|
||||
bool get requiresVerification => state == UserRole.customer;
|
||||
|
||||
/// Get the display name for the current role (Vietnamese)
|
||||
///
|
||||
/// Returns a user-friendly Vietnamese name for the selected role,
|
||||
/// or null if no role is selected.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final displayName = ref.read(selectedRoleProvider.notifier).displayName;
|
||||
/// // Returns: "Đại lý hệ thống" for customer role
|
||||
/// ```
|
||||
String? get displayName {
|
||||
if (state == null) return null;
|
||||
|
||||
switch (state!) {
|
||||
case UserRole.customer:
|
||||
return 'Đại lý/Thầu thợ/Kiến trúc sư';
|
||||
case UserRole.sales:
|
||||
return 'Nhân viên kinh doanh';
|
||||
case UserRole.admin:
|
||||
return 'Quản trị viên';
|
||||
case UserRole.accountant:
|
||||
return 'Kế toán';
|
||||
case UserRole.designer:
|
||||
return 'Thiết kế';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience provider for checking if verification is required
|
||||
///
|
||||
/// Returns true if the currently selected role requires verification
|
||||
/// documents (CCCD, certificates, etc.).
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final needsVerification = ref.watch(requiresVerificationProvider);
|
||||
/// if (needsVerification) {
|
||||
/// // Show verification section with file uploads
|
||||
/// }
|
||||
/// ```
|
||||
@riverpod
|
||||
bool requiresVerification(Ref ref) {
|
||||
final selectedRole = ref.watch(selectedRoleProvider);
|
||||
return selectedRole == UserRole.customer;
|
||||
}
|
||||
|
||||
/// Convenience provider for getting role display name
|
||||
///
|
||||
/// Returns a user-friendly Vietnamese name for the selected role,
|
||||
/// or null if no role is selected.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final roleName = ref.watch(roleDisplayNameProvider);
|
||||
/// if (roleName != null) {
|
||||
/// Text('Bạn đang đăng ký với vai trò: $roleName');
|
||||
/// }
|
||||
/// ```
|
||||
@riverpod
|
||||
String? roleDisplayName(Ref ref) {
|
||||
final selectedRole = ref.watch(selectedRoleProvider);
|
||||
|
||||
if (selectedRole == null) return null;
|
||||
|
||||
switch (selectedRole) {
|
||||
case UserRole.customer:
|
||||
return 'Đại lý/Thầu thợ/Kiến trúc sư';
|
||||
case UserRole.sales:
|
||||
return 'Nhân viên kinh doanh';
|
||||
case UserRole.admin:
|
||||
return 'Quản trị viên';
|
||||
case UserRole.accountant:
|
||||
return 'Kế toán';
|
||||
case UserRole.designer:
|
||||
return 'Thiết kế';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'selected_role_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Selected Role Provider
|
||||
///
|
||||
/// Manages the currently selected user role in the registration form.
|
||||
/// Provides methods to select and clear role selection.
|
||||
///
|
||||
/// This provider is used to:
|
||||
/// - Track which role the user has selected
|
||||
/// - Conditionally show/hide verification fields based on role
|
||||
/// - Validate required documents for dealer/worker roles
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// // Watch the selected role
|
||||
/// final selectedRole = ref.watch(selectedRoleProvider);
|
||||
///
|
||||
/// // Select a role
|
||||
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
|
||||
///
|
||||
/// // Clear selection
|
||||
/// ref.read(selectedRoleProvider.notifier).clearRole();
|
||||
///
|
||||
/// // Show verification section conditionally
|
||||
/// if (selectedRole == UserRole.customer) {
|
||||
/// VerificationSection(),
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@ProviderFor(SelectedRole)
|
||||
const selectedRoleProvider = SelectedRoleProvider._();
|
||||
|
||||
/// Selected Role Provider
|
||||
///
|
||||
/// Manages the currently selected user role in the registration form.
|
||||
/// Provides methods to select and clear role selection.
|
||||
///
|
||||
/// This provider is used to:
|
||||
/// - Track which role the user has selected
|
||||
/// - Conditionally show/hide verification fields based on role
|
||||
/// - Validate required documents for dealer/worker roles
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// // Watch the selected role
|
||||
/// final selectedRole = ref.watch(selectedRoleProvider);
|
||||
///
|
||||
/// // Select a role
|
||||
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
|
||||
///
|
||||
/// // Clear selection
|
||||
/// ref.read(selectedRoleProvider.notifier).clearRole();
|
||||
///
|
||||
/// // Show verification section conditionally
|
||||
/// if (selectedRole == UserRole.customer) {
|
||||
/// VerificationSection(),
|
||||
/// }
|
||||
/// ```
|
||||
final class SelectedRoleProvider
|
||||
extends $NotifierProvider<SelectedRole, UserRole?> {
|
||||
/// Selected Role Provider
|
||||
///
|
||||
/// Manages the currently selected user role in the registration form.
|
||||
/// Provides methods to select and clear role selection.
|
||||
///
|
||||
/// This provider is used to:
|
||||
/// - Track which role the user has selected
|
||||
/// - Conditionally show/hide verification fields based on role
|
||||
/// - Validate required documents for dealer/worker roles
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// // Watch the selected role
|
||||
/// final selectedRole = ref.watch(selectedRoleProvider);
|
||||
///
|
||||
/// // Select a role
|
||||
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
|
||||
///
|
||||
/// // Clear selection
|
||||
/// ref.read(selectedRoleProvider.notifier).clearRole();
|
||||
///
|
||||
/// // Show verification section conditionally
|
||||
/// if (selectedRole == UserRole.customer) {
|
||||
/// VerificationSection(),
|
||||
/// }
|
||||
/// ```
|
||||
const SelectedRoleProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedRoleProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedRoleHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedRole create() => SelectedRole();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(UserRole? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<UserRole?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedRoleHash() => r'098c7fdaec4694d14a48c049556960eb6ed2dc06';
|
||||
|
||||
/// Selected Role Provider
|
||||
///
|
||||
/// Manages the currently selected user role in the registration form.
|
||||
/// Provides methods to select and clear role selection.
|
||||
///
|
||||
/// This provider is used to:
|
||||
/// - Track which role the user has selected
|
||||
/// - Conditionally show/hide verification fields based on role
|
||||
/// - Validate required documents for dealer/worker roles
|
||||
///
|
||||
/// Usage in widgets:
|
||||
/// ```dart
|
||||
/// // Watch the selected role
|
||||
/// final selectedRole = ref.watch(selectedRoleProvider);
|
||||
///
|
||||
/// // Select a role
|
||||
/// ref.read(selectedRoleProvider.notifier).selectRole(UserRole.customer);
|
||||
///
|
||||
/// // Clear selection
|
||||
/// ref.read(selectedRoleProvider.notifier).clearRole();
|
||||
///
|
||||
/// // Show verification section conditionally
|
||||
/// if (selectedRole == UserRole.customer) {
|
||||
/// VerificationSection(),
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
abstract class _$SelectedRole extends $Notifier<UserRole?> {
|
||||
UserRole? build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<UserRole?, UserRole?>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<UserRole?, UserRole?>,
|
||||
UserRole?,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience provider for checking if verification is required
|
||||
///
|
||||
/// Returns true if the currently selected role requires verification
|
||||
/// documents (CCCD, certificates, etc.).
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final needsVerification = ref.watch(requiresVerificationProvider);
|
||||
/// if (needsVerification) {
|
||||
/// // Show verification section with file uploads
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@ProviderFor(requiresVerification)
|
||||
const requiresVerificationProvider = RequiresVerificationProvider._();
|
||||
|
||||
/// Convenience provider for checking if verification is required
|
||||
///
|
||||
/// Returns true if the currently selected role requires verification
|
||||
/// documents (CCCD, certificates, etc.).
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final needsVerification = ref.watch(requiresVerificationProvider);
|
||||
/// if (needsVerification) {
|
||||
/// // Show verification section with file uploads
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
final class RequiresVerificationProvider
|
||||
extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// Convenience provider for checking if verification is required
|
||||
///
|
||||
/// Returns true if the currently selected role requires verification
|
||||
/// documents (CCCD, certificates, etc.).
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final needsVerification = ref.watch(requiresVerificationProvider);
|
||||
/// if (needsVerification) {
|
||||
/// // Show verification section with file uploads
|
||||
/// }
|
||||
/// ```
|
||||
const RequiresVerificationProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'requiresVerificationProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$requiresVerificationHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return requiresVerification(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$requiresVerificationHash() =>
|
||||
r'400b4242bca2defd14e46361d2b77dd94a4e3e5e';
|
||||
|
||||
/// Convenience provider for getting role display name
|
||||
///
|
||||
/// Returns a user-friendly Vietnamese name for the selected role,
|
||||
/// or null if no role is selected.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final roleName = ref.watch(roleDisplayNameProvider);
|
||||
/// if (roleName != null) {
|
||||
/// Text('Bạn đang đăng ký với vai trò: $roleName');
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@ProviderFor(roleDisplayName)
|
||||
const roleDisplayNameProvider = RoleDisplayNameProvider._();
|
||||
|
||||
/// Convenience provider for getting role display name
|
||||
///
|
||||
/// Returns a user-friendly Vietnamese name for the selected role,
|
||||
/// or null if no role is selected.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final roleName = ref.watch(roleDisplayNameProvider);
|
||||
/// if (roleName != null) {
|
||||
/// Text('Bạn đang đăng ký với vai trò: $roleName');
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
final class RoleDisplayNameProvider
|
||||
extends $FunctionalProvider<String?, String?, String?>
|
||||
with $Provider<String?> {
|
||||
/// Convenience provider for getting role display name
|
||||
///
|
||||
/// Returns a user-friendly Vietnamese name for the selected role,
|
||||
/// or null if no role is selected.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final roleName = ref.watch(roleDisplayNameProvider);
|
||||
/// if (roleName != null) {
|
||||
/// Text('Bạn đang đăng ký với vai trò: $roleName');
|
||||
/// }
|
||||
/// ```
|
||||
const RoleDisplayNameProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'roleDisplayNameProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$roleDisplayNameHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<String?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
String? create(Ref ref) {
|
||||
return roleDisplayName(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$roleDisplayNameHash() => r'6cb4bfd9e76fb2f3ed52d4a249e5a2477bc6f39e';
|
||||
216
lib/features/auth/presentation/widgets/file_upload_card.dart
Normal file
216
lib/features/auth/presentation/widgets/file_upload_card.dart
Normal file
@@ -0,0 +1,216 @@
|
||||
/// File Upload Card Widget
|
||||
///
|
||||
/// Reusable widget for uploading image files with preview.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// File Upload Card
|
||||
///
|
||||
/// A reusable widget for uploading files with preview functionality.
|
||||
/// Features:
|
||||
/// - Dashed border upload area
|
||||
/// - Camera/file icon
|
||||
/// - Title and subtitle
|
||||
/// - Image preview after selection
|
||||
/// - Remove button
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// FileUploadCard(
|
||||
/// file: selectedFile,
|
||||
/// onTap: () => pickImage(),
|
||||
/// onRemove: () => removeImage(),
|
||||
/// icon: Icons.camera_alt,
|
||||
/// title: 'Chụp ảnh hoặc chọn file',
|
||||
/// subtitle: 'JPG, PNG tối đa 5MB',
|
||||
/// )
|
||||
/// ```
|
||||
class FileUploadCard extends StatelessWidget {
|
||||
/// Creates a file upload card
|
||||
const FileUploadCard({
|
||||
super.key,
|
||||
required this.file,
|
||||
required this.onTap,
|
||||
required this.onRemove,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
});
|
||||
|
||||
/// Selected file (null if not selected)
|
||||
final File? file;
|
||||
|
||||
/// Callback when upload area is tapped
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// Callback to remove selected file
|
||||
final VoidCallback onRemove;
|
||||
|
||||
/// Icon to display in upload area
|
||||
final IconData icon;
|
||||
|
||||
/// Title text
|
||||
final String title;
|
||||
|
||||
/// Subtitle text
|
||||
final String subtitle;
|
||||
|
||||
/// Format file size in bytes to human-readable string
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes == 0) return '0 B';
|
||||
const suffixes = ['B', 'KB', 'MB', 'GB'];
|
||||
final i = (bytes.bitLength - 1) ~/ 10;
|
||||
final size = bytes / (1 << (i * 10));
|
||||
return '${size.toStringAsFixed(2)} ${suffixes[i]}';
|
||||
}
|
||||
|
||||
/// Get file name from path
|
||||
String _getFileName(String path) {
|
||||
return path.split('/').last;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (file != null) {
|
||||
// Show preview with remove option
|
||||
return _buildPreview(context);
|
||||
} else {
|
||||
// Show upload area
|
||||
return _buildUploadArea(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Build upload area
|
||||
Widget _buildUploadArea(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
border: Border.all(
|
||||
color: const Color(0xFFCBD5E1),
|
||||
width: 2,
|
||||
strokeAlign: BorderSide.strokeAlignInside,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
),
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
children: [
|
||||
// Icon
|
||||
Icon(icon, size: 32, color: AppColors.grey500),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
|
||||
// Subtitle
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(fontSize: 12, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build preview with remove button
|
||||
Widget _buildPreview(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
border: Border.all(color: AppColors.grey100, width: 1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
child: Row(
|
||||
children: [
|
||||
// Thumbnail
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
child: Image.file(
|
||||
file!,
|
||||
width: 50,
|
||||
height: 50,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
color: AppColors.grey100,
|
||||
child: const Icon(
|
||||
Icons.broken_image,
|
||||
color: AppColors.grey500,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
|
||||
// File info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_getFileName(file!.path),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
FutureBuilder<int>(
|
||||
future: file!.length(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text(
|
||||
_formatFileSize(snapshot.data!),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
|
||||
// Remove button
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: AppColors.danger, size: 20),
|
||||
onPressed: onRemove,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
splashRadius: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
133
lib/features/auth/presentation/widgets/phone_input_field.dart
Normal file
133
lib/features/auth/presentation/widgets/phone_input_field.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
/// Phone Input Field Widget
|
||||
///
|
||||
/// Custom text field for Vietnamese phone number input.
|
||||
/// Supports formats: 0xxx xxx xxx, +84xxx xxx xxx, 84xxx xxx xxx
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Phone Input Field
|
||||
///
|
||||
/// A custom text field widget specifically designed for Vietnamese phone number input.
|
||||
/// Features:
|
||||
/// - Phone icon prefix
|
||||
/// - Numeric keyboard
|
||||
/// - Phone number formatting
|
||||
/// - Vietnamese phone validation support
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// PhoneInputField(
|
||||
/// controller: phoneController,
|
||||
/// validator: Validators.phone,
|
||||
/// onChanged: (value) {
|
||||
/// // Handle phone number change
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
class PhoneInputField extends StatelessWidget {
|
||||
/// Creates a phone input field
|
||||
const PhoneInputField({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.focusNode,
|
||||
this.validator,
|
||||
this.onChanged,
|
||||
this.onFieldSubmitted,
|
||||
this.enabled = true,
|
||||
this.autofocus = false,
|
||||
});
|
||||
|
||||
/// Text editing controller
|
||||
final TextEditingController controller;
|
||||
|
||||
/// Focus node for keyboard focus management
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// Form field validator
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
/// Callback when text changes
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
/// Callback when field is submitted
|
||||
final void Function(String)? onFieldSubmitted;
|
||||
|
||||
/// Whether the field is enabled
|
||||
final bool enabled;
|
||||
|
||||
/// Whether the field should auto-focus
|
||||
final bool autofocus;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
enabled: enabled,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.next,
|
||||
inputFormatters: [
|
||||
// Allow digits, spaces, +, and parentheses
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9+\s()]')),
|
||||
// Limit to reasonable phone length
|
||||
LengthLimitingTextInputFormatter(15),
|
||||
],
|
||||
style: const TextStyle(
|
||||
fontSize: InputFieldSpecs.fontSize,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Số điện thoại',
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.labelFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
hintText: 'Nhập số điện thoại',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.hintFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.phone,
|
||||
color: AppColors.primaryBlue,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 1.0),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 2.0),
|
||||
),
|
||||
errorStyle: const TextStyle(fontSize: 12.0, color: AppColors.danger),
|
||||
),
|
||||
validator: validator,
|
||||
onChanged: onChanged,
|
||||
onFieldSubmitted: onFieldSubmitted,
|
||||
);
|
||||
}
|
||||
}
|
||||
115
lib/features/auth/presentation/widgets/role_dropdown.dart
Normal file
115
lib/features/auth/presentation/widgets/role_dropdown.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
/// Role Dropdown Widget
|
||||
///
|
||||
/// Dropdown for selecting user role during registration.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Role Dropdown
|
||||
///
|
||||
/// A custom dropdown widget for selecting user role.
|
||||
/// Roles:
|
||||
/// - dealer: Đại lý hệ thống
|
||||
/// - worker: Kiến trúc sư/ Thầu thợ
|
||||
/// - broker: Khách lẻ
|
||||
/// - other: Khác
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// RoleDropdown(
|
||||
/// value: selectedRole,
|
||||
/// onChanged: (value) {
|
||||
/// setState(() {
|
||||
/// selectedRole = value;
|
||||
/// });
|
||||
/// },
|
||||
/// validator: (value) {
|
||||
/// if (value == null || value.isEmpty) {
|
||||
/// return 'Vui lòng chọn vai trò';
|
||||
/// }
|
||||
/// return null;
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
class RoleDropdown extends StatelessWidget {
|
||||
/// Creates a role dropdown
|
||||
const RoleDropdown({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
/// Selected role value
|
||||
final String? value;
|
||||
|
||||
/// Callback when role changes
|
||||
final void Function(String?) onChanged;
|
||||
|
||||
/// Form field validator
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonFormField<String>(
|
||||
value: value,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Chọn vai trò của bạn',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.hintFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.work,
|
||||
color: AppColors.primaryBlue,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 1.0),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 2.0),
|
||||
),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'dealer', child: Text('Đại lý hệ thống')),
|
||||
DropdownMenuItem(
|
||||
value: 'worker',
|
||||
child: Text('Kiến trúc sư/ Thầu thợ'),
|
||||
),
|
||||
DropdownMenuItem(value: 'broker', child: Text('Khách lẻ')),
|
||||
DropdownMenuItem(value: 'other', child: Text('Khác')),
|
||||
],
|
||||
onChanged: onChanged,
|
||||
validator: validator,
|
||||
icon: const Icon(Icons.arrow_drop_down, color: AppColors.grey500),
|
||||
dropdownColor: AppColors.white,
|
||||
style: const TextStyle(
|
||||
fontSize: InputFieldSpecs.fontSize,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user