import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:geolocator/geolocator.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../core/api/api_exception.dart'; import '../../../core/api/api_failure.dart'; import '../../../core/config.dart'; import '../data/auth_repository.dart'; import 'login_state.dart'; import 'phone_error.dart'; /// تدفّق الدخول كاملاً: الشروط ← إذن الموقع ← الهاتف ← الرمز. /// /// الـCubit لا يعرف Flutter: لا `BuildContext` ولا تنقّل ولا `SnackBar` /// (docs/23 §3). يُصدر حالة، والواجهة تتصرّف. class LoginCubit extends Cubit { LoginCubit(this._repo, this._prefs) : super(const LoginState()) { _resolveInitialStep(); } final AuthRepository _repo; final SharedPreferences _prefs; static const _kAgreed = 'auth.agreed_terms'; Timer? _resendTimer; @override Future close() { _resendTimer?.cancel(); return super.close(); } // ── البوابتان ────────────────────────────────────────────────────────── void _resolveInitialStep() { if (_prefs.getBool(_kAgreed) != true) { emit(state.copyWith(step: LoginStep.agreement)); return; } emit(state.copyWith(step: LoginStep.permission, agreed: true)); unawaited(checkLocationPermission()); } void toggleAgreement(bool value) => emit(state.copyWith(agreed: value)); Future acceptAgreement() async { if (!state.agreed) return; await _prefs.setBool(_kAgreed, true); emit(state.copyWith(step: LoginStep.permission)); await checkLocationPermission(); } /// تُستدعى أيضاً عند **العودة من إعدادات النظام**: بلا إعادة الفحص تبقى /// الشاشة عالقة بعد أن يمنح المستخدم الإذن يدوياً (حالة حافة من docs/39 §2). Future checkLocationPermission() async { final geoStatus = await Geolocator.checkPermission(); if (geoStatus == LocationPermission.always || geoStatus == LocationPermission.whileInUse) { emit(state.copyWith( step: LoginStep.phone, permissionPermanentlyDenied: false, )); return; } final status = await Permission.locationWhenInUse.status; if (status.isGranted || status.isLimited) { emit(state.copyWith( step: LoginStep.phone, permissionPermanentlyDenied: false, )); return; } emit(state.copyWith( permissionPermanentlyDenied: geoStatus == LocationPermission.deniedForever || status.isPermanentlyDenied, )); } Future requestLocationPermission() async { final geoStatus = await Geolocator.requestPermission(); if (geoStatus == LocationPermission.always || geoStatus == LocationPermission.whileInUse) { emit(state.copyWith( step: LoginStep.phone, permissionPermanentlyDenied: false, )); return; } final status = await Permission.locationWhenInUse.request(); if (status.isGranted || status.isLimited) { emit(state.copyWith( step: LoginStep.phone, permissionPermanentlyDenied: false, )); return; } emit(state.copyWith( permissionPermanentlyDenied: geoStatus == LocationPermission.deniedForever || status.isPermanentlyDenied, )); } Future openSystemSettings() => openAppSettings(); // ── الهاتف ───────────────────────────────────────────────────────────── void phoneChanged(String value) { emit(state.copyWith( phone: value, clearPhoneError: true, clearFailure: true, )); } Future submitPhone() async { final error = validatePhone(state.phone); if (error != null) { emit(state.copyWith(phoneError: error)); return; } emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true)); try { await _repo.sendOtp(state.phone.trim()); emit(state.copyWith(status: LoginStatus.idle, step: LoginStep.otp)); _startResendCountdown(); } on ApiException catch (e) { emit(state.copyWith( status: LoginStatus.failure, failure: e.toApiFailure(), )); } } void editPhone() { _resendTimer?.cancel(); emit(state.copyWith( step: LoginStep.phone, status: LoginStatus.idle, resendIn: 0, clearFailure: true, )); } // ── الرمز ────────────────────────────────────────────────────────────── Future resendOtp() async { if (!state.canResend) return; emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true)); try { await _repo.sendOtp(state.phone.trim()); emit(state.copyWith(status: LoginStatus.idle)); _startResendCountdown(); } on ApiException catch (e) { emit(state.copyWith( status: LoginStatus.failure, failure: e.toApiFailure(), )); // حتى عند الرفض بحدّ المعدّل نبدأ العدّاد — وإلا ضغط المستخدم مجدداً // فوراً وعمّق الحظر. _startResendCountdown(); } } Future submitOtp(String code) async { if (code.length != AppConfig.otpLength) return; emit(state.copyWith(status: LoginStatus.submitting, clearFailure: true)); try { final user = await _repo.verifyOtp(phone: state.phone.trim(), code: code); _resendTimer?.cancel(); emit(state.copyWith( status: LoginStatus.success, step: LoginStep.done, user: user, )); } on ApiException catch (e) { emit(state.copyWith( status: LoginStatus.failure, failure: e.toApiFailure(), )); } } void _startResendCountdown() { _resendTimer?.cancel(); emit(state.copyWith(resendIn: AppConfig.otpResendCooldown.inSeconds)); _resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final next = state.resendIn - 1; if (next <= 0) timer.cancel(); emit(state.copyWith(resendIn: next < 0 ? 0 : next)); }); } }