init
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'auth_repository.dart';
|
||||
|
||||
enum AuthStatus { initial, loading, otpSent, authenticated, error }
|
||||
|
||||
class AuthState extends Equatable {
|
||||
final AuthStatus status;
|
||||
final String mobile;
|
||||
final String? error;
|
||||
|
||||
const AuthState({
|
||||
this.status = AuthStatus.initial,
|
||||
this.mobile = '',
|
||||
this.error,
|
||||
});
|
||||
|
||||
AuthState copyWith({AuthStatus? status, String? mobile, String? error}) =>
|
||||
AuthState(
|
||||
status: status ?? this.status,
|
||||
mobile: mobile ?? this.mobile,
|
||||
error: error,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, mobile, error];
|
||||
}
|
||||
|
||||
class AuthCubit extends Cubit<AuthState> {
|
||||
final AuthRepository _repo;
|
||||
AuthCubit(this._repo) : super(const AuthState());
|
||||
|
||||
Future<void> requestOtp(String mobile) async {
|
||||
emit(state.copyWith(status: AuthStatus.loading, mobile: mobile));
|
||||
try {
|
||||
await _repo.requestOtp(mobile);
|
||||
emit(state.copyWith(status: AuthStatus.otpSent, mobile: mobile));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(status: AuthStatus.error, error: _msg(e)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyOtp(String code) async {
|
||||
emit(state.copyWith(status: AuthStatus.loading));
|
||||
try {
|
||||
await _repo.verifyOtp(state.mobile, code);
|
||||
emit(state.copyWith(status: AuthStatus.authenticated));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(status: AuthStatus.error, error: _msg(e)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _repo.logout();
|
||||
emit(const AuthState());
|
||||
}
|
||||
|
||||
/// بازنشانی وضعیت خطا به حالت مناسب فرم.
|
||||
void resetError({required bool onOtpScreen}) {
|
||||
emit(state.copyWith(
|
||||
status: onOtpScreen ? AuthStatus.otpSent : AuthStatus.initial));
|
||||
}
|
||||
|
||||
String _msg(Object e) {
|
||||
if (e is DioException) {
|
||||
final data = e.response?.data;
|
||||
if (data is Map && data['message'] != null) {
|
||||
return data['message'].toString();
|
||||
}
|
||||
return 'خطا در ارتباط با سرور';
|
||||
}
|
||||
return 'خطای نامشخص';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import '../../core/network/api_client.dart';
|
||||
import '../../core/storage/token_storage.dart';
|
||||
|
||||
/// دسترسی به endpointهای احراز هویت (login-otp / check-otp).
|
||||
class AuthRepository {
|
||||
final ApiClient _api;
|
||||
final TokenStorage _storage;
|
||||
|
||||
AuthRepository(this._api, this._storage);
|
||||
|
||||
/// درخواست ارسال کد یکبارمصرف.
|
||||
Future<void> requestOtp(String mobile) async {
|
||||
await _api.dio.post('/auth/login-otp', data: {'mobile': mobile});
|
||||
}
|
||||
|
||||
/// اعتبارسنجی کد و ذخیرهی توکن JWT.
|
||||
Future<void> verifyOtp(String mobile, String code) async {
|
||||
final res = await _api.dio.post(
|
||||
'/auth/check-otp',
|
||||
data: {'mobile': mobile, 'token': code},
|
||||
);
|
||||
final token = res.data['token'] as String?;
|
||||
if (token == null || token.isEmpty) {
|
||||
throw Exception('no token in response');
|
||||
}
|
||||
await _storage.write(token);
|
||||
}
|
||||
|
||||
Future<bool> isLoggedIn() async {
|
||||
final t = await _storage.read();
|
||||
return t != null && t.isNotEmpty;
|
||||
}
|
||||
|
||||
Future<void> logout() => _storage.clear();
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import 'auth_cubit.dart';
|
||||
|
||||
/// صفحهی ورود شماره موبایل.
|
||||
class MobileScreen extends StatefulWidget {
|
||||
const MobileScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MobileScreen> createState() => _MobileScreenState();
|
||||
}
|
||||
|
||||
class _MobileScreenState extends State<MobileScreen> {
|
||||
final _controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _valid => RegExp(r'^09\d{9}$').hasMatch(_controller.text.trim());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: BlocConsumer<AuthCubit, AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == AuthStatus.otpSent) {
|
||||
context.push('/otp');
|
||||
} else if (state.status == AuthStatus.error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.error ?? 'خطا')),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final loading = state.status == AuthStatus.loading;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('سلطان حکم',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.gold)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('برای ورود شماره موبایلت رو وارد کن',
|
||||
style: TextStyle(color: Colors.white70)),
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.phone,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 20, letterSpacing: 2),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(11),
|
||||
],
|
||||
decoration: const InputDecoration(hintText: '09xxxxxxxxx'),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: (!_valid || loading)
|
||||
? null
|
||||
: () => context
|
||||
.read<AuthCubit>()
|
||||
.requestOtp(_controller.text.trim()),
|
||||
child: loading
|
||||
? const SizedBox(
|
||||
height: 22,
|
||||
width: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('دریافت کد'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import 'auth_cubit.dart';
|
||||
|
||||
/// صفحهی ورود کد یکبارمصرف (۵ رقمی).
|
||||
class OtpScreen extends StatefulWidget {
|
||||
const OtpScreen({super.key});
|
||||
|
||||
@override
|
||||
State<OtpScreen> createState() => _OtpScreenState();
|
||||
}
|
||||
|
||||
class _OtpScreenState extends State<OtpScreen> {
|
||||
final _controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _valid => _controller.text.trim().length == 5;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('تأیید کد')),
|
||||
body: SafeArea(
|
||||
child: BlocConsumer<AuthCubit, AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == AuthStatus.authenticated) {
|
||||
context.go('/lobby');
|
||||
} else if (state.status == AuthStatus.error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.error ?? 'خطا')),
|
||||
);
|
||||
context.read<AuthCubit>().resetError(onOtpScreen: true);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final loading = state.status == AuthStatus.loading;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('کد پیامکشده به ${state.mobile} را وارد کنید',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: Colors.white70)),
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 28, letterSpacing: 12),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(5),
|
||||
],
|
||||
decoration: const InputDecoration(hintText: '- - - - -'),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: (!_valid || loading)
|
||||
? null
|
||||
: () => context
|
||||
.read<AuthCubit>()
|
||||
.verifyOtp(_controller.text.trim()),
|
||||
child: loading
|
||||
? const SizedBox(
|
||||
height: 22,
|
||||
width: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Text('ورود'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: loading ? null : () => context.pop(),
|
||||
child: const Text('تغییر شماره',
|
||||
style: TextStyle(color: AppColors.gold)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user