add auth register
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
/// Matches design from html/register.html
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -12,12 +13,17 @@ 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/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/core/utils/validators.dart';
|
||||
import 'package:worker/features/auth/domain/entities/business_unit.dart';
|
||||
import 'package:worker/features/auth/domain/entities/city.dart';
|
||||
import 'package:worker/features/auth/domain/entities/customer_group.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/cities_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/customer_groups_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/session_provider.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
|
||||
///
|
||||
@@ -65,16 +71,60 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
final _companyFocus = FocusNode();
|
||||
|
||||
// State
|
||||
String? _selectedRole;
|
||||
String? _selectedCity;
|
||||
CustomerGroup? _selectedRole;
|
||||
City? _selectedCity;
|
||||
File? _idCardFile;
|
||||
File? _certificateFile;
|
||||
bool _termsAccepted = false;
|
||||
bool _passwordVisible = false;
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingData = true;
|
||||
bool _hasInitialized = false;
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
/// Initialize session and load data
|
||||
/// This should be called from build method or after widget is mounted
|
||||
Future<void> _initializeData() async {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isLoadingData = true;
|
||||
_hasInitialized = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Step 1: Get session (public API user)
|
||||
await ref.read(sessionProvider.notifier).getSession();
|
||||
|
||||
// Step 2: Fetch cities and customer groups in parallel using the session
|
||||
await Future.wait([
|
||||
ref.read(citiesProvider.notifier).fetchCities(),
|
||||
ref.read(customerGroupsProvider.notifier).fetchCustomerGroups(),
|
||||
]);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi tải dữ liệu: $e'),
|
||||
backgroundColor: AppColors.danger,
|
||||
action: SnackBarAction(
|
||||
label: 'Thử lại',
|
||||
textColor: AppColors.white,
|
||||
onPressed: _initializeData,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingData = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fullNameController.dispose();
|
||||
@@ -95,8 +145,23 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
}
|
||||
|
||||
/// Check if verification section should be shown
|
||||
/// Note: This is based on the old role system
|
||||
/// TODO: Update this logic based on actual customer group requirements
|
||||
bool get _shouldShowVerification {
|
||||
return _selectedRole == 'worker' || _selectedRole == 'dealer';
|
||||
// For now, always hide verification section since we're using customer groups
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Convert file to base64 string
|
||||
Future<String?> _fileToBase64(File? file) async {
|
||||
if (file == null) return null;
|
||||
try {
|
||||
final bytes = await file.readAsBytes();
|
||||
return base64Encode(bytes);
|
||||
} catch (e) {
|
||||
debugPrint('Error converting file to base64: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick image from gallery or camera
|
||||
@@ -238,48 +303,60 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Implement actual registration API call
|
||||
// Include widget.selectedBusinessUnit?.id in the API request
|
||||
// Example:
|
||||
// final result = await authRepository.register(
|
||||
// fullName: _fullNameController.text.trim(),
|
||||
// phone: _phoneController.text.trim(),
|
||||
// email: _emailController.text.trim(),
|
||||
// password: _passwordController.text,
|
||||
// role: _selectedRole,
|
||||
// businessUnitId: widget.selectedBusinessUnit?.id,
|
||||
// ...
|
||||
// );
|
||||
// Get session state for CSRF token and SID
|
||||
final sessionState = ref.read(sessionProvider);
|
||||
|
||||
// For now, simulate API delay
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (!sessionState.hasSession) {
|
||||
throw Exception('Session không hợp lệ. Vui lòng thử lại.');
|
||||
}
|
||||
|
||||
// Convert files to base64
|
||||
final idCardFrontBase64 = await _fileToBase64(_idCardFile);
|
||||
final certificatesBase64 = _certificateFile != null
|
||||
? [await _fileToBase64(_certificateFile)]
|
||||
: <String?>[];
|
||||
|
||||
// Remove null values from certificates list
|
||||
final validCertificates = certificatesBase64
|
||||
.whereType<String>()
|
||||
.toList();
|
||||
|
||||
// Call registration API
|
||||
final dataSource = ref.read(authRemoteDataSourceProvider);
|
||||
final response = await dataSource.register(
|
||||
csrfToken: sessionState.csrfToken!,
|
||||
sid: sessionState.sid!,
|
||||
fullName: _fullNameController.text.trim(),
|
||||
phone: _phoneController.text.trim(),
|
||||
email: _emailController.text.trim(),
|
||||
customerGroupCode: _selectedRole?.value ?? _selectedRole?.name ?? '',
|
||||
cityCode: _selectedCity?.code ?? '',
|
||||
companyName: _companyController.text.trim().isEmpty
|
||||
? null
|
||||
: _companyController.text.trim(),
|
||||
taxCode: _taxCodeController.text.trim().isEmpty
|
||||
? null
|
||||
: _taxCodeController.text.trim(),
|
||||
idCardFrontBase64: idCardFrontBase64,
|
||||
idCardBackBase64: null, // Not collecting back side in current UI
|
||||
certificatesBase64: validCertificates,
|
||||
);
|
||||
|
||||
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,
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Đăng ký thành công!',
|
||||
// response['message']?.toString() ?? 'Đăng ký thành công!',
|
||||
),
|
||||
);
|
||||
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();
|
||||
}
|
||||
duration: Duration(seconds: 1),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
|
||||
Future<void>.delayed(const Duration(seconds: 1)).then((_) => context.goLogin());
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@@ -301,6 +378,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Initialize data on first build
|
||||
if (!_hasInitialized) {
|
||||
// Use addPostFrameCallback to avoid calling setState during build
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initializeData();
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
appBar: AppBar(
|
||||
@@ -320,8 +405,22 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Form(
|
||||
body: _isLoadingData
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'Đang tải dữ liệu...',
|
||||
style: TextStyle(color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
@@ -442,29 +541,9 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Role Selection
|
||||
// Role Selection (Customer Groups)
|
||||
_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;
|
||||
},
|
||||
),
|
||||
_buildCustomerGroupDropdown(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Verification Section (conditional)
|
||||
@@ -488,47 +567,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
|
||||
// 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;
|
||||
},
|
||||
),
|
||||
_buildCityDropdown(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Terms and Conditions
|
||||
@@ -648,8 +687,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -712,6 +751,143 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build customer group dropdown
|
||||
Widget _buildCustomerGroupDropdown() {
|
||||
final customerGroupsAsync = ref.watch(customerGroupsProvider);
|
||||
|
||||
return customerGroupsAsync.when(
|
||||
data: (groups) {
|
||||
return DropdownButtonFormField<CustomerGroup>(
|
||||
value: _selectedRole,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Chọn vai trò',
|
||||
prefixIcon: Icons.work,
|
||||
),
|
||||
items: groups
|
||||
.map(
|
||||
(group) => DropdownMenuItem(
|
||||
value: group,
|
||||
child: Text(group.customerGroupName),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
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) {
|
||||
return 'Vui lòng chọn vai trò';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox(
|
||||
height: 48,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (error, stack) => Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.danger.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
border: Border.all(color: AppColors.danger),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: AppColors.danger, size: 20),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Lỗi tải vai trò',
|
||||
style: const TextStyle(color: AppColors.danger, fontSize: 12),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _initializeData,
|
||||
child: const Text('Thử lại', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build city dropdown
|
||||
Widget _buildCityDropdown() {
|
||||
final citiesAsync = ref.watch(citiesProvider);
|
||||
|
||||
return citiesAsync.when(
|
||||
data: (cities) {
|
||||
return DropdownButtonFormField<City>(
|
||||
value: _selectedCity,
|
||||
decoration: _buildInputDecoration(
|
||||
hintText: 'Chọn tỉnh/thành phố',
|
||||
prefixIcon: Icons.location_city,
|
||||
),
|
||||
items: cities
|
||||
.map(
|
||||
(city) => DropdownMenuItem(
|
||||
value: city,
|
||||
child: Text(city.cityName),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedCity = value;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return 'Vui lòng chọn tỉnh/thành phố';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox(
|
||||
height: 48,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (error, stack) => Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.danger.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
border: Border.all(color: AppColors.danger),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: AppColors.danger, size: 20),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Lỗi tải danh sách tỉnh/thành phố',
|
||||
style: const TextStyle(color: AppColors.danger, fontSize: 12),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _initializeData,
|
||||
child: const Text('Thử lại', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build verification section
|
||||
Widget _buildVerificationSection() {
|
||||
return Container(
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/// Cities Provider
|
||||
///
|
||||
/// Manages the list of cities/provinces for address selection
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
|
||||
import 'package:worker/features/auth/domain/entities/city.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/session_provider.dart';
|
||||
|
||||
part 'cities_provider.g.dart';
|
||||
|
||||
/// Cities Provider
|
||||
///
|
||||
/// Fetches list of cities from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
|
||||
@Riverpod(keepAlive: true)
|
||||
class Cities extends _$Cities {
|
||||
@override
|
||||
Future<List<City>> build() async {
|
||||
// Don't auto-fetch on build, wait for manual call
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Fetch cities from API
|
||||
Future<void> fetchCities() async {
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
state = await AsyncValue.guard(() async {
|
||||
final sessionState = ref.read(sessionProvider);
|
||||
|
||||
if (!sessionState.hasSession) {
|
||||
throw Exception('No active session. Please get session first.');
|
||||
}
|
||||
|
||||
final dataSource = ref.read(authRemoteDataSourceProvider);
|
||||
return await dataSource.getCities(
|
||||
csrfToken: sessionState.csrfToken!,
|
||||
sid: sessionState.sid!,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Refresh cities list
|
||||
Future<void> refresh() async {
|
||||
await fetchCities();
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider to get a specific city by code
|
||||
@riverpod
|
||||
City? cityByCode(Ref ref, String code) {
|
||||
final citiesAsync = ref.watch(citiesProvider);
|
||||
|
||||
return citiesAsync.whenOrNull(
|
||||
data: (List<City> cities) => cities.firstWhere(
|
||||
(City city) => city.code == code,
|
||||
orElse: () => cities.first,
|
||||
),
|
||||
);
|
||||
}
|
||||
160
lib/features/auth/presentation/providers/cities_provider.g.dart
Normal file
160
lib/features/auth/presentation/providers/cities_provider.g.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'cities_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Cities Provider
|
||||
///
|
||||
/// Fetches list of cities from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
|
||||
|
||||
@ProviderFor(Cities)
|
||||
const citiesProvider = CitiesProvider._();
|
||||
|
||||
/// Cities Provider
|
||||
///
|
||||
/// Fetches list of cities from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
|
||||
final class CitiesProvider extends $AsyncNotifierProvider<Cities, List<City>> {
|
||||
/// Cities Provider
|
||||
///
|
||||
/// Fetches list of cities from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
|
||||
const CitiesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'citiesProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$citiesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Cities create() => Cities();
|
||||
}
|
||||
|
||||
String _$citiesHash() => r'0de4a7d44e576d74ecd875ddad46d6cb52a38bf8';
|
||||
|
||||
/// Cities Provider
|
||||
///
|
||||
/// Fetches list of cities from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the cities list persists and doesn't auto-dispose.
|
||||
|
||||
abstract class _$Cities extends $AsyncNotifier<List<City>> {
|
||||
FutureOr<List<City>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<List<City>>, List<City>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<City>>, List<City>>,
|
||||
AsyncValue<List<City>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider to get a specific city by code
|
||||
|
||||
@ProviderFor(cityByCode)
|
||||
const cityByCodeProvider = CityByCodeFamily._();
|
||||
|
||||
/// Provider to get a specific city by code
|
||||
|
||||
final class CityByCodeProvider extends $FunctionalProvider<City?, City?, City?>
|
||||
with $Provider<City?> {
|
||||
/// Provider to get a specific city by code
|
||||
const CityByCodeProvider._({
|
||||
required CityByCodeFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'cityByCodeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$cityByCodeHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'cityByCodeProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<City?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
City? create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return cityByCode(ref, argument);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(City? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<City?>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CityByCodeProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$cityByCodeHash() => r'4e4a9a72526b0a366b8697244f2e7e2aedf7cabe';
|
||||
|
||||
/// Provider to get a specific city by code
|
||||
|
||||
final class CityByCodeFamily extends $Family
|
||||
with $FunctionalFamilyOverride<City?, String> {
|
||||
const CityByCodeFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'cityByCodeProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider to get a specific city by code
|
||||
|
||||
CityByCodeProvider call(String code) =>
|
||||
CityByCodeProvider._(argument: code, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'cityByCodeProvider';
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/// Customer Groups Provider
|
||||
///
|
||||
/// Manages the list of customer groups (roles) for user registration
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
|
||||
import 'package:worker/features/auth/domain/entities/customer_group.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/session_provider.dart';
|
||||
|
||||
part 'customer_groups_provider.g.dart';
|
||||
|
||||
/// Customer Groups Provider
|
||||
///
|
||||
/// Fetches list of customer groups (roles) from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
|
||||
@Riverpod(keepAlive: true)
|
||||
class CustomerGroups extends _$CustomerGroups {
|
||||
@override
|
||||
Future<List<CustomerGroup>> build() async {
|
||||
// Don't auto-fetch on build, wait for manual call
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Fetch customer groups from API
|
||||
Future<void> fetchCustomerGroups() async {
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
state = await AsyncValue.guard(() async {
|
||||
final sessionState = ref.read(sessionProvider);
|
||||
|
||||
if (!sessionState.hasSession) {
|
||||
throw Exception('No active session. Please get session first.');
|
||||
}
|
||||
|
||||
final dataSource = ref.read(authRemoteDataSourceProvider);
|
||||
return await dataSource.getCustomerGroups(
|
||||
csrfToken: sessionState.csrfToken!,
|
||||
sid: sessionState.sid!,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Refresh customer groups list
|
||||
Future<void> refresh() async {
|
||||
await fetchCustomerGroups();
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider to get a specific customer group by code/value
|
||||
@riverpod
|
||||
CustomerGroup? customerGroupByCode(
|
||||
Ref ref,
|
||||
String code,
|
||||
) {
|
||||
final groupsAsync = ref.watch(customerGroupsProvider);
|
||||
|
||||
return groupsAsync.whenOrNull(
|
||||
data: (List<CustomerGroup> groups) => groups.firstWhere(
|
||||
(CustomerGroup group) => group.value == code || group.name == code,
|
||||
orElse: () => groups.first,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'customer_groups_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Customer Groups Provider
|
||||
///
|
||||
/// Fetches list of customer groups (roles) from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
|
||||
|
||||
@ProviderFor(CustomerGroups)
|
||||
const customerGroupsProvider = CustomerGroupsProvider._();
|
||||
|
||||
/// Customer Groups Provider
|
||||
///
|
||||
/// Fetches list of customer groups (roles) from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
|
||||
final class CustomerGroupsProvider
|
||||
extends $AsyncNotifierProvider<CustomerGroups, List<CustomerGroup>> {
|
||||
/// Customer Groups Provider
|
||||
///
|
||||
/// Fetches list of customer groups (roles) from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
|
||||
const CustomerGroupsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'customerGroupsProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$customerGroupsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
CustomerGroups create() => CustomerGroups();
|
||||
}
|
||||
|
||||
String _$customerGroupsHash() => r'df9107ef844e3cd320804af8d5dcf2fee2462208';
|
||||
|
||||
/// Customer Groups Provider
|
||||
///
|
||||
/// Fetches list of customer groups (roles) from API for registration form.
|
||||
/// Requires active session (CSRF token and SID).
|
||||
/// keepAlive: true ensures the customer groups list persists and doesn't auto-dispose.
|
||||
|
||||
abstract class _$CustomerGroups extends $AsyncNotifier<List<CustomerGroup>> {
|
||||
FutureOr<List<CustomerGroup>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref =
|
||||
this.ref as $Ref<AsyncValue<List<CustomerGroup>>, List<CustomerGroup>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<CustomerGroup>>, List<CustomerGroup>>,
|
||||
AsyncValue<List<CustomerGroup>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider to get a specific customer group by code/value
|
||||
|
||||
@ProviderFor(customerGroupByCode)
|
||||
const customerGroupByCodeProvider = CustomerGroupByCodeFamily._();
|
||||
|
||||
/// Provider to get a specific customer group by code/value
|
||||
|
||||
final class CustomerGroupByCodeProvider
|
||||
extends $FunctionalProvider<CustomerGroup?, CustomerGroup?, CustomerGroup?>
|
||||
with $Provider<CustomerGroup?> {
|
||||
/// Provider to get a specific customer group by code/value
|
||||
const CustomerGroupByCodeProvider._({
|
||||
required CustomerGroupByCodeFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'customerGroupByCodeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$customerGroupByCodeHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'customerGroupByCodeProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<CustomerGroup?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
CustomerGroup? create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return customerGroupByCode(ref, argument);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(CustomerGroup? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<CustomerGroup?>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CustomerGroupByCodeProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$customerGroupByCodeHash() =>
|
||||
r'0ddf96a19d7c20c9fe3ddfd2af80ac49bfe76512';
|
||||
|
||||
/// Provider to get a specific customer group by code/value
|
||||
|
||||
final class CustomerGroupByCodeFamily extends $Family
|
||||
with $FunctionalFamilyOverride<CustomerGroup?, String> {
|
||||
const CustomerGroupByCodeFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'customerGroupByCodeProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider to get a specific customer group by code/value
|
||||
|
||||
CustomerGroupByCodeProvider call(String code) =>
|
||||
CustomerGroupByCodeProvider._(argument: code, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'customerGroupByCodeProvider';
|
||||
}
|
||||
107
lib/features/auth/presentation/providers/session_provider.dart
Normal file
107
lib/features/auth/presentation/providers/session_provider.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
/// Session Provider
|
||||
///
|
||||
/// Manages authentication session (SID and CSRF token)
|
||||
library;
|
||||
|
||||
import 'package:curl_logger_dio_interceptor/curl_logger_dio_interceptor.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
|
||||
import 'package:worker/features/auth/data/models/auth_session_model.dart';
|
||||
|
||||
part 'session_provider.g.dart';
|
||||
|
||||
/// Provider for Dio instance
|
||||
@Riverpod(keepAlive: true)
|
||||
Dio dio(Ref ref) {
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: 'https://land.dbiz.com',
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
),
|
||||
);
|
||||
|
||||
// Add curl logger interceptor for debugging
|
||||
dio.interceptors.add(CurlLoggerDioInterceptor(printOnSuccess: true));
|
||||
|
||||
return dio;
|
||||
}
|
||||
|
||||
/// Provider for AuthRemoteDataSource
|
||||
@Riverpod(keepAlive: true)
|
||||
AuthRemoteDataSource authRemoteDataSource(Ref ref) {
|
||||
final dio = ref.watch(dioProvider);
|
||||
return AuthRemoteDataSource(dio);
|
||||
}
|
||||
|
||||
/// Session State
|
||||
class SessionState {
|
||||
final String? sid;
|
||||
final String? csrfToken;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const SessionState({
|
||||
this.sid,
|
||||
this.csrfToken,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
SessionState copyWith({
|
||||
String? sid,
|
||||
String? csrfToken,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) {
|
||||
return SessionState(
|
||||
sid: sid ?? this.sid,
|
||||
csrfToken: csrfToken ?? this.csrfToken,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error ?? this.error,
|
||||
);
|
||||
}
|
||||
|
||||
bool get hasSession => sid != null && csrfToken != null;
|
||||
}
|
||||
|
||||
/// Session Provider
|
||||
///
|
||||
/// Manages the authentication session including SID and CSRF token.
|
||||
/// This should be called before making any authenticated requests.
|
||||
/// keepAlive: true ensures the session persists across the app lifecycle.
|
||||
@Riverpod(keepAlive: true)
|
||||
class Session extends _$Session {
|
||||
@override
|
||||
SessionState build() {
|
||||
return const SessionState();
|
||||
}
|
||||
|
||||
/// Get session from API
|
||||
Future<void> getSession() async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
final dataSource = ref.read(authRemoteDataSourceProvider);
|
||||
final response = await dataSource.getSession();
|
||||
|
||||
state = SessionState(
|
||||
sid: response.message.data.sid,
|
||||
csrfToken: response.message.data.csrfToken,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
state = SessionState(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear session
|
||||
void clearSession() {
|
||||
state = const SessionState();
|
||||
}
|
||||
}
|
||||
181
lib/features/auth/presentation/providers/session_provider.g.dart
Normal file
181
lib/features/auth/presentation/providers/session_provider.g.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'session_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for Dio instance
|
||||
|
||||
@ProviderFor(dio)
|
||||
const dioProvider = DioProvider._();
|
||||
|
||||
/// Provider for Dio instance
|
||||
|
||||
final class DioProvider extends $FunctionalProvider<Dio, Dio, Dio>
|
||||
with $Provider<Dio> {
|
||||
/// Provider for Dio instance
|
||||
const DioProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'dioProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$dioHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Dio> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Dio create(Ref ref) {
|
||||
return dio(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Dio value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Dio>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$dioHash() => r'2bc10725a1b646cfaabd88c722e5101c06837c75';
|
||||
|
||||
/// Provider for AuthRemoteDataSource
|
||||
|
||||
@ProviderFor(authRemoteDataSource)
|
||||
const authRemoteDataSourceProvider = AuthRemoteDataSourceProvider._();
|
||||
|
||||
/// Provider for AuthRemoteDataSource
|
||||
|
||||
final class AuthRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AuthRemoteDataSource,
|
||||
AuthRemoteDataSource,
|
||||
AuthRemoteDataSource
|
||||
>
|
||||
with $Provider<AuthRemoteDataSource> {
|
||||
/// Provider for AuthRemoteDataSource
|
||||
const AuthRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'authRemoteDataSourceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$authRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<AuthRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
AuthRemoteDataSource create(Ref ref) {
|
||||
return authRemoteDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(AuthRemoteDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<AuthRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$authRemoteDataSourceHash() =>
|
||||
r'6a18a0d3fee86c512b6eec01b8e8fda53a307a4c';
|
||||
|
||||
/// Session Provider
|
||||
///
|
||||
/// Manages the authentication session including SID and CSRF token.
|
||||
/// This should be called before making any authenticated requests.
|
||||
/// keepAlive: true ensures the session persists across the app lifecycle.
|
||||
|
||||
@ProviderFor(Session)
|
||||
const sessionProvider = SessionProvider._();
|
||||
|
||||
/// Session Provider
|
||||
///
|
||||
/// Manages the authentication session including SID and CSRF token.
|
||||
/// This should be called before making any authenticated requests.
|
||||
/// keepAlive: true ensures the session persists across the app lifecycle.
|
||||
final class SessionProvider extends $NotifierProvider<Session, SessionState> {
|
||||
/// Session Provider
|
||||
///
|
||||
/// Manages the authentication session including SID and CSRF token.
|
||||
/// This should be called before making any authenticated requests.
|
||||
/// keepAlive: true ensures the session persists across the app lifecycle.
|
||||
const SessionProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'sessionProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$sessionHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Session create() => Session();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(SessionState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<SessionState>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$sessionHash() => r'9c755f010681d87ab3898c4daaa920501104df46';
|
||||
|
||||
/// Session Provider
|
||||
///
|
||||
/// Manages the authentication session including SID and CSRF token.
|
||||
/// This should be called before making any authenticated requests.
|
||||
/// keepAlive: true ensures the session persists across the app lifecycle.
|
||||
|
||||
abstract class _$Session extends $Notifier<SessionState> {
|
||||
SessionState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<SessionState, SessionState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<SessionState, SessionState>,
|
||||
SessionState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user