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,
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
analyzer:
|
||||||
|
errors:
|
||||||
|
file_names: ignore
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lba/screens/auth/onboarding.dart';
|
import 'features/auth/presentation/pages/onboarding_page.dart';
|
||||||
import 'package:lba/screens/auth/cubit/auth_cubit.dart';
|
import 'injection_container.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(
|
sl.init();
|
||||||
BlocProvider(
|
runApp(const MyApp());
|
||||||
create: (context) => AuthCubit(),
|
|
||||||
child: const MyApp(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
|
|
@ -17,38 +15,41 @@ class MyApp extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return BlocProvider(
|
||||||
title: 'LBA',
|
create: (context) => sl.authBloc,
|
||||||
theme: ThemeData(
|
child: MaterialApp(
|
||||||
fontFamily: 'Roboto',
|
title: 'LBA',
|
||||||
scaffoldBackgroundColor: Colors.white,
|
theme: ThemeData(
|
||||||
primaryColor: const Color.fromARGB(255, 14, 63, 102),
|
fontFamily: 'Roboto',
|
||||||
buttonTheme: const ButtonThemeData(
|
scaffoldBackgroundColor: Colors.white,
|
||||||
buttonColor: Color.fromARGB(255, 14, 63, 102),
|
primaryColor: const Color.fromARGB(255, 14, 63, 102),
|
||||||
),
|
buttonTheme: const ButtonThemeData(
|
||||||
appBarTheme: const AppBarTheme(
|
buttonColor: Color.fromARGB(255, 14, 63, 102),
|
||||||
backgroundColor: Color.fromARGB(255, 14, 63, 102),
|
),
|
||||||
),
|
appBarTheme: const AppBarTheme(
|
||||||
dialogTheme: DialogTheme(
|
backgroundColor: Color.fromARGB(255, 14, 63, 102),
|
||||||
backgroundColor: Colors.white,
|
),
|
||||||
),
|
dialogTheme: DialogTheme(
|
||||||
dropdownMenuTheme: DropdownMenuThemeData(
|
backgroundColor: Colors.white,
|
||||||
menuStyle: MenuStyle(
|
),
|
||||||
backgroundColor: MaterialStatePropertyAll(Colors.white),
|
dropdownMenuTheme: DropdownMenuThemeData(
|
||||||
),
|
menuStyle: MenuStyle(
|
||||||
),
|
backgroundColor: MaterialStatePropertyAll(Colors.white),
|
||||||
inputDecorationTheme: const InputDecorationTheme(
|
),
|
||||||
labelStyle: TextStyle(color: Colors.black),
|
),
|
||||||
hintStyle: TextStyle(color: Colors.grey),
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
enabledBorder: OutlineInputBorder(
|
labelStyle: TextStyle(color: Colors.black),
|
||||||
borderSide: BorderSide(color: Colors.grey),
|
hintStyle: TextStyle(color: Colors.grey),
|
||||||
),
|
enabledBorder: OutlineInputBorder(
|
||||||
focusedBorder: OutlineInputBorder(
|
borderSide: BorderSide(color: Colors.grey),
|
||||||
borderSide: BorderSide(color: Color.fromARGB(255, 14, 63, 102), width: 2),
|
),
|
||||||
|
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 {
|
Future<void> sendOTP(String phoneNumber) async {
|
||||||
emit(AuthLoading());
|
emit(AuthLoading());
|
||||||
|
|
||||||
// Simulate API delay
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
// Mock data for development
|
|
||||||
_timeStamp = DateTime.now().millisecondsSinceEpoch.toString();
|
_timeStamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||||
_timeDue = DateTime.now().add(const Duration(minutes: 2)).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) {}
|
void verifyOTP(String otpCode) {}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ final class AuthLoading extends AuthState {}
|
||||||
|
|
||||||
final class AuthSuccess extends AuthState {
|
final class AuthSuccess extends AuthState {
|
||||||
final String phoneNumber;
|
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 {
|
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:country_pickers/country_pickers.dart';
|
||||||
import 'package:lba/extension/screenSize.dart';
|
import 'package:lba/extension/screenSize.dart';
|
||||||
import 'package:lba/gen/assets.gen.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/screens/auth/otpVerifcation.dart';
|
||||||
import 'package:lba/widgets/button.dart';
|
import 'package:lba/widgets/button.dart';
|
||||||
|
|
||||||
|
|
@ -148,9 +148,9 @@ class _LoginState extends State<Login> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: height / 50),
|
SizedBox(height: height / 50),
|
||||||
BlocConsumer<AuthCubit, AuthState>(
|
BlocConsumer<cubit.AuthCubit, cubit.AuthState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state is AuthSuccess) {
|
if (state is cubit.AuthSuccess) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
|
@ -159,7 +159,7 @@ class _LoginState extends State<Login> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (state is AuthError) {
|
if (state is cubit.AuthError) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
|
|
@ -170,7 +170,7 @@ class _LoginState extends State<Login> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is AuthLoading) {
|
if (state is cubit.AuthLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
return Column(
|
return Column(
|
||||||
|
|
@ -182,7 +182,7 @@ class _LoginState extends State<Login> {
|
||||||
color: const Color.fromARGB(255, 30, 137, 221),
|
color: const Color.fromARGB(255, 30, 137, 221),
|
||||||
text: "Get OTP",
|
text: "Get OTP",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<AuthCubit>().sendOTP(
|
context.read<cubit.AuthCubit>().sendOTP(
|
||||||
"+${_selectedCountry.phoneCode}${phoneController.text}",
|
"+${_selectedCountry.phoneCode}${phoneController.text}",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ class _OTPVerificationState extends State<OTPVerification> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// ignore: unused_local_variable
|
||||||
final width = MediaQuery.of(context).size.width;
|
final width = MediaQuery.of(context).size.width;
|
||||||
final height = MediaQuery.of(context).size.height;
|
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/gen/assets.gen.dart';
|
||||||
import 'package:lba/res/colors.dart';
|
import 'package:lba/res/colors.dart';
|
||||||
import 'package:lba/screens/mains/navigation/navigation.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/button.dart';
|
||||||
import 'package:lba/widgets/datePicker.dart';
|
|
||||||
|
|
||||||
class UserInfo extends StatefulWidget {
|
class UserInfo extends StatefulWidget {
|
||||||
const UserInfo({super.key});
|
const UserInfo({super.key});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// ignore_for_file: deprecated_member_use
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:lba/gen/assets.gen.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/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:lba/extension/screenSize.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/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// ignore_for_file: deprecated_member_use
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:lba/gen/assets.gen.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/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:lba/gen/assets.gen.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/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:lba/data/model/workingHours.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/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:lba/data/model/workingHours.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:flutter/material.dart';
|
||||||
import 'package:lba/extension/screenSize.dart';
|
import 'package:lba/extension/screenSize.dart';
|
||||||
import 'package:lba/res/colors.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/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:lba/gen/assets.gen.dart';
|
import 'package:lba/gen/assets.gen.dart';
|
||||||
|
|
@ -16,7 +18,7 @@ class CustomCard extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 320, // عرض کمی افزایش یافت
|
width: 320,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 5),
|
margin: const EdgeInsets.symmetric(horizontal: 5),
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// ignore_for_file: use_build_context_synchronously, deprecated_member_use
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// ignore_for_file: prefer_final_fields
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:lba/gen/assets.gen.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/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:lba/res/colors.dart';
|
import 'package:lba/res/colors.dart';
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class RemainingTime {
|
||||||
|
|
||||||
void initializeFromExpiry({required String expiryTimeString}) {
|
void initializeFromExpiry({required String expiryTimeString}) {
|
||||||
try {
|
try {
|
||||||
_expiryTime = DateTime.parse(expiryTimeString).toUtc();
|
_expiryTime = DateTime.fromMillisecondsSinceEpoch(int.parse(expiryTimeString)).toUtc();
|
||||||
_updateRemainingSeconds();
|
_updateRemainingSeconds();
|
||||||
startTimer();
|
startTimer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -21,10 +21,10 @@ class RemainingTime {
|
||||||
|
|
||||||
void _updateRemainingSeconds() {
|
void _updateRemainingSeconds() {
|
||||||
final now = DateTime.now().toUtc();
|
final now = DateTime.now().toUtc();
|
||||||
|
if (_expiryTime == null) return;
|
||||||
final difference = _expiryTime!.difference(now).inSeconds;
|
final difference = _expiryTime!.difference(now).inSeconds;
|
||||||
remainingSeconds.value = difference > 0 ? difference : 0;
|
remainingSeconds.value = difference > 0 ? difference : 0;
|
||||||
canResend.value = remainingSeconds.value <= 0;
|
canResend.value = remainingSeconds.value <= 0;
|
||||||
// debugPrint("Remaining seconds: ${remainingSeconds.value}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void startTimer() {
|
void startTimer() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue