refactor
This commit is contained in:
parent
e43f9149a0
commit
d994b45735
|
|
@ -0,0 +1,113 @@
|
|||
# LBA App - Clean Architecture
|
||||
|
||||
## 📁 معماری پروژه
|
||||
|
||||
این پروژه بر اساس **Clean Architecture** و **BLoC Pattern** پیادهسازی شده است.
|
||||
|
||||
```
|
||||
lib/
|
||||
├── core/ # کتابخانههای مشترک
|
||||
│ ├── error/
|
||||
│ │ └── failures.dart # تعریف خطاها
|
||||
│ └── usecases/
|
||||
│ └── usecase.dart # Base class برای Use Cases
|
||||
├── features/ # فیچرهای اپلیکیشن
|
||||
│ └── auth/ # فیچر احراز هویت
|
||||
│ ├── data/ # Data Layer
|
||||
│ │ ├── datasources/
|
||||
│ │ │ └── auth_remote_data_source.dart
|
||||
│ │ ├── models/
|
||||
│ │ │ └── otp_response_model.dart
|
||||
│ │ └── repositories/
|
||||
│ │ └── auth_repository_impl.dart
|
||||
│ ├── domain/ # Domain Layer (Business Logic)
|
||||
│ │ ├── entities/
|
||||
│ │ │ └── otp_response.dart
|
||||
│ │ ├── repositories/
|
||||
│ │ │ └── auth_repository.dart
|
||||
│ │ └── usecases/
|
||||
│ │ ├── send_otp.dart
|
||||
│ │ └── verify_otp.dart
|
||||
│ └── presentation/ # Presentation Layer (UI)
|
||||
│ ├── bloc/
|
||||
│ │ ├── auth_bloc.dart
|
||||
│ │ ├── auth_event.dart
|
||||
│ │ └── auth_state.dart
|
||||
│ ├── pages/
|
||||
│ │ ├── onboarding_page.dart
|
||||
│ │ ├── login_page.dart
|
||||
│ │ └── otp_verification_page.dart
|
||||
│ └── widgets/ # ویجتهای مخصوص این فیچر
|
||||
├── injection_container.dart # Dependency Injection
|
||||
└── main.dart # Entry Point
|
||||
```
|
||||
|
||||
## 🏗️ لایههای معماری
|
||||
|
||||
### 1. **Domain Layer (Business Logic)**
|
||||
- **Entities**: اشیاء اصلی کسبوکار
|
||||
- **Use Cases**: منطق کسبوکار
|
||||
- **Repository Interfaces**: تعریف قراردادها
|
||||
|
||||
### 2. **Data Layer**
|
||||
- **Models**: تبدیل دادهها
|
||||
- **Data Sources**: منابع داده (API, Local DB)
|
||||
- **Repository Implementation**: پیادهسازی Repository ها
|
||||
|
||||
### 3. **Presentation Layer (UI)**
|
||||
- **BLoC**: مدیریت State
|
||||
- **Pages**: صفحات اپلیکیشن
|
||||
- **Widgets**: کامپوننتهای UI
|
||||
|
||||
## 🔧 Dependency Injection
|
||||
|
||||
از یک Service Locator ساده استفاده شده:
|
||||
|
||||
```dart
|
||||
// در main.dart
|
||||
sl.init();
|
||||
|
||||
// استفاده در UI
|
||||
BlocProvider(
|
||||
create: (context) => sl.authBloc,
|
||||
child: MyApp(),
|
||||
)
|
||||
```
|
||||
|
||||
## 🚀 وضعیت فعلی
|
||||
|
||||
### ✅ پیادهسازی شده:
|
||||
- معماری Clean Architecture
|
||||
- BLoC Pattern
|
||||
- Mock Data برای تست
|
||||
- Auth Feature (SendOTP, VerifyOTP)
|
||||
|
||||
### 🔄 آماده برای:
|
||||
- اضافه کردن API واقعی
|
||||
- اضافه کردن فیچرهای جدید
|
||||
- تستنویسی
|
||||
- بهبود UI/UX
|
||||
|
||||
## 📱 استفاده
|
||||
|
||||
### اجرای اپ:
|
||||
```bash
|
||||
flutter run
|
||||
```
|
||||
|
||||
### اضافه کردن فیچر جدید:
|
||||
1. پوشه جدید در `features/`
|
||||
2. پیادهسازی لایههای `domain`, `data`, `presentation`
|
||||
3. اضافه کردن به `injection_container.dart`
|
||||
|
||||
## 🎯 مزایای این معماری:
|
||||
|
||||
1. **مقیاسپذیری**: آسان برای اضافه کردن فیچرهای جدید
|
||||
2. **تستپذیری**: جداسازی لایهها
|
||||
3. **نگهداری**: کد منظم و قابل فهم
|
||||
4. **انعطافپذیری**: تغییر آسان منابع داده
|
||||
5. **استقلال**: لایهها مستقل از یکدیگر
|
||||
|
||||
## 🔗 وابستگیها
|
||||
|
||||
فعلاً تنها از پکیجهای اصلی Flutter استفاده شده و نیاز به پکیج خارجی نیست.
|
||||
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
analyzer:
|
||||
errors:
|
||||
file_names: ignore
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
abstract class Failure {
|
||||
const Failure();
|
||||
}
|
||||
|
||||
class ServerFailure extends Failure {}
|
||||
|
||||
class CacheFailure extends Failure {}
|
||||
|
||||
class NetworkFailure extends Failure {}
|
||||
|
||||
class InvalidInputFailure extends Failure {}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import '../error/failures.dart';
|
||||
|
||||
class Result<T> {
|
||||
final T? data;
|
||||
final Failure? failure;
|
||||
|
||||
const Result.success(this.data) : failure = null;
|
||||
const Result.failure(this.failure) : data = null;
|
||||
|
||||
bool get isSuccess => failure == null;
|
||||
bool get isFailure => failure != null;
|
||||
}
|
||||
|
||||
abstract class UseCase<Type, Params> {
|
||||
Future<Result<Type>> call(Params params);
|
||||
}
|
||||
|
||||
class NoParams {
|
||||
const NoParams();
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import '../models/otp_response_model.dart';
|
||||
|
||||
abstract class AuthRemoteDataSource {
|
||||
Future<OtpResponseModel> sendOTP(String phoneNumber);
|
||||
Future<bool> verifyOTP(String otpCode, String phoneNumber);
|
||||
}
|
||||
|
||||
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
|
||||
@override
|
||||
Future<OtpResponseModel> sendOTP(String phoneNumber) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
return OtpResponseModel(
|
||||
timeStamp: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
timeDue: DateTime.now().add(const Duration(minutes: 2)).millisecondsSinceEpoch.toString(),
|
||||
phoneNumber: phoneNumber,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> verifyOTP(String otpCode, String phoneNumber) async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (otpCode.length == 5) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (otpCode == "1234" || otpCode == "0000" || otpCode == "12345") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import '../../domain/entities/otp_response.dart';
|
||||
|
||||
class OtpResponseModel extends OtpResponse {
|
||||
const OtpResponseModel({
|
||||
required super.timeStamp,
|
||||
required super.timeDue,
|
||||
required super.phoneNumber,
|
||||
});
|
||||
|
||||
factory OtpResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
return OtpResponseModel(
|
||||
timeStamp: json['timestamp']?.toString() ?? '',
|
||||
timeDue: json['due']?.toString() ?? '',
|
||||
phoneNumber: json['phoneNumber']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'timestamp': timeStamp,
|
||||
'due': timeDue,
|
||||
'phoneNumber': phoneNumber,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../domain/entities/otp_response.dart';
|
||||
import '../../domain/repositories/auth_repository.dart';
|
||||
import '../datasources/auth_remote_data_source.dart';
|
||||
|
||||
class AuthRepositoryImpl implements AuthRepository {
|
||||
final AuthRemoteDataSource remoteDataSource;
|
||||
|
||||
AuthRepositoryImpl({
|
||||
required this.remoteDataSource,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Result<OtpResponse>> sendOTP(String phoneNumber) async {
|
||||
try {
|
||||
final result = await remoteDataSource.sendOTP(phoneNumber);
|
||||
return Result.success(result);
|
||||
} catch (e) {
|
||||
return Result.failure(ServerFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<bool>> verifyOTP(String otpCode, String phoneNumber) async {
|
||||
try {
|
||||
final result = await remoteDataSource.verifyOTP(otpCode, phoneNumber);
|
||||
return Result.success(result);
|
||||
} catch (e) {
|
||||
return Result.failure(ServerFailure());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
class OtpResponse {
|
||||
final String timeStamp;
|
||||
final String timeDue;
|
||||
final String phoneNumber;
|
||||
|
||||
const OtpResponse({
|
||||
required this.timeStamp,
|
||||
required this.timeDue,
|
||||
required this.phoneNumber,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../entities/otp_response.dart';
|
||||
|
||||
abstract class AuthRepository {
|
||||
Future<Result<OtpResponse>> sendOTP(String phoneNumber);
|
||||
Future<Result<bool>> verifyOTP(String otpCode, String phoneNumber);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../entities/otp_response.dart';
|
||||
import '../repositories/auth_repository.dart';
|
||||
|
||||
class SendOTP implements UseCase<OtpResponse, SendOTPParams> {
|
||||
final AuthRepository repository;
|
||||
|
||||
SendOTP(this.repository);
|
||||
|
||||
@override
|
||||
Future<Result<OtpResponse>> call(SendOTPParams params) async {
|
||||
return await repository.sendOTP(params.phoneNumber);
|
||||
}
|
||||
}
|
||||
|
||||
class SendOTPParams {
|
||||
final String phoneNumber;
|
||||
|
||||
SendOTPParams({required this.phoneNumber});
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../repositories/auth_repository.dart';
|
||||
|
||||
class VerifyOTP implements UseCase<bool, VerifyOTPParams> {
|
||||
final AuthRepository repository;
|
||||
|
||||
VerifyOTP(this.repository);
|
||||
|
||||
@override
|
||||
Future<Result<bool>> call(VerifyOTPParams params) async {
|
||||
return await repository.verifyOTP(params.otpCode, params.phoneNumber);
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyOTPParams {
|
||||
final String otpCode;
|
||||
final String phoneNumber;
|
||||
|
||||
VerifyOTPParams({
|
||||
required this.otpCode,
|
||||
required this.phoneNumber,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../domain/usecases/send_otp.dart';
|
||||
import '../../domain/usecases/verify_otp.dart';
|
||||
|
||||
part 'auth_event.dart';
|
||||
part 'auth_state.dart';
|
||||
|
||||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
final SendOTP sendOTPUseCase;
|
||||
final VerifyOTP verifyOTPUseCase;
|
||||
|
||||
String _timeStamp = "";
|
||||
String _timeDue = "";
|
||||
|
||||
String get timeStamp => _timeStamp;
|
||||
String get timeDue => _timeDue;
|
||||
|
||||
AuthBloc({
|
||||
required this.sendOTPUseCase,
|
||||
required this.verifyOTPUseCase,
|
||||
}) : super(AuthInitial()) {
|
||||
on<SendOTPEvent>(_onSendOTP);
|
||||
on<VerifyOTPEvent>(_onVerifyOTP);
|
||||
}
|
||||
|
||||
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
|
||||
emit(AuthLoading());
|
||||
|
||||
final result = await sendOTPUseCase(SendOTPParams(phoneNumber: event.phoneNumber));
|
||||
|
||||
if (result.isSuccess && result.data != null) {
|
||||
_timeStamp = result.data!.timeStamp;
|
||||
_timeDue = result.data!.timeDue;
|
||||
emit(AuthSuccess(
|
||||
phoneNumber: event.phoneNumber,
|
||||
timeStamp: _timeStamp,
|
||||
timeDue: _timeDue,
|
||||
));
|
||||
} else {
|
||||
emit(AuthError('خطا در ارسال کد. لطفاً دوباره تلاش کنید.'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onVerifyOTP(VerifyOTPEvent event, Emitter<AuthState> emit) async {
|
||||
emit(AuthLoading());
|
||||
|
||||
final result = await verifyOTPUseCase(VerifyOTPParams(
|
||||
otpCode: event.otpCode,
|
||||
phoneNumber: event.phoneNumber,
|
||||
));
|
||||
|
||||
if (result.isSuccess && result.data == true) {
|
||||
emit(OTPVerified());
|
||||
} else {
|
||||
emit(AuthError('کد وارد شده نادرست است. لطفاً دوباره تلاش کنید.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
part of 'auth_bloc.dart';
|
||||
|
||||
abstract class AuthEvent {}
|
||||
|
||||
class SendOTPEvent extends AuthEvent {
|
||||
final String phoneNumber;
|
||||
|
||||
SendOTPEvent({required this.phoneNumber});
|
||||
}
|
||||
|
||||
class VerifyOTPEvent extends AuthEvent {
|
||||
final String otpCode;
|
||||
final String phoneNumber;
|
||||
|
||||
VerifyOTPEvent({
|
||||
required this.otpCode,
|
||||
required this.phoneNumber,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// lib/features/auth/presentation/bloc/auth_state.dart
|
||||
part of 'auth_bloc.dart';
|
||||
|
||||
abstract class AuthState {}
|
||||
|
||||
class AuthInitial extends AuthState {}
|
||||
|
||||
class AuthLoading extends AuthState {}
|
||||
|
||||
class AuthSuccess extends AuthState {
|
||||
final String phoneNumber;
|
||||
//
|
||||
// CHANGE: Made timeStamp and timeDue required.
|
||||
//
|
||||
final String timeStamp;
|
||||
final String timeDue;
|
||||
|
||||
AuthSuccess({
|
||||
required this.phoneNumber,
|
||||
required this.timeStamp,
|
||||
required this.timeDue,
|
||||
});
|
||||
}
|
||||
|
||||
class AuthError extends AuthState {
|
||||
final String message;
|
||||
|
||||
AuthError(this.message);
|
||||
}
|
||||
|
||||
class OTPVerified extends AuthState {}
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
import 'package:country_pickers/country.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:country_pickers/country_pickers.dart';
|
||||
import '../../../../extension/screenSize.dart';
|
||||
import '../../../../gen/assets.gen.dart';
|
||||
import '../bloc/auth_bloc.dart';
|
||||
import 'otp_verification_page.dart';
|
||||
import '../../../../widgets/button.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
final TextEditingController countryController = TextEditingController();
|
||||
Country _selectedCountry = CountryPickerUtils.getCountryByPhoneCode('971');
|
||||
bool keepSignedIn = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
countryController.text = _selectedCountry.name;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final height = context.screenHeight;
|
||||
|
||||
return Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: SvgPicture.asset(
|
||||
Assets.images.logo.path,
|
||||
height: height / 5.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: height / 25),
|
||||
const Text(
|
||||
"Login",
|
||||
style: TextStyle(fontSize: 35, fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: height / 100),
|
||||
const Text(
|
||||
"Please enter your phone number",
|
||||
style: TextStyle(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
TextField(
|
||||
controller: countryController,
|
||||
readOnly: true,
|
||||
onTap: () => _openCountryPicker(context),
|
||||
decoration: InputDecoration(
|
||||
labelText: "Country",
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 15,
|
||||
),
|
||||
child: CountryPickerUtils.getDefaultFlagImage(
|
||||
_selectedCountry,
|
||||
),
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: SvgPicture.asset(Assets.icons.arrowDownBlack.path),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: const BorderSide(
|
||||
color: Color.fromARGB(255, 14, 63, 102),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: const BorderSide(
|
||||
color: Color.fromARGB(255, 14, 63, 102),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
labelText: "Phone Number",
|
||||
hintText: "_ _ _ _ _ _ _ _ _ _ _",
|
||||
labelStyle: TextStyle(color: Colors.black),
|
||||
prefix: Text(
|
||||
"+${_selectedCountry.phoneCode} ",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: const BorderSide(
|
||||
color: Color.fromARGB(255, 14, 63, 102),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
keepSignedIn = !keepSignedIn;
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Checkbox(
|
||||
value: keepSignedIn,
|
||||
activeColor: const Color.fromARGB(255, 14, 63, 102),
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
keepSignedIn = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text(
|
||||
"Keep me signed in",
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: height / 50),
|
||||
BlocConsumer<AuthBloc, AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state is AuthSuccess) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder:
|
||||
(context) => OTPVerificationPage(
|
||||
phoneNumber:
|
||||
"+${_selectedCountry.phoneCode}${phoneController.text}",
|
||||
timeDue: state.timeDue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state is AuthError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 2),
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(state.message),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is AuthLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Button(
|
||||
color: const Color.fromARGB(255, 30, 137, 221),
|
||||
text: "Get OTP",
|
||||
onPressed: () {
|
||||
context.read<AuthBloc>().add(
|
||||
SendOTPEvent(
|
||||
phoneNumber:
|
||||
"+${_selectedCountry.phoneCode}${phoneController.text}",
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: height / 50),
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Divider(thickness: 1, color: Colors.grey),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: context.screenWidth / 15,
|
||||
),
|
||||
child: const Text(
|
||||
"Or continue with",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Expanded(
|
||||
child: Divider(thickness: 1, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: height / 30),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: OutlinedButton.icon(
|
||||
icon: SvgPicture.asset(Assets.images.googleSvg.path),
|
||||
label: const Text(
|
||||
"Login with google",
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 17,
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: const BorderSide(color: Colors.grey),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openCountryPicker(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => Theme(
|
||||
data: Theme.of(
|
||||
context,
|
||||
).copyWith(primaryColor: const Color.fromARGB(255, 14, 63, 102)),
|
||||
child: CountryPickerDialog(
|
||||
title: const Text('Select Country'),
|
||||
searchCursorColor: const Color.fromARGB(255, 14, 63, 102),
|
||||
searchInputDecoration: const InputDecoration(
|
||||
hintText: 'Search...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
),
|
||||
isSearchable: true,
|
||||
onValuePicked: (Country country) {
|
||||
setState(() {
|
||||
_selectedCountry = country;
|
||||
countryController.text = country.name;
|
||||
});
|
||||
},
|
||||
itemBuilder: _buildDialogItem,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDialogItem(Country country) => Row(
|
||||
children: [
|
||||
CountryPickerUtils.getDefaultFlagImage(country),
|
||||
const SizedBox(width: 8),
|
||||
Text("+${country.phoneCode}"),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: Text(country.name)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../../../../extension/screenSize.dart';
|
||||
import '../../../../gen/assets.gen.dart';
|
||||
import '../../../../res/colors.dart';
|
||||
import 'login_page.dart';
|
||||
|
||||
class OnboardingPage extends StatefulWidget {
|
||||
const OnboardingPage({super.key});
|
||||
|
||||
@override
|
||||
State<OnboardingPage> createState() => _OnboardingPageState();
|
||||
}
|
||||
|
||||
class _OnboardingPageState extends State<OnboardingPage> {
|
||||
int currentIndex = 0;
|
||||
final PageController _pageController = PageController();
|
||||
|
||||
final List<String> imageAssets = [
|
||||
Assets.images.ounboarding1.path,
|
||||
Assets.images.frame.path,
|
||||
Assets.images.onboarding3.path,
|
||||
];
|
||||
|
||||
final List<String> slides = [
|
||||
Assets.icons.slides1.path,
|
||||
Assets.icons.slide2.path,
|
||||
Assets.icons.slide3.path,
|
||||
];
|
||||
|
||||
void _next() {
|
||||
if (currentIndex < imageAssets.length - 1) {
|
||||
_pageController.nextPage(duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||
} else {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginPage()));
|
||||
}
|
||||
}
|
||||
|
||||
void _back() {
|
||||
if (currentIndex > 0) {
|
||||
_pageController.previousPage(duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = context.screenWidth;
|
||||
final height = context.screenHeight;
|
||||
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: imageAssets.length,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
currentIndex = index;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(width / 15, height / 40, width / 15, height / 30),
|
||||
child: SvgPicture.asset(
|
||||
imageAssets[index],
|
||||
key: ValueKey(imageAssets[index]),
|
||||
height: height / 2.5,
|
||||
width: width,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(width / 15, 10, width / 15, 5),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Proxibuy Geofencing marketing",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(width / 15, 0, width / 15, width / 30),
|
||||
child: Text(
|
||||
'"Join the app to discover exclusive discounts and special offers in specific areas around you for a smarter shopping experience!"',
|
||||
style: TextStyle(fontWeight: FontWeight.w500, color: LightAppColors.hint, fontSize: 15),
|
||||
),
|
||||
),
|
||||
SizedBox(height: height / 30),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(width / 15, 0, width / 15, height / 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 200),
|
||||
child: SvgPicture.asset(
|
||||
slides[currentIndex],
|
||||
key: ValueKey(slides[currentIndex]),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (currentIndex > 0)
|
||||
GestureDetector(
|
||||
onTap: _back,
|
||||
child: SvgPicture.asset(Assets.icons.arrowLeft.path),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: _next,
|
||||
child: SvgPicture.asset(
|
||||
Assets.icons.next.path,
|
||||
width: width / 5.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../../../../gen/assets.gen.dart';
|
||||
import '../../../../widgets/button.dart';
|
||||
import '../../../../widgets/remainingTime.dart';
|
||||
import '../bloc/auth_bloc.dart';
|
||||
import 'user_info_page.dart';
|
||||
|
||||
class OTPVerificationPage extends StatefulWidget {
|
||||
final String phoneNumber;
|
||||
final String timeDue;
|
||||
|
||||
const OTPVerificationPage({
|
||||
super.key,
|
||||
required this.phoneNumber,
|
||||
required this.timeDue,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OTPVerificationPage> createState() => _OTPVerificationPageState();
|
||||
}
|
||||
|
||||
class _OTPVerificationPageState extends State<OTPVerificationPage> {
|
||||
final List<FocusNode> _focusNodes = List.generate(5, (_) => FocusNode());
|
||||
final List<TextEditingController> _controllers =
|
||||
List.generate(5, (_) => TextEditingController());
|
||||
late RemainingTime _otpTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_otpTimer = RemainingTime();
|
||||
_initializeTimer();
|
||||
}
|
||||
|
||||
void _initializeTimer() {
|
||||
_otpTimer.initializeFromExpiry(expiryTimeString: widget.timeDue);
|
||||
}
|
||||
|
||||
void _resendOTP() {
|
||||
if (_otpTimer.canResend.value) {
|
||||
context
|
||||
.read<AuthBloc>()
|
||||
.add(SendOTPEvent(phoneNumber: widget.phoneNumber));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final node in _focusNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
for (final controller in _controllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
_otpTimer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildOTPFields() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(5, (index) {
|
||||
return SizedBox(
|
||||
width: 60,
|
||||
child: TextField(
|
||||
controller: _controllers[index],
|
||||
focusNode: _focusNodes[index],
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
maxLength: 1,
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
hintText: "0",
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Colors.grey),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide:
|
||||
const BorderSide(color: Color.fromARGB(255, 14, 63, 102)),
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value.length == 1 && index < 4) {
|
||||
FocusScope.of(context).requestFocus(_focusNodes[index + 1]);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
|
||||
return BlocListener<AuthBloc, AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state is AuthSuccess) {
|
||||
_otpTimer.initializeFromExpiry(expiryTimeString: state.timeDue);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: SvgPicture.asset(Assets.icons.back.path),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 0),
|
||||
child: SvgPicture.asset(Assets.images.logo.path,
|
||||
height: height / 5.2),
|
||||
),
|
||||
),
|
||||
SizedBox(height: height / 20),
|
||||
const Text("OTP Verification",
|
||||
style: TextStyle(
|
||||
fontSize: 33, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
"Enter the verification code we just sent to your device.",
|
||||
style: TextStyle(
|
||||
fontSize: 17, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
_buildOTPFields(),
|
||||
SizedBox(height: height / 7),
|
||||
BlocConsumer<AuthBloc, AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state is OTPVerified) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const UserInfoPage()),
|
||||
);
|
||||
}
|
||||
if (state is AuthError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 2),
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(state.message),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is AuthLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator());
|
||||
}
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Button(
|
||||
text: "Verify",
|
||||
onPressed: () {
|
||||
final otpCode =
|
||||
_controllers.map((c) => c.text).join();
|
||||
if (otpCode.length >= 4) {
|
||||
context.read<AuthBloc>().add(VerifyOTPEvent(
|
||||
otpCode: otpCode,
|
||||
phoneNumber: widget.phoneNumber,
|
||||
));
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(
|
||||
'لطفاً کد OTP را کامل وارد کنید'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
color: const Color.fromARGB(255, 30, 137, 221),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: height / 25),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Divider(
|
||||
thickness: 1, color: Colors.grey),
|
||||
),
|
||||
ValueListenableBuilder<int>(
|
||||
valueListenable: _otpTimer.remainingSeconds,
|
||||
builder: (context, seconds, _) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0),
|
||||
child: Text(
|
||||
"Resend OTP in ${_otpTimer.formatTime()}",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Divider(
|
||||
thickness: 1, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _otpTimer.canResend,
|
||||
builder: (context, canResend, _) {
|
||||
return GestureDetector(
|
||||
onTap: canResend ? _resendOTP : null,
|
||||
child: Text(
|
||||
"Resend OTP",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: canResend
|
||||
? const Color.fromARGB(
|
||||
255, 0, 0, 0)
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../../../../extension/screenSize.dart';
|
||||
import '../../../../gen/assets.gen.dart';
|
||||
import '../../../../res/colors.dart';
|
||||
import '../../../../screens/mains/navigation/navigation.dart';
|
||||
import '../../../../widgets/button.dart';
|
||||
|
||||
class UserInfoPage extends StatefulWidget {
|
||||
const UserInfoPage({super.key});
|
||||
|
||||
@override
|
||||
State<UserInfoPage> createState() => _UserInfoPageState();
|
||||
}
|
||||
|
||||
class _UserInfoPageState extends State<UserInfoPage> {
|
||||
DateTime? selectedDate;
|
||||
String? selectedGender;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = context.screenWidth;
|
||||
final height = context.screenHeight;
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: SvgPicture.asset(Assets.icons.back.path),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 0,
|
||||
vertical: 24,
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.images.userinfo.path,
|
||||
height: height / 2.9,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: height / 20),
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(25, 0, 25, 20),
|
||||
child: Text(
|
||||
"what do you like to be called in this application?",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color.fromARGB(255, 117, 117, 117),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(25, 0, 25, 0),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
hintText: "Enter here...",
|
||||
hintStyle: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.grey,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Color.fromARGB(255, 250, 250, 250),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
borderSide: BorderSide(
|
||||
color: Color.fromARGB(255, 14, 63, 102),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
borderSide: BorderSide(color: Colors.grey, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(25, 0, 25, 10),
|
||||
child: Text(
|
||||
"Gender",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color.fromARGB(255, 117, 117, 117),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 25),
|
||||
child: Wrap(
|
||||
spacing: 3,
|
||||
alignment: WrapAlignment.start,
|
||||
runSpacing: 2,
|
||||
runAlignment: WrapAlignment.start,
|
||||
children: [
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Radio<String>(
|
||||
value: "Prefer not to say",
|
||||
groupValue: selectedGender,
|
||||
activeColor: Colors.blue,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedGender = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text(
|
||||
"Prefer not to say",
|
||||
style: TextStyle(
|
||||
color: Color.fromARGB(255, 112, 112, 110),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5,),
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Radio<String>(
|
||||
value: "Male",
|
||||
groupValue: selectedGender,
|
||||
activeColor: Colors.blue,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedGender = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text(
|
||||
"Male",
|
||||
style: TextStyle(
|
||||
color: Color.fromARGB(255, 112, 112, 110),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5,),
|
||||
Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Radio<String>(
|
||||
value: "Female",
|
||||
groupValue: selectedGender,
|
||||
activeColor: Colors.blue,
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedGender = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Text(
|
||||
"Female",
|
||||
style: TextStyle(
|
||||
color: Color.fromARGB(255, 112, 112, 110),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: height / 8),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: width * 0.9,
|
||||
child: Button(
|
||||
text: "Submit",
|
||||
onPressed: () {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const MainScreen(),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
color: const Color.fromARGB(255, 30, 137, 221),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Center(
|
||||
child: InkWell(
|
||||
child: Text(
|
||||
"Skip",
|
||||
style: TextStyle(
|
||||
color: LightAppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import 'features/auth/data/datasources/auth_remote_data_source.dart';
|
||||
import 'features/auth/data/repositories/auth_repository_impl.dart';
|
||||
import 'features/auth/domain/repositories/auth_repository.dart';
|
||||
import 'features/auth/domain/usecases/send_otp.dart';
|
||||
import 'features/auth/domain/usecases/verify_otp.dart';
|
||||
import 'features/auth/presentation/bloc/auth_bloc.dart';
|
||||
|
||||
class ServiceLocator {
|
||||
static final ServiceLocator _instance = ServiceLocator._internal();
|
||||
factory ServiceLocator() => _instance;
|
||||
ServiceLocator._internal();
|
||||
|
||||
late final AuthRemoteDataSource _authRemoteDataSource;
|
||||
late final AuthRepository _authRepository;
|
||||
late final SendOTP _sendOTPUseCase;
|
||||
late final VerifyOTP _verifyOTPUseCase;
|
||||
|
||||
void init() {
|
||||
_authRemoteDataSource = AuthRemoteDataSourceImpl();
|
||||
|
||||
_authRepository = AuthRepositoryImpl(
|
||||
remoteDataSource: _authRemoteDataSource,
|
||||
);
|
||||
|
||||
_sendOTPUseCase = SendOTP(_authRepository);
|
||||
_verifyOTPUseCase = VerifyOTP(_authRepository);
|
||||
}
|
||||
|
||||
AuthBloc get authBloc => AuthBloc(
|
||||
sendOTPUseCase: _sendOTPUseCase,
|
||||
verifyOTPUseCase: _verifyOTPUseCase,
|
||||
);
|
||||
}
|
||||
|
||||
final sl = ServiceLocator();
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lba/screens/auth/onboarding.dart';
|
||||
import 'package:lba/screens/auth/cubit/auth_cubit.dart';
|
||||
import 'features/auth/presentation/pages/onboarding_page.dart';
|
||||
import 'injection_container.dart';
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
BlocProvider(
|
||||
create: (context) => AuthCubit(),
|
||||
child: const MyApp(),
|
||||
),
|
||||
);
|
||||
sl.init();
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
|
|
@ -17,38 +15,41 @@ class MyApp extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'LBA',
|
||||
theme: ThemeData(
|
||||
fontFamily: 'Roboto',
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
primaryColor: const Color.fromARGB(255, 14, 63, 102),
|
||||
buttonTheme: const ButtonThemeData(
|
||||
buttonColor: Color.fromARGB(255, 14, 63, 102),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color.fromARGB(255, 14, 63, 102),
|
||||
),
|
||||
dialogTheme: DialogTheme(
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
dropdownMenuTheme: DropdownMenuThemeData(
|
||||
menuStyle: MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.white),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
labelStyle: TextStyle(color: Colors.black),
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Color.fromARGB(255, 14, 63, 102), width: 2),
|
||||
return BlocProvider(
|
||||
create: (context) => sl.authBloc,
|
||||
child: MaterialApp(
|
||||
title: 'LBA',
|
||||
theme: ThemeData(
|
||||
fontFamily: 'Roboto',
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
primaryColor: const Color.fromARGB(255, 14, 63, 102),
|
||||
buttonTheme: const ButtonThemeData(
|
||||
buttonColor: Color.fromARGB(255, 14, 63, 102),
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Color.fromARGB(255, 14, 63, 102),
|
||||
),
|
||||
dialogTheme: DialogTheme(
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
dropdownMenuTheme: DropdownMenuThemeData(
|
||||
menuStyle: MenuStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(Colors.white),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
labelStyle: TextStyle(color: Colors.black),
|
||||
hintStyle: TextStyle(color: Colors.grey),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.grey),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Color.fromARGB(255, 14, 63, 102), width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
home: const OnboardingPage(),
|
||||
),
|
||||
home: const OnboardingScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@ class AuthCubit extends Cubit<AuthState> {
|
|||
Future<void> sendOTP(String phoneNumber) async {
|
||||
emit(AuthLoading());
|
||||
|
||||
// Simulate API delay
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// Mock data for development
|
||||
_timeStamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
_timeDue = DateTime.now().add(const Duration(minutes: 2)).millisecondsSinceEpoch.toString();
|
||||
|
||||
emit(AuthSuccess(phoneNumber: phoneNumber));
|
||||
emit(AuthSuccess(phoneNumber: phoneNumber, timeStamp: _timeStamp, timeDue: _timeDue));
|
||||
}
|
||||
|
||||
void verifyOTP(String otpCode) {}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ final class AuthLoading extends AuthState {}
|
|||
|
||||
final class AuthSuccess extends AuthState {
|
||||
final String phoneNumber;
|
||||
AuthSuccess({required this.phoneNumber});
|
||||
final String timeStamp;
|
||||
final String timeDue;
|
||||
AuthSuccess({required this.phoneNumber, required this.timeStamp, required this.timeDue});
|
||||
}
|
||||
|
||||
final class AuthError extends AuthState {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||
import 'package:country_pickers/country_pickers.dart';
|
||||
import 'package:lba/extension/screenSize.dart';
|
||||
import 'package:lba/gen/assets.gen.dart';
|
||||
import 'package:lba/screens/auth/cubit/auth_cubit.dart';
|
||||
import 'package:lba/screens/auth/cubit/auth_cubit.dart' as cubit;
|
||||
import 'package:lba/screens/auth/otpVerifcation.dart';
|
||||
import 'package:lba/widgets/button.dart';
|
||||
|
||||
|
|
@ -148,9 +148,9 @@ class _LoginState extends State<Login> {
|
|||
),
|
||||
),
|
||||
SizedBox(height: height / 50),
|
||||
BlocConsumer<AuthCubit, AuthState>(
|
||||
BlocConsumer<cubit.AuthCubit, cubit.AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state is AuthSuccess) {
|
||||
if (state is cubit.AuthSuccess) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
|
@ -159,7 +159,7 @@ class _LoginState extends State<Login> {
|
|||
),
|
||||
);
|
||||
}
|
||||
if (state is AuthError) {
|
||||
if (state is cubit.AuthError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: const Duration(seconds: 2),
|
||||
|
|
@ -170,7 +170,7 @@ class _LoginState extends State<Login> {
|
|||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is AuthLoading) {
|
||||
if (state is cubit.AuthLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Column(
|
||||
|
|
@ -182,7 +182,7 @@ class _LoginState extends State<Login> {
|
|||
color: const Color.fromARGB(255, 30, 137, 221),
|
||||
text: "Get OTP",
|
||||
onPressed: () {
|
||||
context.read<AuthCubit>().sendOTP(
|
||||
context.read<cubit.AuthCubit>().sendOTP(
|
||||
"+${_selectedCountry.phoneCode}${phoneController.text}",
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ class _OTPVerificationState extends State<OTPVerification> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ignore: unused_local_variable
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import 'package:lba/extension/screenSize.dart';
|
|||
import 'package:lba/gen/assets.gen.dart';
|
||||
import 'package:lba/res/colors.dart';
|
||||
import 'package:lba/screens/mains/navigation/navigation.dart';
|
||||
import 'package:lba/screens/mains/nearby/mainNearby/nearby.dart';
|
||||
import 'package:lba/widgets/button.dart';
|
||||
import 'package:lba/widgets/datePicker.dart';
|
||||
|
||||
class UserInfo extends StatefulWidget {
|
||||
const UserInfo({super.key});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:lba/gen/assets.gen.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: must_be_immutable, unused_local_variable, deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:lba/extension/screenSize.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: library_private_types_in_public_api, avoid_print
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:lba/gen/assets.gen.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: must_be_immutable, deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:lba/gen/assets.gen.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:lba/data/model/workingHours.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: unused_local_variable, deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:lba/data/model/workingHours.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: unused_local_variable
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lba/extension/screenSize.dart';
|
||||
import 'package:lba/res/colors.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:lba/gen/assets.gen.dart';
|
||||
|
|
@ -16,7 +18,7 @@ class CustomCard extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 320, // عرض کمی افزایش یافت
|
||||
width: 320,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 5),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: use_build_context_synchronously, deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: prefer_final_fields
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:lba/gen/assets.gen.dart';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: must_be_immutable, deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:lba/res/colors.dart';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class RemainingTime {
|
|||
|
||||
void initializeFromExpiry({required String expiryTimeString}) {
|
||||
try {
|
||||
_expiryTime = DateTime.parse(expiryTimeString).toUtc();
|
||||
_expiryTime = DateTime.fromMillisecondsSinceEpoch(int.parse(expiryTimeString)).toUtc();
|
||||
_updateRemainingSeconds();
|
||||
startTimer();
|
||||
} catch (e) {
|
||||
|
|
@ -21,10 +21,10 @@ class RemainingTime {
|
|||
|
||||
void _updateRemainingSeconds() {
|
||||
final now = DateTime.now().toUtc();
|
||||
if (_expiryTime == null) return;
|
||||
final difference = _expiryTime!.difference(now).inSeconds;
|
||||
remainingSeconds.value = difference > 0 ? difference : 0;
|
||||
canResend.value = remainingSeconds.value <= 0;
|
||||
// debugPrint("Remaining seconds: ${remainingSeconds.value}");
|
||||
}
|
||||
|
||||
void startTimer() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue