diff --git a/docs/auth.sh b/docs/auth.sh new file mode 100644 index 0000000..2fc35c6 --- /dev/null +++ b/docs/auth.sh @@ -0,0 +1,58 @@ +GET SESSION +curl --location --request POST 'https://land.dbiz.com//api/method/dbiz_common.dbiz_common.api.auth.get_session' \ +--data '' + +DATA RETURN +{ + "message": { + "data": { + "sid": "edb6059ecf147f268176cd4aff8ca034a75ebb8ff23464f9913c9537", + "csrf_token": "d0077178c349f69bc1456401d9a3d90ef0f7b9df3e08cfd26794a53f" + } + }, + "home_page": "/app", + "full_name": "PublicAPI" +} + +GET CITY +curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \ +--header 'X-Frappe-Csrf-Token: 3d072ea39d245c2340ecc42d0825f6cbb7e674943c9c74346c8e4629' \ +--header 'Content-Type: application/json' \ +--header 'Cookie: full_name=PublicAPI; sid=b5a564f45d2fbe8b47dab89382801bf9a6e5c618500feb84a738f205; system_user=no; user_id=public_api%40dbiz.com; user_image=' \ +--data '{ + "doctype": "City", + "fields": ["city_name","name","code"], + "limit_page_length": 0 +}' + +GET ROLE +curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \ +--header 'X-Frappe-Csrf-Token: 3d072ea39d245c2340ecc42d0825f6cbb7e674943c9c74346c8e4629' \ +--header 'Content-Type: application/json' \ +--header 'Cookie: full_name=PublicAPI; sid=b5a564f45d2fbe8b47dab89382801bf9a6e5c618500feb84a738f205; system_user=no; user_id=public_api%40dbiz.com; user_image=' \ +--data '{ + "doctype": "Customer Group", + "fields": ["customer_group_name","name","value"], + "filters": {"is_group": 0, "is_active" : 1, "customer" : 1}, + "limit_page_length": 0 +}' + +REGISTER +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.user.register' \ +--header 'X-Frappe-Csrf-Token: 3d072ea39d245c2340ecc42d0825f6cbb7e674943c9c74346c8e4629' \ +--header 'Cookie: sid=b5a564f45d2fbe8b47dab89382801bf9a6e5c618500feb84a738f205; full_name=PublicAPI; sid=b5a564f45d2fbe8b47dab89382801bf9a6e5c618500feb84a738f205; system_user=no; user_id=public_api%40dbiz.com; user_image=' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "full_name" : "Nguyễn Lê Duy Tiến", + "phone" : "091321236", + "email" : "tiennld6@dbiz.com", + "customer_group_code" : "ACT", + "company_name" : null, + "city_code" : "01", + "tax_code" : "091231", + "id_card_front_base64" : "base64 tr", + "id_card_back_base64" : "base64 str", + "certificates_base64" : [ + "bas64_1 str","base64_2 str" + ] +}' \ No newline at end of file diff --git a/lib/features/auth/data/datasources/auth_remote_datasource.dart b/lib/features/auth/data/datasources/auth_remote_datasource.dart new file mode 100644 index 0000000..54d27e9 --- /dev/null +++ b/lib/features/auth/data/datasources/auth_remote_datasource.dart @@ -0,0 +1,245 @@ +/// Authentication Remote Data Source +/// +/// Handles all authentication-related API calls. +library; + +import 'package:dio/dio.dart'; +import 'package:worker/core/errors/exceptions.dart'; +import 'package:worker/features/auth/data/models/auth_session_model.dart'; +import 'package:worker/features/auth/domain/entities/city.dart'; +import 'package:worker/features/auth/domain/entities/customer_group.dart'; + +/// Authentication Remote Data Source +/// +/// Provides methods for: +/// - Getting session (CSRF token and SID) +/// - Fetching cities for registration +/// - Fetching customer groups (roles) for registration +/// - User registration +class AuthRemoteDataSource { + final Dio _dio; + + AuthRemoteDataSource(this._dio); + + /// Get Session + /// + /// Fetches session data including SID and CSRF token. + /// This should be called before making authenticated requests. + /// + /// API: POST /api/method/dbiz_common.dbiz_common.api.auth.get_session + Future getSession() async { + try { + final response = await _dio.post>( + '/api/method/dbiz_common.dbiz_common.api.auth.get_session', + data: '', + ); + + if (response.statusCode == 200 && response.data != null) { + return GetSessionResponse.fromJson(response.data!); + } else { + throw ServerException( + 'Failed to get session: ${response.statusCode}', + ); + } + } on DioException catch (e) { + if (e.response?.statusCode == 401) { + throw const UnauthorizedException(); + } else if (e.response?.statusCode == 404) { + throw NotFoundException('Session endpoint not found'); + } else { + throw NetworkException( + e.message ?? 'Failed to get session', + ); + } + } catch (e) { + throw ServerException('Unexpected error: $e'); + } + } + + /// Get Cities + /// + /// Fetches list of cities/provinces for address selection. + /// Requires authenticated session (CSRF token and Cookie). + /// + /// API: POST /api/method/frappe.client.get_list + /// DocType: City + Future> getCities({ + required String csrfToken, + required String sid, + }) async { + try { + final response = await _dio.post>( + '/api/method/frappe.client.get_list', + data: { + 'doctype': 'City', + 'fields': ['city_name', 'name', 'code'], + 'limit_page_length': 0, + }, + options: Options( + headers: { + 'X-Frappe-Csrf-Token': csrfToken, + 'Cookie': 'sid=$sid', + }, + ), + ); + + if (response.statusCode == 200 && response.data != null) { + final message = response.data!['message']; + if (message is List) { + return message + .map((json) => City.fromJson(json as Map)) + .toList(); + } else { + throw ServerException('Invalid response format for cities'); + } + } else { + throw ServerException( + 'Failed to get cities: ${response.statusCode}', + ); + } + } on DioException catch (e) { + if (e.response?.statusCode == 401) { + throw const UnauthorizedException(); + } else if (e.response?.statusCode == 404) { + throw NotFoundException('Cities endpoint not found'); + } else { + throw NetworkException( + e.message ?? 'Failed to get cities', + ); + } + } catch (e) { + throw ServerException('Unexpected error: $e'); + } + } + + /// Get Customer Groups (Roles) + /// + /// Fetches list of customer groups for user role selection. + /// Requires authenticated session (CSRF token and Cookie). + /// + /// API: POST /api/method/frappe.client.get_list + /// DocType: Customer Group + Future> getCustomerGroups({ + required String csrfToken, + required String sid, + }) async { + try { + final response = await _dio.post>( + '/api/method/frappe.client.get_list', + data: { + 'doctype': 'Customer Group', + 'fields': ['customer_group_name', 'name', 'value'], + 'filters': { + 'is_group': 0, + 'is_active': 1, + 'customer': 1, + }, + 'limit_page_length': 0, + }, + options: Options( + headers: { + 'X-Frappe-Csrf-Token': csrfToken, + 'Cookie': 'sid=$sid', + }, + ), + ); + + if (response.statusCode == 200 && response.data != null) { + final message = response.data!['message']; + if (message is List) { + return message + .map((json) => + CustomerGroup.fromJson(json as Map)) + .toList(); + } else { + throw ServerException('Invalid response format for customer groups'); + } + } else { + throw ServerException( + 'Failed to get customer groups: ${response.statusCode}', + ); + } + } on DioException catch (e) { + if (e.response?.statusCode == 401) { + throw const UnauthorizedException(); + } else if (e.response?.statusCode == 404) { + throw NotFoundException('Customer groups endpoint not found'); + } else { + throw NetworkException( + e.message ?? 'Failed to get customer groups', + ); + } + } catch (e) { + throw ServerException('Unexpected error: $e'); + } + } + + /// Register User + /// + /// Registers a new user with the provided information. + /// Requires authenticated session (CSRF token and Cookie). + /// + /// API: POST /api/method/building_material.building_material.api.user.register + Future> register({ + required String csrfToken, + required String sid, + required String fullName, + required String phone, + required String email, + required String customerGroupCode, + required String cityCode, + String? companyName, + String? taxCode, + String? idCardFrontBase64, + String? idCardBackBase64, + List? certificatesBase64, + }) async { + try { + final response = await _dio.post>( + '/api/method/building_material.building_material.api.user.register', + data: { + 'full_name': fullName, + 'phone': phone, + 'email': email, + 'customer_group_code': customerGroupCode, + 'city_code': cityCode, + 'company_name': companyName, + 'tax_code': taxCode, + 'id_card_front_base64': idCardFrontBase64, + 'id_card_back_base64': idCardBackBase64, + 'certificates_base64': certificatesBase64 ?? [], + }, + options: Options( + headers: { + 'X-Frappe-Csrf-Token': csrfToken, + 'Cookie': 'sid=$sid', + }, + ), + ); + + if (response.statusCode == 200 && response.data != null) { + return response.data!; + } else { + throw ServerException( + 'Failed to register: ${response.statusCode}', + ); + } + } on DioException catch (e) { + if (e.response?.statusCode == 401) { + throw const UnauthorizedException(); + } else if (e.response?.statusCode == 400) { + throw ValidationException( + e.response?.data?['message'] as String? ?? 'Validation error', + ); + } else if (e.response?.statusCode == 404) { + throw NotFoundException('Register endpoint not found'); + } else { + throw NetworkException( + e.message ?? 'Failed to register', + ); + } + } catch (e) { + throw ServerException('Unexpected error: $e'); + } + } +} diff --git a/lib/features/auth/data/models/auth_session_model.dart b/lib/features/auth/data/models/auth_session_model.dart index 6b6ee05..507dc7b 100644 --- a/lib/features/auth/data/models/auth_session_model.dart +++ b/lib/features/auth/data/models/auth_session_model.dart @@ -57,6 +57,48 @@ sealed class AuthSessionResponse with _$AuthSessionResponse { _$AuthSessionResponseFromJson(json); } +/// Session Data (for GET SESSION API) +/// +/// Represents the session data structure from get_session API. +@freezed +sealed class GetSessionData with _$GetSessionData { + const factory GetSessionData({ + required String sid, + @JsonKey(name: 'csrf_token') required String csrfToken, + }) = _GetSessionData; + + factory GetSessionData.fromJson(Map json) => + _$GetSessionDataFromJson(json); +} + +/// Get Session Message +/// +/// Wrapper for session data in get_session API response. +@freezed +sealed class GetSessionMessage with _$GetSessionMessage { + const factory GetSessionMessage({ + required GetSessionData data, + }) = _GetSessionMessage; + + factory GetSessionMessage.fromJson(Map json) => + _$GetSessionMessageFromJson(json); +} + +/// Get Session Response +/// +/// Complete response from get_session API. +@freezed +sealed class GetSessionResponse with _$GetSessionResponse { + const factory GetSessionResponse({ + required GetSessionMessage message, + @JsonKey(name: 'home_page') required String homePage, + @JsonKey(name: 'full_name') required String fullName, + }) = _GetSessionResponse; + + factory GetSessionResponse.fromJson(Map json) => + _$GetSessionResponseFromJson(json); +} + /// Session Storage Model /// /// Simplified model for storing session data in Hive. @@ -73,7 +115,17 @@ sealed class SessionData with _$SessionData { factory SessionData.fromJson(Map json) => _$SessionDataFromJson(json); - /// Create from API response + /// Create from get_session API response + factory SessionData.fromGetSessionResponse(GetSessionResponse response) { + return SessionData( + sid: response.message.data.sid, + csrfToken: response.message.data.csrfToken, + fullName: response.fullName, + createdAt: DateTime.now(), + ); + } + + /// Create from auth login API response factory SessionData.fromAuthResponse(AuthSessionResponse response) { return SessionData( sid: response.message.sid, diff --git a/lib/features/auth/data/models/auth_session_model.freezed.dart b/lib/features/auth/data/models/auth_session_model.freezed.dart index 3fb1040..51aeecf 100644 --- a/lib/features/auth/data/models/auth_session_model.freezed.dart +++ b/lib/features/auth/data/models/auth_session_model.freezed.dart @@ -834,6 +834,822 @@ $LoginMessageCopyWith<$Res> get message { } +/// @nodoc +mixin _$GetSessionData { + + String get sid;@JsonKey(name: 'csrf_token') String get csrfToken; +/// Create a copy of GetSessionData +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$GetSessionDataCopyWith get copyWith => _$GetSessionDataCopyWithImpl(this as GetSessionData, _$identity); + + /// Serializes this GetSessionData to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is GetSessionData&&(identical(other.sid, sid) || other.sid == sid)&&(identical(other.csrfToken, csrfToken) || other.csrfToken == csrfToken)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,sid,csrfToken); + +@override +String toString() { + return 'GetSessionData(sid: $sid, csrfToken: $csrfToken)'; +} + + +} + +/// @nodoc +abstract mixin class $GetSessionDataCopyWith<$Res> { + factory $GetSessionDataCopyWith(GetSessionData value, $Res Function(GetSessionData) _then) = _$GetSessionDataCopyWithImpl; +@useResult +$Res call({ + String sid,@JsonKey(name: 'csrf_token') String csrfToken +}); + + + + +} +/// @nodoc +class _$GetSessionDataCopyWithImpl<$Res> + implements $GetSessionDataCopyWith<$Res> { + _$GetSessionDataCopyWithImpl(this._self, this._then); + + final GetSessionData _self; + final $Res Function(GetSessionData) _then; + +/// Create a copy of GetSessionData +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? sid = null,Object? csrfToken = null,}) { + return _then(_self.copyWith( +sid: null == sid ? _self.sid : sid // ignore: cast_nullable_to_non_nullable +as String,csrfToken: null == csrfToken ? _self.csrfToken : csrfToken // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [GetSessionData]. +extension GetSessionDataPatterns on GetSessionData { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _GetSessionData value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _GetSessionData() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _GetSessionData value) $default,){ +final _that = this; +switch (_that) { +case _GetSessionData(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _GetSessionData value)? $default,){ +final _that = this; +switch (_that) { +case _GetSessionData() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String sid, @JsonKey(name: 'csrf_token') String csrfToken)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _GetSessionData() when $default != null: +return $default(_that.sid,_that.csrfToken);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String sid, @JsonKey(name: 'csrf_token') String csrfToken) $default,) {final _that = this; +switch (_that) { +case _GetSessionData(): +return $default(_that.sid,_that.csrfToken);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String sid, @JsonKey(name: 'csrf_token') String csrfToken)? $default,) {final _that = this; +switch (_that) { +case _GetSessionData() when $default != null: +return $default(_that.sid,_that.csrfToken);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _GetSessionData implements GetSessionData { + const _GetSessionData({required this.sid, @JsonKey(name: 'csrf_token') required this.csrfToken}); + factory _GetSessionData.fromJson(Map json) => _$GetSessionDataFromJson(json); + +@override final String sid; +@override@JsonKey(name: 'csrf_token') final String csrfToken; + +/// Create a copy of GetSessionData +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$GetSessionDataCopyWith<_GetSessionData> get copyWith => __$GetSessionDataCopyWithImpl<_GetSessionData>(this, _$identity); + +@override +Map toJson() { + return _$GetSessionDataToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _GetSessionData&&(identical(other.sid, sid) || other.sid == sid)&&(identical(other.csrfToken, csrfToken) || other.csrfToken == csrfToken)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,sid,csrfToken); + +@override +String toString() { + return 'GetSessionData(sid: $sid, csrfToken: $csrfToken)'; +} + + +} + +/// @nodoc +abstract mixin class _$GetSessionDataCopyWith<$Res> implements $GetSessionDataCopyWith<$Res> { + factory _$GetSessionDataCopyWith(_GetSessionData value, $Res Function(_GetSessionData) _then) = __$GetSessionDataCopyWithImpl; +@override @useResult +$Res call({ + String sid,@JsonKey(name: 'csrf_token') String csrfToken +}); + + + + +} +/// @nodoc +class __$GetSessionDataCopyWithImpl<$Res> + implements _$GetSessionDataCopyWith<$Res> { + __$GetSessionDataCopyWithImpl(this._self, this._then); + + final _GetSessionData _self; + final $Res Function(_GetSessionData) _then; + +/// Create a copy of GetSessionData +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? sid = null,Object? csrfToken = null,}) { + return _then(_GetSessionData( +sid: null == sid ? _self.sid : sid // ignore: cast_nullable_to_non_nullable +as String,csrfToken: null == csrfToken ? _self.csrfToken : csrfToken // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + + +/// @nodoc +mixin _$GetSessionMessage { + + GetSessionData get data; +/// Create a copy of GetSessionMessage +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$GetSessionMessageCopyWith get copyWith => _$GetSessionMessageCopyWithImpl(this as GetSessionMessage, _$identity); + + /// Serializes this GetSessionMessage to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is GetSessionMessage&&(identical(other.data, data) || other.data == data)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,data); + +@override +String toString() { + return 'GetSessionMessage(data: $data)'; +} + + +} + +/// @nodoc +abstract mixin class $GetSessionMessageCopyWith<$Res> { + factory $GetSessionMessageCopyWith(GetSessionMessage value, $Res Function(GetSessionMessage) _then) = _$GetSessionMessageCopyWithImpl; +@useResult +$Res call({ + GetSessionData data +}); + + +$GetSessionDataCopyWith<$Res> get data; + +} +/// @nodoc +class _$GetSessionMessageCopyWithImpl<$Res> + implements $GetSessionMessageCopyWith<$Res> { + _$GetSessionMessageCopyWithImpl(this._self, this._then); + + final GetSessionMessage _self; + final $Res Function(GetSessionMessage) _then; + +/// Create a copy of GetSessionMessage +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? data = null,}) { + return _then(_self.copyWith( +data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as GetSessionData, + )); +} +/// Create a copy of GetSessionMessage +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$GetSessionDataCopyWith<$Res> get data { + + return $GetSessionDataCopyWith<$Res>(_self.data, (value) { + return _then(_self.copyWith(data: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [GetSessionMessage]. +extension GetSessionMessagePatterns on GetSessionMessage { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _GetSessionMessage value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _GetSessionMessage() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _GetSessionMessage value) $default,){ +final _that = this; +switch (_that) { +case _GetSessionMessage(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _GetSessionMessage value)? $default,){ +final _that = this; +switch (_that) { +case _GetSessionMessage() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( GetSessionData data)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _GetSessionMessage() when $default != null: +return $default(_that.data);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( GetSessionData data) $default,) {final _that = this; +switch (_that) { +case _GetSessionMessage(): +return $default(_that.data);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( GetSessionData data)? $default,) {final _that = this; +switch (_that) { +case _GetSessionMessage() when $default != null: +return $default(_that.data);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _GetSessionMessage implements GetSessionMessage { + const _GetSessionMessage({required this.data}); + factory _GetSessionMessage.fromJson(Map json) => _$GetSessionMessageFromJson(json); + +@override final GetSessionData data; + +/// Create a copy of GetSessionMessage +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$GetSessionMessageCopyWith<_GetSessionMessage> get copyWith => __$GetSessionMessageCopyWithImpl<_GetSessionMessage>(this, _$identity); + +@override +Map toJson() { + return _$GetSessionMessageToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _GetSessionMessage&&(identical(other.data, data) || other.data == data)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,data); + +@override +String toString() { + return 'GetSessionMessage(data: $data)'; +} + + +} + +/// @nodoc +abstract mixin class _$GetSessionMessageCopyWith<$Res> implements $GetSessionMessageCopyWith<$Res> { + factory _$GetSessionMessageCopyWith(_GetSessionMessage value, $Res Function(_GetSessionMessage) _then) = __$GetSessionMessageCopyWithImpl; +@override @useResult +$Res call({ + GetSessionData data +}); + + +@override $GetSessionDataCopyWith<$Res> get data; + +} +/// @nodoc +class __$GetSessionMessageCopyWithImpl<$Res> + implements _$GetSessionMessageCopyWith<$Res> { + __$GetSessionMessageCopyWithImpl(this._self, this._then); + + final _GetSessionMessage _self; + final $Res Function(_GetSessionMessage) _then; + +/// Create a copy of GetSessionMessage +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? data = null,}) { + return _then(_GetSessionMessage( +data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as GetSessionData, + )); +} + +/// Create a copy of GetSessionMessage +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$GetSessionDataCopyWith<$Res> get data { + + return $GetSessionDataCopyWith<$Res>(_self.data, (value) { + return _then(_self.copyWith(data: value)); + }); +} +} + + +/// @nodoc +mixin _$GetSessionResponse { + + GetSessionMessage get message;@JsonKey(name: 'home_page') String get homePage;@JsonKey(name: 'full_name') String get fullName; +/// Create a copy of GetSessionResponse +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$GetSessionResponseCopyWith get copyWith => _$GetSessionResponseCopyWithImpl(this as GetSessionResponse, _$identity); + + /// Serializes this GetSessionResponse to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is GetSessionResponse&&(identical(other.message, message) || other.message == message)&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.fullName, fullName) || other.fullName == fullName)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,message,homePage,fullName); + +@override +String toString() { + return 'GetSessionResponse(message: $message, homePage: $homePage, fullName: $fullName)'; +} + + +} + +/// @nodoc +abstract mixin class $GetSessionResponseCopyWith<$Res> { + factory $GetSessionResponseCopyWith(GetSessionResponse value, $Res Function(GetSessionResponse) _then) = _$GetSessionResponseCopyWithImpl; +@useResult +$Res call({ + GetSessionMessage message,@JsonKey(name: 'home_page') String homePage,@JsonKey(name: 'full_name') String fullName +}); + + +$GetSessionMessageCopyWith<$Res> get message; + +} +/// @nodoc +class _$GetSessionResponseCopyWithImpl<$Res> + implements $GetSessionResponseCopyWith<$Res> { + _$GetSessionResponseCopyWithImpl(this._self, this._then); + + final GetSessionResponse _self; + final $Res Function(GetSessionResponse) _then; + +/// Create a copy of GetSessionResponse +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? message = null,Object? homePage = null,Object? fullName = null,}) { + return _then(_self.copyWith( +message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable +as GetSessionMessage,homePage: null == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable +as String,fullName: null == fullName ? _self.fullName : fullName // ignore: cast_nullable_to_non_nullable +as String, + )); +} +/// Create a copy of GetSessionResponse +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$GetSessionMessageCopyWith<$Res> get message { + + return $GetSessionMessageCopyWith<$Res>(_self.message, (value) { + return _then(_self.copyWith(message: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [GetSessionResponse]. +extension GetSessionResponsePatterns on GetSessionResponse { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _GetSessionResponse value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _GetSessionResponse() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _GetSessionResponse value) $default,){ +final _that = this; +switch (_that) { +case _GetSessionResponse(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _GetSessionResponse value)? $default,){ +final _that = this; +switch (_that) { +case _GetSessionResponse() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( GetSessionMessage message, @JsonKey(name: 'home_page') String homePage, @JsonKey(name: 'full_name') String fullName)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _GetSessionResponse() when $default != null: +return $default(_that.message,_that.homePage,_that.fullName);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( GetSessionMessage message, @JsonKey(name: 'home_page') String homePage, @JsonKey(name: 'full_name') String fullName) $default,) {final _that = this; +switch (_that) { +case _GetSessionResponse(): +return $default(_that.message,_that.homePage,_that.fullName);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( GetSessionMessage message, @JsonKey(name: 'home_page') String homePage, @JsonKey(name: 'full_name') String fullName)? $default,) {final _that = this; +switch (_that) { +case _GetSessionResponse() when $default != null: +return $default(_that.message,_that.homePage,_that.fullName);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _GetSessionResponse implements GetSessionResponse { + const _GetSessionResponse({required this.message, @JsonKey(name: 'home_page') required this.homePage, @JsonKey(name: 'full_name') required this.fullName}); + factory _GetSessionResponse.fromJson(Map json) => _$GetSessionResponseFromJson(json); + +@override final GetSessionMessage message; +@override@JsonKey(name: 'home_page') final String homePage; +@override@JsonKey(name: 'full_name') final String fullName; + +/// Create a copy of GetSessionResponse +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$GetSessionResponseCopyWith<_GetSessionResponse> get copyWith => __$GetSessionResponseCopyWithImpl<_GetSessionResponse>(this, _$identity); + +@override +Map toJson() { + return _$GetSessionResponseToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _GetSessionResponse&&(identical(other.message, message) || other.message == message)&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.fullName, fullName) || other.fullName == fullName)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,message,homePage,fullName); + +@override +String toString() { + return 'GetSessionResponse(message: $message, homePage: $homePage, fullName: $fullName)'; +} + + +} + +/// @nodoc +abstract mixin class _$GetSessionResponseCopyWith<$Res> implements $GetSessionResponseCopyWith<$Res> { + factory _$GetSessionResponseCopyWith(_GetSessionResponse value, $Res Function(_GetSessionResponse) _then) = __$GetSessionResponseCopyWithImpl; +@override @useResult +$Res call({ + GetSessionMessage message,@JsonKey(name: 'home_page') String homePage,@JsonKey(name: 'full_name') String fullName +}); + + +@override $GetSessionMessageCopyWith<$Res> get message; + +} +/// @nodoc +class __$GetSessionResponseCopyWithImpl<$Res> + implements _$GetSessionResponseCopyWith<$Res> { + __$GetSessionResponseCopyWithImpl(this._self, this._then); + + final _GetSessionResponse _self; + final $Res Function(_GetSessionResponse) _then; + +/// Create a copy of GetSessionResponse +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? message = null,Object? homePage = null,Object? fullName = null,}) { + return _then(_GetSessionResponse( +message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable +as GetSessionMessage,homePage: null == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable +as String,fullName: null == fullName ? _self.fullName : fullName // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +/// Create a copy of GetSessionResponse +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$GetSessionMessageCopyWith<$Res> get message { + + return $GetSessionMessageCopyWith<$Res>(_self.message, (value) { + return _then(_self.copyWith(message: value)); + }); +} +} + + /// @nodoc mixin _$SessionData { diff --git a/lib/features/auth/data/models/auth_session_model.g.dart b/lib/features/auth/data/models/auth_session_model.g.dart index f5c4868..e226ab1 100644 --- a/lib/features/auth/data/models/auth_session_model.g.dart +++ b/lib/features/auth/data/models/auth_session_model.g.dart @@ -93,6 +93,57 @@ Map _$AuthSessionResponseToJson( 'full_name': instance.fullName, }; +_GetSessionData _$GetSessionDataFromJson(Map json) => + $checkedCreate('_GetSessionData', json, ($checkedConvert) { + final val = _GetSessionData( + sid: $checkedConvert('sid', (v) => v as String), + csrfToken: $checkedConvert('csrf_token', (v) => v as String), + ); + return val; + }, fieldKeyMap: const {'csrfToken': 'csrf_token'}); + +Map _$GetSessionDataToJson(_GetSessionData instance) => + {'sid': instance.sid, 'csrf_token': instance.csrfToken}; + +_GetSessionMessage _$GetSessionMessageFromJson(Map json) => + $checkedCreate('_GetSessionMessage', json, ($checkedConvert) { + final val = _GetSessionMessage( + data: $checkedConvert( + 'data', + (v) => GetSessionData.fromJson(v as Map), + ), + ); + return val; + }); + +Map _$GetSessionMessageToJson(_GetSessionMessage instance) => + {'data': instance.data.toJson()}; + +_GetSessionResponse _$GetSessionResponseFromJson(Map json) => + $checkedCreate( + '_GetSessionResponse', + json, + ($checkedConvert) { + final val = _GetSessionResponse( + message: $checkedConvert( + 'message', + (v) => GetSessionMessage.fromJson(v as Map), + ), + homePage: $checkedConvert('home_page', (v) => v as String), + fullName: $checkedConvert('full_name', (v) => v as String), + ); + return val; + }, + fieldKeyMap: const {'homePage': 'home_page', 'fullName': 'full_name'}, + ); + +Map _$GetSessionResponseToJson(_GetSessionResponse instance) => + { + 'message': instance.message.toJson(), + 'home_page': instance.homePage, + 'full_name': instance.fullName, + }; + _SessionData _$SessionDataFromJson(Map json) => $checkedCreate( '_SessionData', json, diff --git a/lib/features/auth/domain/entities/city.dart b/lib/features/auth/domain/entities/city.dart new file mode 100644 index 0000000..c194616 --- /dev/null +++ b/lib/features/auth/domain/entities/city.dart @@ -0,0 +1,60 @@ +/// Domain Entity: City +/// +/// Represents a city/province for address selection. +library; + +/// City Entity +class City { + /// Unique city identifier + final String name; + + /// City code (e.g., "01", "02") + final String code; + + /// Display name + final String cityName; + + const City({ + required this.name, + required this.code, + required this.cityName, + }); + + /// Create from JSON map + factory City.fromJson(Map json) { + return City( + name: json['name'] as String, + code: json['code'] as String, + cityName: json['city_name'] as String, + ); + } + + /// Convert to JSON map + Map toJson() { + return { + 'name': name, + 'code': code, + 'city_name': cityName, + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is City && + other.name == name && + other.code == code && + other.cityName == cityName; + } + + @override + int get hashCode { + return Object.hash(name, code, cityName); + } + + @override + String toString() { + return 'City(name: $name, code: $code, cityName: $cityName)'; + } +} diff --git a/lib/features/auth/domain/entities/customer_group.dart b/lib/features/auth/domain/entities/customer_group.dart new file mode 100644 index 0000000..9073b26 --- /dev/null +++ b/lib/features/auth/domain/entities/customer_group.dart @@ -0,0 +1,60 @@ +/// Domain Entity: Customer Group (Role) +/// +/// Represents a customer group/role for user registration. +library; + +/// Customer Group Entity +class CustomerGroup { + /// Unique identifier + final String name; + + /// Display name + final String customerGroupName; + + /// Group value/code + final String? value; + + const CustomerGroup({ + required this.name, + required this.customerGroupName, + this.value, + }); + + /// Create from JSON map + factory CustomerGroup.fromJson(Map json) { + return CustomerGroup( + name: json['name'] as String, + customerGroupName: json['customer_group_name'] as String, + value: json['value'] as String?, + ); + } + + /// Convert to JSON map + Map toJson() { + return { + 'name': name, + 'customer_group_name': customerGroupName, + if (value != null) 'value': value, + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CustomerGroup && + other.name == name && + other.customerGroupName == customerGroupName && + other.value == value; + } + + @override + int get hashCode { + return Object.hash(name, customerGroupName, value); + } + + @override + String toString() { + return 'CustomerGroup(name: $name, customerGroupName: $customerGroupName, value: $value)'; + } +} diff --git a/lib/features/auth/presentation/pages/register_page.dart b/lib/features/auth/presentation/pages/register_page.dart index c441266..f8fb4ce 100644 --- a/lib/features/auth/presentation/pages/register_page.dart +++ b/lib/features/auth/presentation/pages/register_page.dart @@ -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 { 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 _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 { } /// 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 _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 { }); 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)] + : []; + + // Remove null values from certificates list + final validCertificates = certificatesBase64 + .whereType() + .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.delayed(const Duration(seconds: 1)).then((_) => context.goLogin()); + } } catch (e) { if (mounted) { @@ -301,6 +378,14 @@ class _RegisterPageState extends ConsumerState { @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 { ), 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 { ), 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 { // City/Province _buildLabel('Tỉnh/Thành phố *'), - DropdownButtonFormField( - 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 { ], ), ), - ), - ), + ), + ), ); } @@ -712,6 +751,143 @@ class _RegisterPageState extends ConsumerState { ); } + /// Build customer group dropdown + Widget _buildCustomerGroupDropdown() { + final customerGroupsAsync = ref.watch(customerGroupsProvider); + + return customerGroupsAsync.when( + data: (groups) { + return DropdownButtonFormField( + 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( + 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( diff --git a/lib/features/auth/presentation/providers/cities_provider.dart b/lib/features/auth/presentation/providers/cities_provider.dart new file mode 100644 index 0000000..4473e0d --- /dev/null +++ b/lib/features/auth/presentation/providers/cities_provider.dart @@ -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> build() async { + // Don't auto-fetch on build, wait for manual call + return []; + } + + /// Fetch cities from API + Future 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 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 cities) => cities.firstWhere( + (City city) => city.code == code, + orElse: () => cities.first, + ), + ); +} diff --git a/lib/features/auth/presentation/providers/cities_provider.g.dart b/lib/features/auth/presentation/providers/cities_provider.g.dart new file mode 100644 index 0000000..4045898 --- /dev/null +++ b/lib/features/auth/presentation/providers/cities_provider.g.dart @@ -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 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> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + 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 + with $Provider { + /// 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 $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(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 { + 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'; +} diff --git a/lib/features/auth/presentation/providers/customer_groups_provider.dart b/lib/features/auth/presentation/providers/customer_groups_provider.dart new file mode 100644 index 0000000..eca17b1 --- /dev/null +++ b/lib/features/auth/presentation/providers/customer_groups_provider.dart @@ -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> build() async { + // Don't auto-fetch on build, wait for manual call + return []; + } + + /// Fetch customer groups from API + Future 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 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 groups) => groups.firstWhere( + (CustomerGroup group) => group.value == code || group.name == code, + orElse: () => groups.first, + ), + ); +} diff --git a/lib/features/auth/presentation/providers/customer_groups_provider.g.dart b/lib/features/auth/presentation/providers/customer_groups_provider.g.dart new file mode 100644 index 0000000..41ee85d --- /dev/null +++ b/lib/features/auth/presentation/providers/customer_groups_provider.g.dart @@ -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> { + /// 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> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + 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 + with $Provider { + /// 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 $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(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 { + 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'; +} diff --git a/lib/features/auth/presentation/providers/session_provider.dart b/lib/features/auth/presentation/providers/session_provider.dart new file mode 100644 index 0000000..c3c85d9 --- /dev/null +++ b/lib/features/auth/presentation/providers/session_provider.dart @@ -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 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(); + } +} diff --git a/lib/features/auth/presentation/providers/session_provider.g.dart b/lib/features/auth/presentation/providers/session_provider.g.dart new file mode 100644 index 0000000..5b81808 --- /dev/null +++ b/lib/features/auth/presentation/providers/session_provider.g.dart @@ -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 + with $Provider { + /// 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 $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(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 { + /// 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 $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(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 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(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 build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + SessionState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/pubspec.lock b/pubspec.lock index 5623157..6beadff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -273,6 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + curl_logger_dio_interceptor: + dependency: "direct main" + description: + name: curl_logger_dio_interceptor + sha256: f20d89187a321d2150e1412bca30ebf4d89130bafc648ce21bd4f1ef4062b214 + url: "https://pub.dev" + source: hosted + version: "1.0.0" custom_lint: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 0b593da..cc75320 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,6 +51,7 @@ dependencies: dio: ^5.4.3+1 connectivity_plus: ^6.0.3 pretty_dio_logger: ^1.3.1 + curl_logger_dio_interceptor: ^1.0.0 dio_cache_interceptor: ^3.5.0 dio_cache_interceptor_hive_store: ^3.2.2