Initial commit

This commit is contained in:
jahongireshonqulov
2025-10-18 09:40:06 +05:00
commit 1bf3e41abe
352 changed files with 16315 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
abstract class AuthRemoteDataSource {
Future<LoginResponse> login(LoginRequest request);
Future<SignUpResponse> signUp(SignUpRequest request);
Future<AuthVerificationResponse> verify(AuthVerificationRequest request);
Future<FcmAddResponse> addFcm(FcmAddRequest request);
}

View File

@@ -0,0 +1,105 @@
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
import 'package:dio/dio.dart';
import '../../../../../constants/constants.dart';
import '../../../../../core/error/exceptions.dart';
import '../../../../../core/local_source/local_source.dart';
import '../../../../../injector_container.dart';
import 'auth_remote_data_source.dart';
class AuthRemoteDataSourceImpl extends AuthRemoteDataSource {
final Dio dio;
AuthRemoteDataSourceImpl(this.dio);
@override
Future<LoginResponse> login(LoginRequest request) async {
try {
final Response response = await dio.post(
Constants.baseUrl + Urls.login,
options: DioConstants.options,
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return LoginResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<SignUpResponse> signUp(SignUpRequest request) async {
try {
final Response response = await dio.post(
Constants.baseUrl + Urls.signUp,
options: DioConstants.options,
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return SignUpResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<AuthVerificationResponse> verify(
AuthVerificationRequest request,
) async {
try {
final Response response = await dio.post(
Constants.baseUrl + Urls.verify,
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return AuthVerificationResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
@override
Future<FcmAddResponse> addFcm(FcmAddRequest request) async {
try {
final Response response = await dio.put(
Constants.baseUrl + Urls.addFcm,
options: DioConstants.options
..headers = {
"Authorization": "Bearer ${sl<LocalSource>().getAccessToken()}",
},
data: request,
);
if (response.statusCode == 200 || response.statusCode == 201) {
return FcmAddResponse.fromJson(response.data);
}
throw ServerException.fromJson(response.data);
} on DioException catch (e) {
throw ServerException.fromJson(e.response?.data);
} on FormatException {
throw ServerException(message: Validations.someThingWentWrong);
}
}
}

View File

@@ -0,0 +1,21 @@
class AuthVerificationRequest {
AuthVerificationRequest({this.phoneNumber, this.smsCode, this.email});
AuthVerificationRequest.fromJson(dynamic json) {
phoneNumber = json['phone_number'];
smsCode = json['smsCode'];
email = json['email'];
}
String? phoneNumber;
String? smsCode;
String? email;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['phone_number'] = phoneNumber;
map['smsCode'] = smsCode;
map['email'] = email;
return map;
}
}

View File

@@ -0,0 +1,16 @@
class AuthVerificationResponse {
AuthVerificationResponse({
this.accessToken,});
AuthVerificationResponse.fromJson(dynamic json) {
accessToken = json['access_token'];
}
String? accessToken;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['access_token'] = accessToken;
return map;
}
}

View File

@@ -0,0 +1,16 @@
class FcmAddRequest {
FcmAddRequest({
this.sfmToken,});
FcmAddRequest.fromJson(dynamic json) {
sfmToken = json['sfm_token'];
}
String? sfmToken;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['sfm_token'] = sfmToken;
return map;
}
}

View File

@@ -0,0 +1,51 @@
class FcmAddResponse {
FcmAddResponse({
this.message,
this.user,});
FcmAddResponse.fromJson(dynamic json) {
message = json['message'];
user = json['user'] != null ? User.fromJson(json['user']) : null;
}
String? message;
User? user;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['message'] = message;
if (user != null) {
map['user'] = user?.toJson();
}
return map;
}
}
class User {
User({
this.fullname,
this.phoneNumber,
this.email,
this.sfmToken,});
User.fromJson(dynamic json) {
fullname = json['fullname'];
phoneNumber = json['phone_number'];
email = json['email'];
sfmToken = json['sfm_token'];
}
String? fullname;
String? phoneNumber;
String? email;
String? sfmToken;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['fullname'] = fullname;
map['phone_number'] = phoneNumber;
map['email'] = email;
map['sfm_token'] = sfmToken;
return map;
}
}

View File

@@ -0,0 +1,24 @@
class LoginRequest {
LoginRequest({
this.email,
this.phone,
this.ucode,});
LoginRequest.fromJson(dynamic json) {
email = json['email'];
phone = json['phone'];
ucode = json['ucode'];
}
String? email;
String? phone;
String? ucode;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['email'] = email;
map['phone'] = phone;
map['ucode'] = ucode;
return map;
}
}

View File

@@ -0,0 +1,20 @@
class LoginResponse {
LoginResponse({
this.token,
this.refreshToken,});
LoginResponse.fromJson(dynamic json) {
token = json['token'];
refreshToken = json['refreshToken'];
}
String? token;
String? refreshToken;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['token'] = token;
map['refreshToken'] = refreshToken;
return map;
}
}

View File

@@ -0,0 +1,25 @@
class SignUpRequest {
SignUpRequest({
this.phoneNumber,
this.email,
this.fullname,});
SignUpRequest.fromJson(dynamic json) {
phoneNumber = json['phone_number'];
email = json['email'];
fullname = json['fullname'];
}
String? phoneNumber;
String? email;
String? fullname;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['phone_number'] = phoneNumber;
map['email'] = email;
map['fullname'] = fullname;
return map;
}
}

View File

@@ -0,0 +1,16 @@
class SignUpResponse {
SignUpResponse({
this.token,});
SignUpResponse.fromJson(dynamic json) {
token = json['token'];
}
String? token;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['token'] = token;
return map;
}
}

View File

@@ -0,0 +1,73 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
import 'package:dartz/dartz.dart';
import '../../../../core/error/exceptions.dart';
import '../../domain/repository/auth_repository.dart';
import '../data_source/remote/auth_remote_data_source.dart';
class AuthRepositoryImpl extends AuthRepository {
final AuthRemoteDataSource remoteDataSource;
AuthRepositoryImpl(this.remoteDataSource);
@override
Future<Either<Failure, LoginResponse>> login(LoginRequest request) async {
try {
final response = await remoteDataSource.login(request);
return Right(response);
} catch (e) {
if (e is ServerException) {
return Left(ServerFailure(message: e.message));
}
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, SignUpResponse>> signUp(SignUpRequest request) async {
try {
final response = await remoteDataSource.signUp(request);
return Right(response);
} catch (e) {
if (e is ServerException) {
return Left(ServerFailure(message: e.message));
}
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, AuthVerificationResponse>> verify(
AuthVerificationRequest request,
) async {
try {
final response = await remoteDataSource.verify(request);
return Right(response);
} catch (e) {
if (e is ServerException) {
return Left(ServerFailure(message: e.message));
}
return Left(ServerFailure(message: e.toString()));
}
}
@override
Future<Either<Failure, FcmAddResponse>> fcmAdd(FcmAddRequest request) async {
try {
final response = await remoteDataSource.addFcm(request);
return Right(response);
} catch (e) {
if (e is ServerException) {
return Left(ServerFailure(message: e.message));
}
return Left(ServerFailure(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,23 @@
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
import 'package:dartz/dartz.dart';
import '../../../../core/error/failure.dart';
abstract class AuthRepository {
Future<Either<Failure, LoginResponse>> login(LoginRequest request);
Future<Either<Failure, SignUpResponse>> signUp(SignUpRequest request);
Future<Either<Failure, AuthVerificationResponse>> verify(
AuthVerificationRequest request,
);
Future<Either<Failure, FcmAddResponse>> fcmAdd(FcmAddRequest request);
}

View File

@@ -0,0 +1,18 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/fcm_add_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/auth_repository.dart';
class FcmAddUseCase extends UseCase<FcmAddResponse, FcmAddRequest> {
final AuthRepository repository;
FcmAddUseCase(this.repository);
@override
Future<Either<Failure, FcmAddResponse>> call(FcmAddRequest params) async {
return await repository.fcmAdd(params);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/login_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/auth_repository.dart';
class LoginUseCase extends UseCase<LoginResponse, LoginRequest> {
final AuthRepository repository;
LoginUseCase(this.repository);
@override
Future<Either<Failure, LoginResponse>> call(LoginRequest params) async {
final response = await repository.login(params);
return response;
}
}

View File

@@ -0,0 +1,19 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/sign_up_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/auth_repository.dart';
class SignUpUseCase extends UseCase<SignUpResponse, SignUpRequest> {
final AuthRepository repository;
SignUpUseCase(this.repository);
@override
Future<Either<Failure, SignUpResponse>> call(SignUpRequest params) async {
final response = await repository.signUp(params);
return response;
}
}

View File

@@ -0,0 +1,22 @@
import 'package:cargocalculaterapp/core/error/failure.dart';
import 'package:cargocalculaterapp/core/usecase/usecase.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_request.dart';
import 'package:cargocalculaterapp/features/auth/data/model/auth_verification_response.dart';
import 'package:dartz/dartz.dart';
import '../repository/auth_repository.dart';
class VerifyPhoneUseCase
extends UseCase<AuthVerificationResponse, AuthVerificationRequest> {
final AuthRepository repository;
VerifyPhoneUseCase(this.repository);
@override
Future<Either<Failure, AuthVerificationResponse>> call(
AuthVerificationRequest params,
) async {
final response = await repository.verify(params);
return response;
}
}

View File

@@ -0,0 +1,116 @@
import 'package:cargocalculaterapp/core/local_source/local_source.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:cargocalculaterapp/router/app_routes.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../constants/constants.dart';
import '../../../../../injector_container.dart';
import '../../../../../router/name_routes.dart';
import '../../../data/model/login_request.dart';
import '../../../domain/usecases/login_usecase.dart';
import '../../pages/auth_confirm/argument/auth_confirm_argument.dart';
part 'auth_event.dart';
part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc(this.loginUseCase)
: super(
const AuthState(
passwordHidden: true,
isLoading: false,
isButtonEnabled: false,
),
) {
on<PasswordVisibilityEvent>(_showHidePassword);
on<OnInputEnterEvent>(_onInputEnter);
on<SubmitEvent>(_onSubmit);
}
final LoginUseCase loginUseCase;
void _showHidePassword(
PasswordVisibilityEvent event,
Emitter<AuthState> emit,
) {
emit(state.copyWith(passwordHidden: !state.passwordHidden));
}
void _onInputEnter(OnInputEnterEvent event, Emitter<AuthState> emit) {
emit(
state.copyWith(
isButtonEnabled:
(isEmail(event.login) || isPhoneNumber(event.login)) &&
event.password.isNotEmpty,
),
);
}
Future<void> _onSubmit(SubmitEvent event, Emitter<AuthState> emit) async {
emit(state.copyWith(isLoading: true));
final response = await loginUseCase(
LoginRequest(
email: isEmail(event.login) ? event.login : "",
phone: isPhoneNumber(event.login)
? event.login.replaceAll("+", "")
: "",
ucode: event.password,
),
);
response.fold(
(l) {
emit(state.copyWith(isLoading: false));
showErrorSnackBar();
},
(r) {
emit(state.copyWith(isLoading: false));
sl<LocalSource>().setUCode(event.password);
Navigator.pushNamed(
rootNavigatorKey.currentContext!,
Routes.authConfirm,
arguments: AuthConfirmArgument(
fullName: "",
mail: isEmail(event.login) ? event.login : "",
phoneNumber: isPhoneNumber(event.login) ? event.login : "",
fromLoginPage: true,
password: event.password,
),
);
},
);
}
bool isEmail(String input) {
return RegExConst.emailRegex.hasMatch(input);
}
bool isPhoneNumber(String input) {
return RegExConst.phoneRegex.hasMatch(input);
}
void showErrorSnackBar() {
final snackBar = SnackBar(
backgroundColor: Colors.red,
content: Row(
children: [
const Icon(Icons.error, color: Colors.white),
const SizedBox(width: 8),
Expanded(
child: Text(
AppLocalization.current.login_error,
style: const TextStyle(color: Colors.white),
),
),
],
),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 3),
);
ScaffoldMessenger.of(
rootNavigatorKey.currentContext!,
).showSnackBar(snackBar);
}
}

View File

@@ -0,0 +1,32 @@
part of 'auth_bloc.dart';
sealed class AuthEvent extends Equatable {
const AuthEvent();
}
final class PasswordVisibilityEvent extends AuthEvent {
const PasswordVisibilityEvent();
@override
List<Object?> get props => [];
}
final class OnInputEnterEvent extends AuthEvent {
final String login;
final String password;
const OnInputEnterEvent({required this.login, required this.password});
@override
List<Object?> get props => [login, password];
}
final class SubmitEvent extends AuthEvent {
final String login;
final String password;
const SubmitEvent({required this.login, required this.password});
@override
List<Object?> get props => [login, password];
}

View File

@@ -0,0 +1,28 @@
part of 'auth_bloc.dart';
class AuthState extends Equatable {
const AuthState({
required this.passwordHidden,
required this.isLoading,
required this.isButtonEnabled,
});
final bool passwordHidden;
final bool isLoading;
final bool isButtonEnabled;
AuthState copyWith({
bool? passwordHidden,
bool? isLoading,
bool? isButtonEnabled,
}) {
return AuthState(
isLoading: isLoading ?? this.isLoading,
passwordHidden: passwordHidden ?? this.passwordHidden,
isButtonEnabled: isButtonEnabled ?? this.isButtonEnabled,
);
}
@override
List<Object?> get props => [passwordHidden, isLoading, isButtonEnabled];
}

View File

@@ -0,0 +1,112 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../core/error/failure.dart';
import '../../../../../core/functions/base_finctions.dart';
import '../../../../../core/local_source/local_source.dart';
import '../../../../../injector_container.dart';
import '../../../../../service/notification_service.dart';
import '../../../data/model/auth_verification_request.dart';
import '../../../data/model/fcm_add_request.dart';
import '../../../data/model/login_request.dart';
import '../../../data/model/sign_up_request.dart';
import '../../../domain/usecases/fcm_add_usecase.dart';
import '../../../domain/usecases/login_usecase.dart';
import '../../../domain/usecases/sign_up_usecase.dart';
import '../../../domain/usecases/verify_phone_usecase.dart';
import '../../pages/auth_confirm/argument/auth_confirm_argument.dart';
part 'auth_confirm_event.dart';
part 'auth_confirm_state.dart';
class AuthConfirmBloc extends Bloc<AuthConfirmEvent, AuthConfirmState> {
AuthConfirmBloc(
this.signUpUseCase,
this.verifyPhoneUseCase,
this.loginUseCase,
this.fcmAddUseCase,
) : super(const InitialState(true)) {
on<TimerChangedEvent>(_timerChanged);
on<SmsCodeEnterEvent>(_codeChanged);
on<ResendCodeEvent>(_resendCode);
on<OnSubmitEvent>(_onSubmitCode);
}
final SignUpUseCase signUpUseCase;
final VerifyPhoneUseCase verifyPhoneUseCase;
final LoginUseCase loginUseCase;
final FcmAddUseCase fcmAddUseCase;
Future<void> _timerChanged(
TimerChangedEvent event,
Emitter<AuthConfirmState> emit,
) async {
emit(InitialState(event.isVisible));
}
void _codeChanged(SmsCodeEnterEvent event, Emitter<AuthConfirmState> emit) {
if (event.code.length == 4) {
emit(ButtonEnableState(state.isTimerVisible, event.code));
} else {
emit(InitialState(state.isTimerVisible));
}
}
Future<void> _resendCode(
ResendCodeEvent event,
Emitter<AuthConfirmState> emit,
) async {
add(const TimerChangedEvent(isVisible: true));
if (event.argument.fromLoginPage) {
await loginUseCase(
LoginRequest(
email: event.argument.mail,
phone: event.argument.phoneNumber?.replaceAll("+", ""),
ucode: event.argument.password,
),
);
} else {
await signUpUseCase(
SignUpRequest(
fullname: event.argument.fullName,
email: event.argument.mail,
phoneNumber: event.argument.phoneNumber?.replaceAll("+", ""),
),
);
}
}
Future<void> _onSubmitCode(
OnSubmitEvent event,
Emitter<AuthConfirmState> emit,
) async {
emit(LoadingState(state.isTimerVisible));
final response = await verifyPhoneUseCase(
AuthVerificationRequest(
phoneNumber: event.argument?.phoneNumber?.replaceAll("+", ""),
smsCode: event.code,
email: event.argument?.mail ?? "",
),
);
await response.fold(
(l) {
if (l is ServerFailure) {
Functions.showErrorSnackBar(l.message);
}
emit(InitialState(state.isTimerVisible));
},
(r) async {
sl<LocalSource>().setHasProfile(true);
sl<LocalSource>().setAccessToken(r.accessToken ?? "");
await _addFcmToken();
emit(SuccessSate(state.isTimerVisible));
},
);
}
Future<void> _addFcmToken() async {
final String fcmToken = await NotificationService.getFcmToken();
await fcmAddUseCase(FcmAddRequest(sfmToken: fcmToken));
}
}

View File

@@ -0,0 +1,42 @@
part of 'auth_confirm_bloc.dart';
sealed class AuthConfirmEvent extends Equatable {
const AuthConfirmEvent();
}
final class SmsCodeEnterEvent extends AuthConfirmEvent {
final String code;
const SmsCodeEnterEvent(this.code);
@override
List<Object?> get props => [code];
}
final class TimerChangedEvent extends AuthConfirmEvent {
final bool isVisible;
const TimerChangedEvent({required this.isVisible});
@override
List<Object?> get props => [isVisible];
}
final class OnSubmitEvent extends AuthConfirmEvent {
final String code;
final AuthConfirmArgument? argument;
const OnSubmitEvent({required this.code, required this.argument});
@override
List<Object?> get props => [code, argument];
}
final class ResendCodeEvent extends AuthConfirmEvent {
final AuthConfirmArgument argument;
const ResendCodeEvent({required this.argument});
@override
List<Object?> get props => [argument];
}

View File

@@ -0,0 +1,44 @@
part of 'auth_confirm_bloc.dart';
sealed class AuthConfirmState extends Equatable {
const AuthConfirmState(this.isTimerVisible);
final bool isTimerVisible;
}
class InitialState extends AuthConfirmState {
const InitialState(super.isTimerVisible);
@override
List<Object?> get props => [super.isTimerVisible];
}
class LoadingState extends AuthConfirmState {
const LoadingState(super.isTimerVisible);
@override
List<Object?> get props => [super.isTimerVisible];
}
class SuccessSate extends AuthConfirmState {
const SuccessSate(super.isTimerVisible);
@override
List<Object?> get props => [super.isTimerVisible];
}
class ErrorState extends AuthConfirmState {
const ErrorState(super.isTimerVisible);
@override
List<Object?> get props => [super.isTimerVisible];
}
class ButtonEnableState extends AuthConfirmState {
const ButtonEnableState(super.isTimerVisible, this.code);
final String code;
@override
List<Object?> get props => [super.isTimerVisible, code];
}

View File

@@ -0,0 +1,95 @@
import 'package:cargocalculaterapp/core/local_source/local_source.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:cargocalculaterapp/router/app_routes.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../constants/constants.dart';
import '../../../../../core/error/failure.dart';
import '../../../../../core/functions/base_finctions.dart';
import '../../../../../injector_container.dart';
import '../../../../../router/name_routes.dart';
import '../../../data/model/sign_up_request.dart';
import '../../../domain/usecases/sign_up_usecase.dart';
import '../../pages/auth_confirm/argument/auth_confirm_argument.dart';
part 'sign_up_event.dart';
part 'sign_up_state.dart';
class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
SignUpBloc(this.signUpUseCase)
: super(
const SignUpState(
isLoading: false,
isButtonEnabled: false,
passwordHidden: true,
),
) {
on<PasswordVisibilityEvent>(_passwordVisibility);
on<OnInputEnterEvent>(_onDataEnter);
on<SubmitEvent>(_signUp);
}
final SignUpUseCase signUpUseCase;
void _passwordVisibility(
PasswordVisibilityEvent event,
Emitter<SignUpState> emit,
) {
emit(state.copyWith(isButtonEnabled: !state.isButtonEnabled));
}
void _onDataEnter(OnInputEnterEvent event, Emitter<SignUpState> emit) {
emit(
state.copyWith(
isButtonEnabled:
(isEmail(event.login) || isPhoneNumber(event.login)) &&
event.fullName.isNotEmpty,
),
);
}
bool isEmail(String input) {
return RegExConst.emailRegex.hasMatch(input);
}
bool isPhoneNumber(String input) {
return RegExConst.phoneRegex.hasMatch(input);
}
Future<void> _signUp(SubmitEvent event, Emitter<SignUpState> emit) async {
emit(state.copyWith(isLoading: true));
final response = await signUpUseCase(
SignUpRequest(
fullname: event.fullName,
email: isEmail(event.login) ? event.login : "",
phoneNumber: isPhoneNumber(event.login)
? event.login.replaceAll("+", "")
: "",
),
);
response.fold(
(l) {
emit(state.copyWith(isLoading: false));
if (l is ServerFailure) {
Functions.showErrorSnackBar(AppLocalization.current.phone_registered);
}
},
(r) {
sl<LocalSource>().setAccessToken(r.token ?? "");
emit(state.copyWith(isLoading: false));
Navigator.pushNamed(
rootNavigatorKey.currentContext!,
Routes.authConfirm,
arguments: AuthConfirmArgument(
fullName: event.fullName,
mail: isEmail(event.login) ? event.login : "",
phoneNumber: isPhoneNumber(event.login) ? event.login : "",
fromLoginPage: false,
),
);
},
);
}
}

View File

@@ -0,0 +1,32 @@
part of 'sign_up_bloc.dart';
sealed class SignUpEvent extends Equatable {
const SignUpEvent();
}
final class PasswordVisibilityEvent extends SignUpEvent {
const PasswordVisibilityEvent();
@override
List<Object?> get props => [];
}
final class OnInputEnterEvent extends SignUpEvent {
final String login;
final String fullName;
const OnInputEnterEvent({required this.login, required this.fullName});
@override
List<Object?> get props => [login, fullName];
}
final class SubmitEvent extends SignUpEvent {
final String login;
final String fullName;
const SubmitEvent({required this.login, required this.fullName});
@override
List<Object?> get props => [login, fullName];
}

View File

@@ -0,0 +1,28 @@
part of 'sign_up_bloc.dart';
class SignUpState extends Equatable {
const SignUpState({
required this.passwordHidden,
required this.isLoading,
required this.isButtonEnabled,
});
final bool passwordHidden;
final bool isLoading;
final bool isButtonEnabled;
SignUpState copyWith({
bool? passwordHidden,
bool? isLoading,
bool? isButtonEnabled,
}) {
return SignUpState(
isLoading: isLoading ?? this.isLoading,
passwordHidden: passwordHidden ?? this.passwordHidden,
isButtonEnabled: isButtonEnabled ?? this.isButtonEnabled,
);
}
@override
List<Object?> get props => [passwordHidden, isLoading, isButtonEnabled];
}

View File

@@ -0,0 +1,185 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../../../core/widgets/loading/custom_loading.dart';
import '../../../../../core/widgets/text_filds/custom_text_field_name.dart';
import '../../../../../router/name_routes.dart';
import '../../bloc/auth/auth_bloc.dart';
import '../mixin/auth_mixin.dart';
class AuthPage extends StatefulWidget {
const AuthPage({super.key});
@override
State<AuthPage> createState() => _AuthPageState();
}
class _AuthPageState extends State<AuthPage> with AuthMixin {
@override
void initState() {
initControllers();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(),
body: ListView(
padding: AppUtils.kPaddingHor16,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SvgPicture.asset("assets/svg/ic_logo_auth.svg"),
),
AppUtils.kBoxHeight48,
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
borderRadius: AppUtils.kBorderRadius12,
color: context.color.iconBackground,
),
padding: AppUtils.kPaddingAll6,
child: SvgPicture.asset("assets/svg/ic_user.svg"),
),
AppUtils.kBoxWidth12,
Text(
AppLocalization.current.auth_login,
style: context.text.authTitle,
),
],
),
AppUtils.kBoxHeight32,
CustomTextFieldName(
hint: AppLocalization.current.enter_phone_or_mail,
inputType: TextInputType.text,
name: AppLocalization.current.phone,
controller: loginController,
onchange: (value) {
context.read<AuthBloc>().add(
OnInputEnterEvent(
login: value,
password: passwordController.text,
),
);
},
),
AppUtils.kBoxHeight16,
CustomTextFieldName(
hint: "********",
prefixWidget: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"CT-",
style: context.text.profileCategory.copyWith(
fontWeight: FontWeight.w400,
),
),
],
),
inputType: TextInputType.number,
name: AppLocalization.current.password,
obscureText: state.passwordHidden,
maxLines: 1,
controller: passwordController,
suffix: IconButton(
onPressed: () {
context.read<AuthBloc>().add(
const PasswordVisibilityEvent(),
);
},
icon: Icon(
state.passwordHidden
? Icons.visibility_off_rounded
: Icons.visibility_rounded,
),
),
onchange: (value) {
context.read<AuthBloc>().add(
OnInputEnterEvent(
login: loginController.text,
password: value,
),
);
},
),
AppUtils.kBoxHeight48,
Padding(
padding: AppUtils.kPaddingHor34,
child: ElevatedButton(
onPressed: state.isButtonEnabled && !state.isLoading
? () {
context.read<AuthBloc>().add(
SubmitEvent(
login: loginController.text,
password: "CT-${passwordController.text}",
),
);
}
: null,
child: state.isLoading
? const CustomLoadingWidget()
: Text(AppLocalization.current.auth),
),
),
AppUtils.kBoxHeight16,
Text(
AppLocalization.current.no_account,
style: context.text.authDesc,
textAlign: TextAlign.center,
),
AppUtils.kBoxHeight16,
Padding(
padding: AppUtils.kPaddingHor34,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Navigator.pushNamed(context, Routes.signUp);
},
borderRadius: AppUtils.kBorderRadius24,
child: Ink(
decoration: BoxDecoration(
color: context.color.scaffoldBackgroundColor,
borderRadius: AppUtils.kBorderRadius24,
border: Border.all(color: context.color.primaryColor),
),
height: 56,
padding: AppUtils.kPaddingHor16,
child: Center(
child: Text(
AppLocalization.current.sign_up,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: context.color.textColor,
),
),
),
),
),
),
),
],
),
);
},
);
}
@override
void dispose() {
disposeControllers();
super.dispose();
}
}

View File

@@ -0,0 +1,15 @@
class AuthConfirmArgument {
final String? fullName;
final String? mail;
final String? phoneNumber;
final bool fromLoginPage;
final String? password;
AuthConfirmArgument({
this.fullName,
required this.mail,
required this.phoneNumber,
required this.fromLoginPage,
this.password,
});
}

View File

@@ -0,0 +1,233 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/core/utils/app_utils.dart';
import 'package:cargocalculaterapp/core/widgets/loading/custom_loading.dart';
import 'package:cargocalculaterapp/features/auth/presentation/pages/auth_confirm/timer_widget.dart';
import 'package:cargocalculaterapp/generated/l10n.dart';
import 'package:cargocalculaterapp/router/name_routes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:pin_code_fields/pin_code_fields.dart';
import '../../../../../core/theme/app_text_styles.dart';
import '../../../../../core/theme/colors/app_colors.dart';
import '../../bloc/auth_confirm/auth_confirm_bloc.dart';
import '../mixin/auth_confirm_mixin.dart';
import 'argument/auth_confirm_argument.dart';
class AuthConfirmPage extends StatefulWidget {
const AuthConfirmPage({super.key, required this.argument});
final AuthConfirmArgument? argument;
@override
State<AuthConfirmPage> createState() => _AuthConfirmPageState();
}
class _AuthConfirmPageState extends State<AuthConfirmPage>
with AuthConfirmMixin {
@override
void initState() {
initControllers();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocConsumer<AuthConfirmBloc, AuthConfirmState>(
listener: (context, state) {
if (state is SuccessSate) {
Navigator.pushNamedAndRemoveUntil(
context,
Routes.main,
(route) => route.isFirst,
);
}
},
builder: (context, state) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: AppUtils.kPaddingAll16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
borderRadius: AppUtils.kBorderRadius16,
color: context.color.iconBackground,
),
padding: AppUtils.kPaddingAll16,
child: SvgPicture.asset(
"assets/svg/ic_mail.svg",
width: 32,
height: 32,
),
),
AppUtils.kBoxHeight16,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
(widget.argument?.mail?.isNotEmpty ?? false)
? AppLocalization.current.confirm_email
: AppLocalization.current.confirm_phone_text,
style: context.text.authTitle,
textAlign: TextAlign.center,
),
),
AppUtils.kBoxHeight16,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
(widget.argument?.mail?.isNotEmpty ?? false)
? AppLocalization.current.enter_code_mail(
widget.argument?.mail ?? "",
)
: AppLocalization.current.enter_code_phone(
widget.argument?.phoneNumber ?? "",
),
style: context.text.authDesc,
textAlign: TextAlign.center,
),
),
AppUtils.kBoxHeight48,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 296,
child: PinCodeTextField(
controller: confirmController,
appContext: context,
autoDisposeControllers: false,
length: 4,
animationType: AnimationType.scale,
enabled: true,
enablePinAutofill: true,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
onChanged: (code) {
context.read<AuthConfirmBloc>().add(
SmsCodeEnterEvent(code),
);
},
autoFocus: true,
keyboardType: const TextInputType.numberWithOptions(),
showCursor: true,
textStyle: context.text.bigTitle,
cursorColor: context.color.accentColor,
cursorHeight: 20,
enableActiveFill: true,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
pinTheme: PinTheme(
activeColor:
state is ErrorState
? ThemeColors.timerRed
: state is SuccessSate
? ThemeColors.successInputSMS
: context.color.borderColor,
selectedFillColor:
context.color.scaffoldBackgroundColor,
activeFillColor:
context.color.scaffoldBackgroundColor,
shape: PinCodeFieldShape.box,
inactiveFillColor:
context.color.scaffoldBackgroundColor,
borderRadius: const BorderRadius.all(
Radius.circular(12),
),
borderWidth: 1,
inactiveColor:
state is ErrorState
? ThemeColors.timerRed
: state is SuccessSate
? ThemeColors.successInputSMS
: context.color.borderColor,
fieldWidth: 56,
fieldHeight: 64,
selectedColor:
state is ErrorState
? ThemeColors.timerRed
: state is SuccessSate
? ThemeColors.successInputSMS
: context.color.primaryColor,
),
),
),
],
),
if (state is ErrorState) AppUtils.kBoxHeight16,
if (state is ErrorState)
Center(
child: Text(
AppLocalization.current.incorrect_code,
style: AppTextStyles.timerBlue,
),
),
AppUtils.kBoxHeight24,
if (state.isTimerVisible)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
AppLocalization.current.send_sms,
style: AppTextStyles.timerBlue,
),
const TimerWidget(),
],
),
if (!state.isTimerVisible)
Center(
child: GestureDetector(
onTap: () {
if (widget.argument != null) {
context.read<AuthConfirmBloc>().add(
ResendCodeEvent(argument: widget.argument!),
);
}
},
child: Text(
AppLocalization.current.send_sms,
style: AppTextStyles.timerBlue,
),
),
),
AppUtils.kBoxHeight48,
Padding(
padding: AppUtils.kPaddingHor34,
child: ElevatedButton(
onPressed:
(confirmController.text.length == 4 &&
state is! LoadingState)
? () {
context.read<AuthConfirmBloc>().add(
OnSubmitEvent(
code: confirmController.text,
argument: widget.argument,
),
);
}
: null,
child:
state is LoadingState
? const CustomLoadingWidget()
: Text(AppLocalization.current.confirm),
),
),
],
),
),
);
},
);
}
@override
void dispose() {
disposeController();
super.dispose();
}
}

View File

@@ -0,0 +1,49 @@
import 'dart:async';
import 'package:cargocalculaterapp/core/theme/app_text_styles.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/auth_confirm/auth_confirm_bloc.dart';
class TimerWidget extends StatefulWidget {
const TimerWidget({super.key});
@override
State<TimerWidget> createState() => _TimerWidgetState();
}
class _TimerWidgetState extends State<TimerWidget> {
Timer? timer;
int time = 60;
String timeText = "60";
@override
void initState() {
if (timer?.isActive ?? false) {
timer?.cancel();
}
timer = Timer.periodic(const Duration(seconds: 1), (Timer timer) async {
time--;
if (time == 0) {
timer.cancel();
context.read<AuthConfirmBloc>().add(
const TimerChangedEvent(isVisible: false),
);
}
setState(() {
timeText = time.toString().padLeft(2, "0");
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Text(" 00:$timeText", style: AppTextStyles.timerStyle);
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/cupertino.dart';
mixin AuthConfirmMixin {
late TextEditingController confirmController;
void initControllers() {
confirmController = TextEditingController();
}
void disposeController() {
confirmController.dispose();
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/cupertino.dart';
mixin AuthMixin {
late TextEditingController loginController;
late TextEditingController passwordController;
void initControllers() {
loginController = TextEditingController();
passwordController = TextEditingController();
}
void disposeControllers() {
loginController.dispose();
passwordController.dispose();
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/cupertino.dart';
mixin SignUpMixin {
late TextEditingController loginController;
late TextEditingController fullNameController;
void initControllers() {
loginController = TextEditingController();
fullNameController = TextEditingController();
}
void disposeControllers() {
loginController.dispose();
fullNameController.dispose();
}
}

View File

@@ -0,0 +1,158 @@
import 'package:cargocalculaterapp/core/extension/build_context_extension.dart';
import 'package:cargocalculaterapp/features/auth/presentation/pages/mixin/sign_up_mixin.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../../core/utils/app_utils.dart';
import '../../../../../core/widgets/loading/custom_loading.dart';
import '../../../../../core/widgets/text_filds/custom_text_field_name.dart';
import '../../../../../generated/l10n.dart';
import '../../bloc/sign_up/sign_up_bloc.dart';
class SignUpPage extends StatefulWidget {
const SignUpPage({super.key});
@override
State<SignUpPage> createState() => _SignUpPageState();
}
class _SignUpPageState extends State<SignUpPage> with SignUpMixin {
@override
void initState() {
initControllers();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<SignUpBloc, SignUpState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(),
body: ListView(
padding: AppUtils.kPaddingAll16,
children: [
Center(
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
borderRadius: AppUtils.kBorderRadius16,
color: context.color.iconBackground,
),
padding: AppUtils.kPaddingAll16,
child: SvgPicture.asset(
"assets/svg/ic_add_user.svg",
width: 32,
height: 32,
),
),
),
AppUtils.kBoxHeight16,
Text(
AppLocalization.current.sign_up,
style: context.text.authTitle,
textAlign: TextAlign.center,
),
AppUtils.kBoxHeight24,
CustomTextFieldName(
hint: AppLocalization.current.full_name_hint,
inputType: TextInputType.name,
name: AppLocalization.current.full_name,
controller: fullNameController,
onchange: (value) {
context.read<SignUpBloc>().add(
OnInputEnterEvent(
login: loginController.text,
fullName: value,
),
);
},
),
AppUtils.kBoxHeight16,
CustomTextFieldName(
hint: AppLocalization.current.enter_phone_or_mail,
inputType: TextInputType.text,
name: AppLocalization.current.phone,
controller: loginController,
onchange: (value) {
context.read<SignUpBloc>().add(
OnInputEnterEvent(
login: value,
fullName: fullNameController.text,
),
);
},
),
AppUtils.kBoxHeight48,
Padding(
padding: AppUtils.kPaddingHor34,
child: ElevatedButton(
onPressed:
state.isButtonEnabled && !state.isLoading
? () {
context.read<SignUpBloc>().add(
SubmitEvent(
login: loginController.text,
fullName: fullNameController.text,
),
);
}
: null,
child:
state.isLoading
? const CustomLoadingWidget()
: Text(AppLocalization.current.sign_up),
),
),
AppUtils.kBoxHeight16,
Text(
AppLocalization.current.has_account,
style: context.text.authDesc,
textAlign: TextAlign.center,
),
AppUtils.kBoxHeight16,
Padding(
padding: AppUtils.kPaddingHor34,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Navigator.pop(context);
},
borderRadius: AppUtils.kBorderRadius24,
child: Ink(
decoration: BoxDecoration(
color: context.color.scaffoldBackgroundColor,
borderRadius: AppUtils.kBorderRadius24,
border: Border.all(color: context.color.primaryColor),
),
height: 56,
padding: AppUtils.kPaddingHor16,
child: Center(
child: Text(
AppLocalization.current.auth,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: context.color.textColor,
),
),
),
),
),
),
),
],
),
);
},
);
}
@override
void dispose() {
disposeControllers();
super.dispose();
}
}