import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import 'package:equatable/equatable.dart'; import '../../../../data/network/api_exception.dart'; import '../../../../core/l10n/l10n.dart'; import '../../data/repositories/auth_repository.dart'; import '../../data/models/auth_response.dart'; import '../../../../shared/forms/inputs.dart'; class LoginState extends Equatable { static const defaultDialCode = '+86'; static final _e164Regex = RegExp(r'^\+[1-9]\d{7,14}$'); final String dialCode; final Phone phone; final VerificationCode code; final bool codeSent; final bool isSendingCode; final int resendCooldownSeconds; final FormzSubmissionStatus status; final String? errorMessage; const LoginState({ this.dialCode = defaultDialCode, this.phone = const Phone.pure(), this.code = const VerificationCode.pure(), this.codeSent = false, this.isSendingCode = false, this.resendCooldownSeconds = 0, this.status = FormzSubmissionStatus.initial, this.errorMessage, }); bool get isPhoneValidForApi => phone.isValid && _e164Regex.hasMatch(e164Phone); bool get isValid => isPhoneValidForApi && code.isValid; String get e164Phone => '$dialCode${phone.value}'; bool get canSendCode => isPhoneValidForApi && !isSendingCode && resendCooldownSeconds == 0; LoginState copyWith({ String? dialCode, Phone? phone, VerificationCode? code, bool? codeSent, bool? isSendingCode, int? resendCooldownSeconds, FormzSubmissionStatus? status, String? errorMessage, }) { return LoginState( dialCode: dialCode ?? this.dialCode, phone: phone ?? this.phone, code: code ?? this.code, codeSent: codeSent ?? this.codeSent, isSendingCode: isSendingCode ?? this.isSendingCode, resendCooldownSeconds: resendCooldownSeconds ?? this.resendCooldownSeconds, status: status ?? this.status, errorMessage: errorMessage, ); } @override List get props => [ dialCode, phone, code, codeSent, isSendingCode, resendCooldownSeconds, status, errorMessage, ]; } class LoginCubit extends Cubit { final AuthRepository _repository; Timer? _resendTimer; LoginCubit(this._repository) : super(const LoginState()); void phoneChanged(String value) { final nextPhone = Phone.dirty(value); if (nextPhone.value == state.phone.value) { emit(state.copyWith(phone: nextPhone, errorMessage: null)); return; } _resendTimer?.cancel(); emit( state.copyWith( phone: nextPhone, code: const VerificationCode.pure(), codeSent: false, resendCooldownSeconds: 0, errorMessage: null, ), ); } void dialCodeChanged(String value) { if (value == state.dialCode) { return; } _resendTimer?.cancel(); emit( state.copyWith( dialCode: value, code: const VerificationCode.pure(), codeSent: false, resendCooldownSeconds: 0, errorMessage: null, ), ); } void codeChanged(String value) { emit(state.copyWith(code: VerificationCode.dirty(value))); } Future sendCode() async { if (!state.phone.isValid) { emit(state.copyWith(errorMessage: L10n.current.authInvalidPhone)); return false; } if (!state.canSendCode) { return false; } final requestPhone = state.e164Phone; emit(state.copyWith(isSendingCode: true, errorMessage: null)); _startResendCooldown(); try { await _repository.sendOtp(requestPhone); if (isClosed) { return false; } if (state.e164Phone != requestPhone) { emit(state.copyWith(isSendingCode: false)); return false; } emit( state.copyWith( isSendingCode: false, codeSent: true, errorMessage: null, ), ); return true; } catch (e) { if (isClosed) { return false; } final message = e is ApiException ? e.message : L10n.current.authSendCodeFailed; emit(state.copyWith(isSendingCode: false, errorMessage: message)); return false; } } Future submit() async { if (!state.isValid) return null; emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { final response = await _repository.createPhoneSession( phone: state.e164Phone, token: state.code.value, ); if (isClosed) { return null; } emit(state.copyWith(status: FormzSubmissionStatus.success)); return response; } catch (e) { if (isClosed) { return null; } final message = e is ApiException ? e.message : e.toString(); emit( state.copyWith( status: FormzSubmissionStatus.failure, errorMessage: message, ), ); return null; } } void _startResendCooldown() { _resendTimer?.cancel(); emit(state.copyWith(resendCooldownSeconds: 60)); _resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (isClosed) { timer.cancel(); return; } if (state.resendCooldownSeconds <= 1) { timer.cancel(); emit(state.copyWith(resendCooldownSeconds: 0)); return; } emit( state.copyWith(resendCooldownSeconds: state.resendCooldownSeconds - 1), ); }); } @override Future close() { _resendTimer?.cancel(); return super.close(); } }