import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_event.dart'; import 'package:syncrow_web/pages/auth/bloc/auth_state.dart'; import 'package:syncrow_web/pages/auth/model/login_with_email_model.dart'; import 'package:syncrow_web/pages/auth/model/token.dart'; import 'package:syncrow_web/pages/auth/model/user_model.dart'; import 'package:syncrow_web/services/auth_api.dart'; import 'package:syncrow_web/utils/constants/strings_manager.dart'; import 'package:syncrow_web/utils/helpers/shared_preferences_helper.dart'; import 'package:syncrow_web/utils/snack_bar.dart'; class AuthBloc extends Bloc { AuthBloc() : super(LoginInitial()) { on(_login); on(checkBoxToggle); on(changePassword); on(_onStartTimer); on(_onStopTimer); on(_onUpdateTimer); on(_passwordVisible); } ////////////////////////////// forget password ////////////////////////////////// final TextEditingController forgetEmailController = TextEditingController(); final TextEditingController forgetPasswordController = TextEditingController(); final TextEditingController forgetOtp = TextEditingController(); final forgetFormKey = GlobalKey(); Timer? _timer; int _remainingTime = 0; Future _onStartTimer(StartTimerEvent event, Emitter emit) async { if (_validateInputs(emit)) return; print("StartTimerEvent received"); if (_timer != null && _timer!.isActive) { print("Timer is already active"); return; } _remainingTime = 60; add(UpdateTimerEvent( remainingTime: _remainingTime, isButtonEnabled: false)); print("Timer started, initial remaining time: $_remainingTime"); await AuthenticationAPI.sendOtp(email: forgetEmailController.text); _timer = Timer.periodic(const Duration(seconds: 1), (timer) { _remainingTime--; print("Timer tick, remaining time: $_remainingTime"); // Debug print if (_remainingTime <= 0) { _timer?.cancel(); add(const UpdateTimerEvent(remainingTime: 0, isButtonEnabled: true)); print("Timer finished"); // Debug print } else { add(UpdateTimerEvent( remainingTime: _remainingTime, isButtonEnabled: false)); } }); } void _onStopTimer(StopTimerEvent event, Emitter emit) { _timer?.cancel(); emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); } Future changePassword( ChangePasswordEvent event, Emitter emit) async { try { emit(LoadingForgetState()); bool response = await AuthenticationAPI.verifyOtp( email: forgetEmailController.text, otpCode: forgetOtp.text); if (response == true) { await AuthenticationAPI.forgetPassword( password: forgetPasswordController.text, email: forgetEmailController.text); _timer?.cancel(); emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); } emit(SuccessForgetState()); } catch (failure) { emit(FailureForgetState(error: failure.toString())); } } void _onUpdateTimer(UpdateTimerEvent event, Emitter emit) { emit(TimerState( isButtonEnabled: event.isButtonEnabled, remainingTime: event.remainingTime)); } ///////////////////////////////////// login ///////////////////////////////////// final TextEditingController loginEmailController = TextEditingController(); final TextEditingController loginPasswordController = TextEditingController(); final loginFormKey = GlobalKey(); bool isChecked = false; bool obscureText = true; String newPassword = ''; String maskedEmail = ''; String otpCode = ''; static Token token = Token.emptyConstructor(); static UserModel? user; bool showValidationMessage = false; void _login(LoginButtonPressed event, Emitter emit) async { emit(LoginLoading()); if (isChecked) { try { if (event.username.isEmpty || event.password.isEmpty) { CustomSnackBar.displaySnackBar('Please enter your credentials'); emit(const LoginFailure(error: 'Something went wrong')); return; } token = await AuthenticationAPI.loginWithEmail( model: LoginWithEmailModel( email: event.username, password: event.password, ), ); } catch (failure) { emit(const LoginFailure(error: 'Something went wrong')); // emit(LoginFailure(error: failure.toString())); return; } if (token.accessTokenIsNotEmpty) { debugPrint('token: ${token.accessToken}'); FlutterSecureStorage storage = const FlutterSecureStorage(); await storage.write( key: Token.loginAccessTokenKey, value: token.accessToken); const FlutterSecureStorage().write( key: UserModel.userUuidKey, value: Token.decodeToken(token.accessToken)['uuid'].toString()); user = UserModel.fromToken(token); loginEmailController.clear(); loginPasswordController.clear(); emit(LoginSuccess()); } else { emit(const LoginFailure(error: 'Something went wrong')); } } else { emit(const LoginFailure(error: 'Accept terms and condition')); } } checkBoxToggle(CheckBoxEvent event, Emitter emit,) { emit(LoginLoading()); isChecked = event.newValue!; emit(LoginInitial()); } checkOtpCode(ChangePasswordEvent event, Emitter emit,) async { emit(LoadingForgetState()); await AuthenticationAPI.verifyOtp( email: forgetEmailController.text, otpCode: forgetOtp.text); emit(SuccessForgetState()); } void _passwordVisible(PasswordVisibleEvent event, Emitter emit) { emit(LoginLoading()); obscureText = !event.newValue!; emit(PasswordVisibleState()); } void launchURL(String url) { if (kDebugMode) { print('Launching URL: $url'); } } /////////////////////////////////////VALIDATORS///////////////////////////////////// String? validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Password is required'; } else if (value.length < 8) { return 'Password must be at least 8 characters'; } return null; } String? validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Email is required'; } else if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) { return 'Enter a valid email address'; } return null; } String? validateCode(String? value) { if (value == null || value.isEmpty) { return 'Code is required'; } return null; } bool _validateInputs(Emitter emit) { emit(LoadingForgetState()); final nameError = validateEmail(forgetEmailController.text); if (nameError != null) { emit(FailureForgetState(error: nameError)); return true; } return false; } String? validateRegion(String? value) { if (value == null || value.isEmpty) { return 'Please select a region'; } return null; } String? passwordValidator(String? value) { if (value == null || value.isEmpty) { return 'Please enter your password'; } List validationErrors = []; if (!RegExp(r'^(?=.*[a-z])').hasMatch(value)) { validationErrors.add(' - one lowercase letter'); } if (!RegExp(r'^(?=.*[A-Z])').hasMatch(value)) { validationErrors.add(' - one uppercase letter'); } if (!RegExp(r'^(?=.*\d)').hasMatch(value)) { validationErrors.add(' - one number'); } if (!RegExp(r'^(?=.*[@$!%*?&])').hasMatch(value)) { validationErrors.add(' - one special character'); } if (value.length < 8) { validationErrors.add(' - minimum 8 characters'); } if (validationErrors.isNotEmpty) { return 'Password must contain at least:\n${validationErrors.join('\n')}'; } return null; } String? fullNameValidator(String? value) { if (value == null) return 'Full name is required'; final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim(); if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) { return 'Full name must be between 2 and 30 characters long'; } if (RegExp(r"/[^ a-zA-Z0-9-\']/").hasMatch(withoutExtraSpaces)) { return 'Only alphanumeric characters, space, dash and single quote are allowed'; } final parts = withoutExtraSpaces.split(' '); if (parts.length < 2) return 'Full name must contain first and last names'; if (parts.length > 3) return 'Full name can at most contain 3 parts'; if (parts.any((part) => part.length < 2 || part.length > 30)) { return 'Full name parts must be between 2 and 30 characters long'; } return null; } String maskEmail(String email) { final emailParts = email.split('@'); if (emailParts.length != 2) return email; final localPart = emailParts[0]; final domainPart = emailParts[1]; if (localPart.length < 3) return email; final start = localPart.substring(0, 2); final end = localPart.substring(localPart.length - 1); final maskedLocalPart = '$start******$end'; return '$maskedLocalPart@$domainPart'; } final List regions = [ 'North America', 'South America', 'Europe', 'Asia', 'Africa', 'Australia', 'Antarctica', ]; static Future getTokenAndValidate() async { try { const storage = FlutterSecureStorage(); final firstLaunch = await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true; if (firstLaunch) { storage.deleteAll(); } await SharedPreferencesHelper.saveBoolToSP(StringsManager.firstLaunch, false); final value = await storage.read(key: Token.loginAccessTokenKey) ?? ''; if (value.isEmpty) { return 'Token not found'; } final tokenData = Token.decodeToken(value); if (tokenData.containsKey('exp')) { final exp = tokenData['exp'] ?? 0; final currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; if (currentTime < exp) { return 'Success'; } else { return 'expired'; } } else { return 'Something went wrong'; } } catch (_) { return 'Something went wrong'; } } }