Finished Base

This commit is contained in:
mohamadmahdi jebeli 2025-07-26 14:32:07 +03:30
parent 050fb6b620
commit 6f6e34e391
30 changed files with 1384 additions and 861 deletions

Binary file not shown.

View File

@ -4,5 +4,7 @@ class ApiConfig {
static const String verifyCode = "/login/getcode"; static const String verifyCode = "/login/getcode";
static const String updateUser = "/user/updateName"; static const String updateUser = "/user/updateName";
static const String updateCategories = "/user/favoriteCategory"; static const String updateCategories = "/user/favoriteCategory";
static const String getFavoriteCategories = "/user/getfavoriteCategory"; // این خط اضافه شد static const String getFavoriteCategories = "/user/getfavoriteCategory";
static const String addReservation = "/reservation/add";
static const String getReservations = "/reservation/get"; // <-- این خط اضافه شد
} }

View File

@ -0,0 +1,12 @@
// lib/core/config/http_overrides.dart
import 'dart:io';
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
}

View File

@ -1,11 +1,7 @@
// lib/data/models/offer_model.dart
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:proxibuy/data/models/comment_model.dart'; import 'package:proxibuy/data/models/comment_model.dart';
import 'package:proxibuy/data/models/discount_info_model.dart';
import 'package:proxibuy/data/models/working_hours.dart'; import 'package:proxibuy/data/models/working_hours.dart';
// کلاس کمکی برای دادههای فروشگاه
class ShopData { class ShopData {
final String id; final String id;
final String name; final String name;
@ -31,8 +27,8 @@ class ShopData {
name: json['Name'] ?? 'نام فروشگاه نامشخص', name: json['Name'] ?? 'نام فروشگاه نامشخص',
category: json['Category'] ?? 'بدون دسته‌بندی', category: json['Category'] ?? 'بدون دسته‌بندی',
address: json['Address'] ?? 'آدرس نامشخص', address: json['Address'] ?? 'آدرس نامشخص',
latitude: (json['Map']['coordinates'][1] as num?)?.toDouble() ?? 0.0, latitude: (json['Map']?['coordinates']?[1] as num?)?.toDouble() ?? 0.0,
longitude: (json['Map']['coordinates'][0] as num?)?.toDouble() ?? 0.0, longitude: (json['Map']?['coordinates']?[0] as num?)?.toDouble() ?? 0.0,
properties: List<String>.from(json['Property'] ?? []), properties: List<String>.from(json['Property'] ?? []),
); );
} }
@ -58,9 +54,8 @@ class OfferModel extends Equatable {
final double originalPrice; final double originalPrice;
final double finalPrice; final double finalPrice;
final List<String> features; final List<String> features;
final DiscountInfoModel? discountInfo; final String discountInfo;
final List<CommentModel> comments; final List<CommentModel> comments;
final String qrCodeData;
const OfferModel({ const OfferModel({
required this.id, required this.id,
@ -82,92 +77,82 @@ class OfferModel extends Equatable {
required this.originalPrice, required this.originalPrice,
required this.finalPrice, required this.finalPrice,
this.features = const [], this.features = const [],
this.discountInfo, required this.discountInfo,
this.comments = const [], this.comments = const [],
required this.qrCodeData,
}); });
factory OfferModel.fromJson(Map<String, dynamic> json) { // <-- پارامتر calculatedDistance حذف شد factory OfferModel.fromJson(Map<String, dynamic> json) {
final shopData = ShopData.fromJson(json['shopData']); final shopDataJson = json['shopData'] as Map<String, dynamic>? ?? {};
final shopData = ShopData.fromJson(shopDataJson);
final now = DateTime.now(); final now = DateTime.now();
bool checkIsOpen = false; bool checkIsOpen = false;
try { try {
final startTimeParts = (json['StartTime'] as String).split(':'); final startTimeString = json['StartTime'] as String?;
final endTimeParts = (json['EndTime'] as String).split(':'); final endTimeString = json['EndTime'] as String?;
final startHour = int.parse(startTimeParts[0]); if (startTimeString != null && endTimeString != null) {
final startMinute = int.parse(startTimeParts[1]); final startTimeParts = startTimeString.split(':');
final endHour = int.parse(endTimeParts[0]); final endTimeParts = endTimeString.split(':');
final endMinute = int.parse(endTimeParts[1]); final startHour = int.parse(startTimeParts[0]);
final startMinute = int.parse(startTimeParts[1]);
final endHour = int.parse(endTimeParts[0]);
final endMinute = int.parse(endTimeParts[1]);
final startTime = DateTime(now.year, now.month, now.day, startHour, startMinute); final startTime = DateTime(now.year, now.month, now.day, startHour, startMinute);
final endTime = DateTime(now.year, now.month, now.day, endHour, endMinute); final endTime = DateTime(now.year, now.month, now.day, endHour, endMinute);
checkIsOpen = now.isAfter(startTime) && now.isBefore(endTime); checkIsOpen = now.isAfter(startTime) && now.isBefore(endTime);
}
} catch(e) { } catch(e) {
checkIsOpen = false; checkIsOpen = false;
} }
final originalPriceValue = (json['Price'] as num?)?.toDouble() ?? 0.0; final originalPriceValue = (json['Price'] as num?)?.toDouble() ?? 0.0;
final finalPriceValue = (json['NPrice'] as num?)?.toDouble() ?? 0.0; final finalPriceValue = (json['NPrice'] as num?)?.toDouble() ?? 0.0;
// **رفع خطا: تبدیل امن عدد فاصله به int**
final distanceFromServer = (json['distance'] as num?)?.round() ?? 0;
// ******** شروع تغییر ۱ ********
// مدیریت هوشمند فاصله و عکسها از منابع مختلف
final distanceFromServer = (json['distance'] as num?)?.round() ?? (json['Distance'] as num?)?.round() ?? 0;
List<String> images = [];
if (json['imageData'] is List) { // برای سازگاری با پاسخ MQTT
images = (json['imageData'] as List<dynamic>)
.map((imageData) => imageData['Url']?.toString() ?? '')
.where((url) => url.isNotEmpty)
.toList();
} else if (json['Images'] is List) { // برای پاسخ جدید API رزرو
images = (json['Images'] as List<dynamic>)
.map((imageData) => (imageData is Map) ? imageData['Url']?.toString() ?? '' : '')
.where((url) => url.isNotEmpty)
.toList();
}
final typeData = json['typeData'] ?? json['Type'];
final discountTypeName = (typeData is Map) ? typeData['Name']?.toString() : typeData?.toString();
// ******** پایان تغییر ۱ ********
return OfferModel( return OfferModel(
id: json['ID'] ?? '', id: json['ID'] ?? '',
title: json['Name'] ?? 'بدون عنوان', title: json['Name'] ?? 'بدون عنوان',
discount: json['Description'] ?? '', discount: json['Description'] ?? '',
imageUrls: (json['Images'] as List<dynamic>?) imageUrls: images, // <-- استفاده از لیست جدید
?.map((imgId) => "$imgId") category: json['categoryData']?['Name']?.toString() ?? '',
.toList() ?? [],
category: json['shopData']['Category']?.toString() ?? 'بدون دسته‌بندی',
expiryTime: DateTime.tryParse(json['EndDate'] ?? '') ?? DateTime.now().add(const Duration(days: 1)), expiryTime: DateTime.tryParse(json['EndDate'] ?? '') ?? DateTime.now().add(const Duration(days: 1)),
discountType: json['Type']?.toString() ?? '', discountType: discountTypeName ?? '',
originalPrice: originalPriceValue, originalPrice: originalPriceValue,
finalPrice: finalPriceValue, finalPrice: finalPriceValue,
qrCodeData: json['QRcode'] ?? '',
storeName: shopData.name, storeName: shopData.name,
address: shopData.address, address: shopData.address,
latitude: shopData.latitude, latitude: shopData.latitude,
longitude: shopData.longitude, longitude: shopData.longitude,
features: shopData.properties, features: shopData.properties,
distanceInMeters: distanceFromServer, // <-- **استفاده از مسافت سرور** distanceInMeters: distanceFromServer, // <-- استفاده از متغیر جدید
isOpen: checkIsOpen, isOpen: checkIsOpen,
workingHours: [], workingHours: [],
rating: 0.0, rating: 0.0,
ratingCount: 0, ratingCount: 0,
comments: [], comments: [],
discountInfo: null, discountInfo: json['Description'],
);
}
OfferModel copyWith() {
return OfferModel(
id: id,
storeName: storeName,
title: title,
discount: discount,
imageUrls: imageUrls,
category: category,
distanceInMeters: distanceInMeters,
expiryTime: expiryTime,
address: address,
workingHours: workingHours,
discountType: discountType,
isOpen: isOpen,
rating: rating,
ratingCount: ratingCount,
latitude: latitude,
longitude: longitude,
originalPrice: originalPrice,
finalPrice: finalPrice,
features: features,
discountInfo: discountInfo,
comments: comments,
qrCodeData: qrCodeData,
); );
} }

View File

@ -1,11 +1,11 @@
import 'dart:io';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
// ignore: depend_on_referenced_packages // ignore: depend_on_referenced_packages
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; import 'package:proxibuy/core/config/http_overrides.dart'; // این خط را اضافه کنید
import 'package:proxibuy/data/repositories/offer_repository.dart';
import 'package:proxibuy/firebase_options.dart'; import 'package:proxibuy/firebase_options.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
@ -18,6 +18,8 @@ import 'package:proxibuy/presentation/pages/splash_screen.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
HttpOverrides.global = MyHttpOverrides();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
Animate.restartOnHotReload = true; Animate.restartOnHotReload = true;
runApp(const MyApp()); runApp(const MyApp());
@ -36,7 +38,6 @@ class MyApp extends StatelessWidget {
BlocProvider<AuthBloc>( BlocProvider<AuthBloc>(
create: (context) => AuthBloc()..add(CheckAuthStatusEvent()), create: (context) => AuthBloc()..add(CheckAuthStatusEvent()),
), ),
// RepositoryProvider برای OfferRepository حذف شد
BlocProvider<ReservationCubit>( BlocProvider<ReservationCubit>(
create: (context) => ReservationCubit(), create: (context) => ReservationCubit(),
), ),
@ -46,6 +47,7 @@ class MyApp extends StatelessWidget {
BlocProvider<NotificationPreferencesBloc>( BlocProvider<NotificationPreferencesBloc>(
create: (context) => NotificationPreferencesBloc(), create: (context) => NotificationPreferencesBloc(),
), ),
], ],
child: MaterialApp( child: MaterialApp(
title: 'Proxibuy', title: 'Proxibuy',
@ -124,38 +126,4 @@ class MyApp extends StatelessWidget {
), ),
); );
} }
} }
// class AppRouter extends StatelessWidget {
// const AppRouter({super.key});
// @override
// Widget build(BuildContext context) {
// final authState = context.select((AuthBloc bloc) => bloc.state);
// if (authState is AuthCodeSentSuccess) {
// return OtpPage(
// phoneNumber: "+${authState.countryCode}${authState.phone}",
// phone: authState.phone,
// countryCode: authState.countryCode,
// );
// }
// if (authState is AuthLoading) {
// final currentState = context.read<AuthBloc>().state;
// if (currentState is! AuthCodeSentSuccess) {
// return const Scaffold(body: Center(child: CircularProgressIndicator()));
// }
// }
// if (authState is AuthSuccess) {
// return const OffersPage();
// }
// if (authState is AuthNeedsInfo) {
// return const UserInfoPage();
// }
// return const OnboardingPage();
// }
// }

View File

@ -3,6 +3,7 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:proxibuy/core/config/api_config.dart'; import 'package:proxibuy/core/config/api_config.dart';
@ -49,6 +50,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
ApiConfig.baseUrl + ApiConfig.sendCode, ApiConfig.baseUrl + ApiConfig.sendCode,
data: {'Phone': event.phoneNumber, 'Code': event.countryCode}, data: {'Phone': event.phoneNumber, 'Code': event.countryCode},
); );
if (isClosed) return;
if (response.statusCode == 200) { if (response.statusCode == 200) {
emit(AuthCodeSentSuccess( emit(AuthCodeSentSuccess(
phone: event.phoneNumber, phone: event.phoneNumber,
@ -58,6 +60,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد')); emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد'));
} }
} on DioException catch (e) { } on DioException catch (e) {
if (isClosed) return;
emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
} }
} }
@ -73,46 +76,74 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
'OTP': event.otp, 'OTP': event.otp,
}, },
); );
if (isClosed) return; // FIX: Add check here
if (response.statusCode == 200) { if (response.statusCode == 200) {
final accessToken = response.data['data']['accessToken']; final accessToken = response.data['data']['accessToken'];
final refreshToken = response.data['data']['refreshToken']; final refreshToken = response.data['data']['refreshToken'];
final userID = response.data['data']['ID']; // <-- خط جدید: استخراج ID final userID = response.data['data']['ID'];
await _storage.write(key: 'accessToken', value: accessToken); await _storage.write(key: 'accessToken', value: accessToken);
await _storage.write(key: 'refreshToken', value: refreshToken); await _storage.write(key: 'refreshToken', value: refreshToken);
await _storage.write(key: 'userID', value: userID); // <-- خط جدید: ذخیره ID await _storage.write(key: 'userID', value: userID);
emit(AuthNeedsInfo()); emit(AuthNeedsInfo());
} else { } else {
emit(AuthFailure(response.data['message'] ?? 'کد صحیح نیست')); emit(AuthFailure(response.data['message'] ?? 'کد صحیح نیست'));
} }
} on DioException catch (e) { } on DioException catch (e) {
if (isClosed) return; // FIX: Add check here
emit(AuthFailure(e.response?.data['message'] ?? 'خطایی در سرور رخ داد')); emit(AuthFailure(e.response?.data['message'] ?? 'خطایی در سرور رخ داد'));
} }
} }
Future<void> _onUpdateUserInfo( Future<void> _onUpdateUserInfo(
UpdateUserInfoEvent event, Emitter<AuthState> emit) async { UpdateUserInfoEvent event, Emitter<AuthState> emit) async {
// ******** لاگ ۱: شروع پردازش ********
debugPrint("AuthBloc: 🔵 ایونت UpdateUserInfoEvent دریافت شد با نام: ${event.name}");
emit(AuthLoading()); emit(AuthLoading());
try { try {
final token = await _storage.read(key: 'accessToken'); final token = await _storage.read(key: 'accessToken');
if (token == null) { if (token == null) {
// ******** لاگ خطا: توکن وجود ندارد ********
debugPrint("AuthBloc: 🔴 خطا: توکن کاربر یافت نشد.");
emit(const AuthFailure("شما وارد نشده‌اید.")); emit(const AuthFailure("شما وارد نشده‌اید."));
return; return;
} }
// ******** لاگ ۲: ارسال درخواست به سرور ********
debugPrint("AuthBloc: 🟡 در حال ارسال درخواست آپدیت به سرور...");
final response = await _dio.post( final response = await _dio.post(
ApiConfig.baseUrl + ApiConfig.updateUser, ApiConfig.baseUrl + ApiConfig.updateUser,
data: {'Name': event.name, 'Gender': event.gender}, data: {'Name': event.name, 'Gender': event.gender},
options: Options(headers: {'Authorization': 'Bearer $token'}), options: Options(headers: {'Authorization': 'Bearer $token'}),
); );
// ******** لاگ ۳: پاسخ سرور ********
debugPrint("AuthBloc: 🟠 پاسخ سرور دریافت شد. StatusCode: ${response.statusCode}");
if (isClosed) {
debugPrint("AuthBloc: 🔴 خطا: BLoC قبل از اتمام عملیات بسته شده است.");
return;
}
if (response.statusCode == 200) { if (response.statusCode == 200) {
// ******** لاگ ۴: موفقیتآمیز بودن عملیات ********
debugPrint("AuthBloc: ✅ درخواست موفق بود. در حال emit کردن AuthSuccess...");
emit(AuthSuccess()); emit(AuthSuccess());
} else { } else {
// ******** لاگ خطا: پاسخ ناموفق از سرور ********
debugPrint("AuthBloc: 🔴 سرور پاسخ ناموفق داد: ${response.data['message']}");
emit(AuthFailure(response.data['message'] ?? 'خطا در ثبت اطلاعات')); emit(AuthFailure(response.data['message'] ?? 'خطا در ثبت اطلاعات'));
} }
} on DioException catch (e) { } on DioException catch (e) {
// ******** لاگ خطا: خطای Dio ********
debugPrint("AuthBloc: 🔴 خطای DioException رخ داد: ${e.response?.data['message']}");
if (isClosed) return;
emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
} }
} }
Future<void> _onLogout(LogoutEvent event, Emitter<AuthState> emit) async { Future<void> _onLogout(LogoutEvent event, Emitter<AuthState> emit) async {
await _storage.deleteAll(); await _storage.deleteAll();
emit(AuthInitial()); emit(AuthInitial());

View File

@ -16,7 +16,7 @@ class NotificationPreferencesBloc
on<LoadCategories>(_onLoadCategories); on<LoadCategories>(_onLoadCategories);
on<ToggleCategorySelection>(_onToggleCategorySelection); on<ToggleCategorySelection>(_onToggleCategorySelection);
on<SubmitPreferences>(_onSubmitPreferences); on<SubmitPreferences>(_onSubmitPreferences);
on<LoadFavoriteCategories>(_onLoadFavoriteCategories); // این خط اضافه شد on<LoadFavoriteCategories>(_onLoadFavoriteCategories);
add(LoadCategories()); add(LoadCategories());
} }
@ -55,6 +55,7 @@ class NotificationPreferencesBloc
try { try {
final token = await _storage.read(key: 'accessToken'); final token = await _storage.read(key: 'accessToken');
if (token == null) { if (token == null) {
if (isClosed) return; // بررسی قبل از emit
emit(state.copyWith(isLoading: false, errorMessage: "شما وارد نشده‌اید.")); emit(state.copyWith(isLoading: false, errorMessage: "شما وارد نشده‌اید."));
return; return;
} }
@ -65,6 +66,8 @@ class NotificationPreferencesBloc
options: Options(headers: {'Authorization': 'Bearer $token'}), options: Options(headers: {'Authorization': 'Bearer $token'}),
); );
if (isClosed) return; // بررسی قبل از emit
if (response.statusCode == 200) { if (response.statusCode == 200) {
emit(state.copyWith(isLoading: false, submissionSuccess: true)); emit(state.copyWith(isLoading: false, submissionSuccess: true));
} else { } else {
@ -73,19 +76,20 @@ class NotificationPreferencesBloc
errorMessage: response.data['message'] ?? 'خطا در ثبت اطلاعات')); errorMessage: response.data['message'] ?? 'خطا در ثبت اطلاعات'));
} }
} on DioException catch (e) { } on DioException catch (e) {
if (isClosed) return; // بررسی قبل از emit
emit(state.copyWith( emit(state.copyWith(
isLoading: false, isLoading: false,
errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
} }
} }
// این متد اضافه شد
Future<void> _onLoadFavoriteCategories( Future<void> _onLoadFavoriteCategories(
LoadFavoriteCategories event, Emitter<NotificationPreferencesState> emit) async { LoadFavoriteCategories event, Emitter<NotificationPreferencesState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null)); emit(state.copyWith(isLoading: true, errorMessage: null));
try { try {
final token = await _storage.read(key: 'accessToken'); final token = await _storage.read(key: 'accessToken');
if (token == null) { if (token == null) {
if (isClosed) return; // بررسی قبل از emit
emit(state.copyWith(isLoading: false, errorMessage: "شما وارد نشده‌اید.")); emit(state.copyWith(isLoading: false, errorMessage: "شما وارد نشده‌اید."));
return; return;
} }
@ -95,6 +99,8 @@ class NotificationPreferencesBloc
options: Options(headers: {'Authorization': 'Bearer $token'}), options: Options(headers: {'Authorization': 'Bearer $token'}),
); );
if (isClosed) return; // بررسی قبل از emit
if (response.statusCode == 200) { if (response.statusCode == 200) {
final List<dynamic> fCategory = response.data['data']['FCategory']; final List<dynamic> fCategory = response.data['data']['FCategory'];
final Set<String> favoriteCategoryIds = final Set<String> favoriteCategoryIds =
@ -109,6 +115,7 @@ class NotificationPreferencesBloc
errorMessage: response.data['message'] ?? 'خطا در دریافت اطلاعات')); errorMessage: response.data['message'] ?? 'خطا در دریافت اطلاعات'));
} }
} on DioException catch (e) { } on DioException catch (e) {
if (isClosed) return; // بررسی قبل از emit
emit(state.copyWith( emit(state.copyWith(
isLoading: false, isLoading: false,
errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));

View File

@ -1,5 +1,3 @@
// lib/presentation/offer/bloc/offer_bloc.dart
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_event.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_event.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_state.dart';
@ -7,35 +5,21 @@ import 'package:proxibuy/presentation/offer/bloc/offer_state.dart';
class OffersBloc extends Bloc<OffersEvent, OffersState> { class OffersBloc extends Bloc<OffersEvent, OffersState> {
OffersBloc() : super(OffersInitial()) { OffersBloc() : super(OffersInitial()) {
on<OffersReceivedFromMqtt>(_onOffersReceivedFromMqtt); on<OffersReceivedFromMqtt>(_onOffersReceivedFromMqtt);
on<ClearOffers>(_onClearOffers); // رویداد جدید برای پاک کردن دیتا on<ClearOffers>(_onClearOffers);
} }
void _onOffersReceivedFromMqtt( void _onOffersReceivedFromMqtt(
OffersReceivedFromMqtt event, OffersReceivedFromMqtt event,
Emitter<OffersState> emit, Emitter<OffersState> emit,
) { ) {
// فقط در صورتی که لیست جدید خالی نباشد، آن را جایگزین کن // همیشه حالت موفقیت را با لیست دریافتی منتشر کن (حتی اگر خالی باشد).
if (event.offers.isNotEmpty) { // این کار تضمین میکند که صفحه از حالت لودینگ خارج میشود و رابط کاربری
emit(OffersLoadSuccess(event.offers)); // آخرین وضعیت (وجود یا عدم وجود تخفیف) را نمایش میدهد.
}
// اگر لیست جدید خالی بود، و قبلا دیتایی داشتیم، حالت را تغییر نده
// این کار از نمایش صفحه خالی جلوگیری میکند
else if (state is! OffersLoadSuccess) {
// اگر اولین بار است و لیست خالی است، حالت موفقیت با لیست خالی را نشان بده
emit(const OffersLoadSuccess([]));
}
}
// برای زمانی که مثلا کاربر GPS را خاموش میکند
void _onClearOffers(ClearOffers event, Emitter<OffersState> emit) {
emit(OffersInitial());
}
}
// مدیریت رویداد جدید
void _onOffersReceivedFromMqtt(
OffersReceivedFromMqtt event,
Emitter<OffersState> emit,
) {
// جایگزین کردن لیست پیشنهادها با دادههای جدید
emit(OffersLoadSuccess(event.offers)); emit(OffersLoadSuccess(event.offers));
} }
// برای زمانی که مثلا کاربر GPS را خاموش میکند
void _onClearOffers(ClearOffers event, Emitter<OffersState> emit) {
emit(OffersInitial());
}
}

View File

@ -44,8 +44,7 @@ class CategoryOffersRow extends StatelessWidget {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) { builder: (_) {
// کل آبجکت offer پاس داده میشود return ProductDetailPage(offer: offer,);
return ProductDetailPage(offer: offer);
}, },
), ),
); );

View File

@ -33,7 +33,11 @@ class _OfferCardState extends State<OfferCard> {
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
child: Column( child: Column(
children: [ children: [
Stack(children: [_buildOfferImage()]), // Hero ویجت به اینجا اضافه شد
Hero(
tag: 'offer_image_${widget.offer.id}',
child: _buildOfferImage(),
),
_buildInfoContainer(textTheme), _buildInfoContainer(textTheme),
], ],
), ),
@ -53,18 +57,16 @@ class _OfferCardState extends State<OfferCard> {
height: 140, height: 140,
width: double.infinity, width: double.infinity,
fit: BoxFit.cover, fit: BoxFit.cover,
placeholder: placeholder: (context, url) => Container(
(context, url) => Container( height: 140,
height: 140, color: Colors.grey[300],
color: Colors.grey[300], child: const Center(child: CircularProgressIndicator()),
child: const Center(child: CircularProgressIndicator()), ),
), errorWidget: (context, url, error) => Container(
errorWidget: height: 140,
(context, url, error) => Container( color: Colors.grey[300],
height: 140, child: const Icon(Icons.broken_image, color: Colors.grey),
color: Colors.grey[300], ),
child: const Icon(Icons.broken_image, color: Colors.grey),
),
), ),
); );
} }
@ -80,7 +82,6 @@ class _OfferCardState extends State<OfferCard> {
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
// ignore: deprecated_member_use
color: Colors.grey.withOpacity(0.2), color: Colors.grey.withOpacity(0.2),
spreadRadius: 2, spreadRadius: 2,
blurRadius: 5, blurRadius: 5,
@ -117,7 +118,6 @@ class _OfferCardState extends State<OfferCard> {
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Row( Row(
children: [ children: [
SvgPicture.asset(Assets.icons.location.path), SvgPicture.asset(Assets.icons.location.path),
@ -127,26 +127,25 @@ class _OfferCardState extends State<OfferCard> {
widget.offer.address, widget.offer.address,
style: textTheme.bodySmall, style: textTheme.bodySmall,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'(${widget.offer.distanceInMeters.toString()}متر تا تخفیف)', '(${widget.offer.distanceInMeters.toString()} متر تا تخفیف)',
style: textTheme.bodySmall, style: textTheme.bodySmall,
), ),
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Row( Row(
children: [ children: [
SvgPicture.asset(Assets.icons.routing.path), SvgPicture.asset(Assets.icons.routing.path),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'نوع تخفیف : ${widget.offer.discount} ${widget.offer.discountType}', 'نوع تخفیف : ${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}% ${widget.offer.discountType}',
style: const TextStyle(color: Color.fromARGB(255, 183, 28, 28),fontSize: 14), style: const TextStyle(
color: Color.fromARGB(255, 183, 28, 28), fontSize: 14),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

View File

@ -1,7 +1,10 @@
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/data/models/offer_model.dart';
@ -9,7 +12,6 @@ import 'package:proxibuy/features/add_photo/cubit/add_photo_cubit.dart';
import 'package:proxibuy/presentation/pages/reservation_details_screen.dart'; import 'package:proxibuy/presentation/pages/reservation_details_screen.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/flutter_staggered_grid_view.dart'; import 'package:proxibuy/presentation/widgets/flutter_staggered_grid_view.dart';
import 'package:image_picker/image_picker.dart'; // ایمپورت جدید
class AddPhotoScreen extends StatelessWidget { class AddPhotoScreen extends StatelessWidget {
final String storeName; final String storeName;
@ -23,6 +25,31 @@ class AddPhotoScreen extends StatelessWidget {
required this.offer, required this.offer,
}); });
// متد ساخت توکن
Future<String> _generateQrToken(BuildContext context) async {
const storage = FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
if (userID == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("خطا: کاربر شناسایی نشد.")),
);
throw Exception("User ID not found");
}
final payload = {
'userID': userID,
'discountID': offer.id,
'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
};
const secretKey = 'your_super_secret_key_for_qr';
final jwt = JWT(payload);
final token = jwt.sign(SecretKey(secretKey));
return token;
}
void _showImageSourceActionSheet(BuildContext context) { void _showImageSourceActionSheet(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -34,7 +61,8 @@ class AddPhotoScreen extends StatelessWidget {
child: Wrap( child: Wrap(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
leading: const Icon(Icons.photo_library, color: AppColors.primary), leading:
const Icon(Icons.photo_library, color: AppColors.primary),
title: const Text('انتخاب از گالری'), title: const Text('انتخاب از گالری'),
onTap: () { onTap: () {
Navigator.of(bottomSheetContext).pop(); Navigator.of(bottomSheetContext).pop();
@ -42,7 +70,8 @@ class AddPhotoScreen extends StatelessWidget {
}, },
), ),
ListTile( ListTile(
leading: const Icon(Icons.camera_alt, color: AppColors.primary), leading:
const Icon(Icons.camera_alt, color: AppColors.primary),
title: const Text('گرفتن عکس با دوربین'), title: const Text('گرفتن عکس با دوربین'),
onTap: () { onTap: () {
Navigator.of(bottomSheetContext).pop(); Navigator.of(bottomSheetContext).pop();
@ -68,7 +97,9 @@ class AddPhotoScreen extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
SizedBox(height: 10,), SizedBox(
height: 10,
),
_buildHeader() _buildHeader()
.animate() .animate()
.fadeIn(duration: 500.ms) .fadeIn(duration: 500.ms)
@ -185,7 +216,6 @@ class AddPhotoScreen extends StatelessWidget {
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
// ignore: deprecated_member_use
color: Colors.black.withOpacity(0.08), color: Colors.black.withOpacity(0.08),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 4),
@ -288,18 +318,27 @@ class AddPhotoScreen extends StatelessWidget {
), ),
), ),
onPressed: () async { onPressed: () async {
context.read<ReservationCubit>().reserveProduct( try {
productId, final qrToken = await _generateQrToken(context);
);
Navigator.of(dialogContext).pop(); context
Navigator.push( .read<ReservationCubit>()
context, .reserveProduct(productId);
MaterialPageRoute(
builder: (_) => ReservationConfirmationPage( Navigator.of(dialogContext).pop();
offer: offer,
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ReservationConfirmationPage(
offer: offer,
qrCodeData: qrToken,
),
), ),
), );
); } catch (e) {
print("Error in reservation popup: $e");
}
}, },
child: const Text( child: const Text(
"رزرو محصول", "رزرو محصول",
@ -318,7 +357,6 @@ class AddPhotoScreen extends StatelessWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
// ignore: deprecated_member_use
color: Colors.black.withOpacity(0.3), color: Colors.black.withOpacity(0.3),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 4), offset: const Offset(0, 4),

View File

@ -1,6 +1,9 @@
// lib/presentation/pages/login_page.dart
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:country_picker/country_picker.dart'; import 'package:country_picker/country_picker.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/pages/otp_page.dart'; import 'package:proxibuy/presentation/pages/otp_page.dart';
import '../../core/gen/assets.gen.dart'; import '../../core/gen/assets.gen.dart';
@ -18,13 +21,12 @@ class _LoginPageState extends State<LoginPage> {
void _sendOtp() { void _sendOtp() {
context.read<AuthBloc>().add( context.read<AuthBloc>().add(
SendOTPEvent( SendOTPEvent(
phoneNumber: _phoneController.text, phoneNumber: _phoneController.text,
countryCode: _selectedCountry.phoneCode, countryCode: _selectedCountry.phoneCode,
), ),
); );
} }
// bool _keepSignedIn = false;
@override @override
void dispose() { void dispose() {
@ -46,7 +48,10 @@ class _LoginPageState extends State<LoginPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Center(child: Assets.icons.logo.svg(height: 160)), Hero(
tag: 'auth_logo',
child: Center(child: Assets.icons.logo.svg(height: 160)),
),
const SizedBox(height: 48), const SizedBox(height: 48),
Text( Text(
"ورود", "ورود",
@ -97,19 +102,6 @@ class _LoginPageState extends State<LoginPage> {
), ),
), ),
), ),
const SizedBox(height: 16),
// Row(
// children: [
// Checkbox(
// value: _keepSignedIn,
// onChanged:
// (value) =>
// setState(() => _keepSignedIn = value ?? false),
// activeColor: AppColors.primary,
// ),
// Text("مرا به خاطر بسپار", style: textTheme.bodyMedium),
// ],
// ),
const SizedBox(height: 24), const SizedBox(height: 24),
BlocConsumer<AuthBloc, AuthState>( BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) { listener: (context, state) {
@ -122,18 +114,20 @@ class _LoginPageState extends State<LoginPage> {
); );
} }
if (state is AuthCodeSentSuccess) { if (state is AuthCodeSentSuccess) {
// ******** شروع تغییر ۱ ********
// BlocProvider.value حذف شد
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: builder: (_) => OtpPage(
(_) => OtpPage( phoneNumber:
phoneNumber: "${state.countryCode}${state.phone}",
"${state.countryCode}${state.phone}", phone: state.phone,
phone: state.phone, countryCode: state.countryCode,
countryCode: state.countryCode, ),
),
), ),
); );
// ******** پایان تغییر ۱ ********
} }
}, },
builder: (context, state) { builder: (context, state) {
@ -143,17 +137,16 @@ class _LoginPageState extends State<LoginPage> {
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: isLoading ? null : _sendOtp, onPressed: isLoading ? null : _sendOtp,
child: child: isLoading
isLoading ? const SizedBox(
? const SizedBox( height: 20,
height: 20, width: 20,
width: 20, child: CircularProgressIndicator(
child: CircularProgressIndicator( color: Colors.white,
color: Colors.white, strokeWidth: 3,
strokeWidth: 3, ),
), )
) : const Text("کد یکبار مصرف"),
: const Text("کد یکبار مصرف"),
), ),
); );
}, },
@ -183,12 +176,13 @@ class _LoginPageState extends State<LoginPage> {
"ورود با حساب گوگل", "ورود با حساب گوگل",
style: TextStyle(color: Colors.black), style: TextStyle(color: Colors.black),
), ),
onPressed: () { onPressed: () {},
// TODO: Implement Google Sign-in
},
), ),
), ),
], ]
.animate(interval: 100.ms)
.fadeIn(duration: 400.ms, curve: Curves.easeOut)
.slideY(begin: 0.2, duration: 400.ms, curve: Curves.easeOut),
), ),
), ),
), ),
@ -214,4 +208,4 @@ class _LoginPageState extends State<LoginPage> {
onSelect: (Country country) => setState(() => _selectedCountry = country), onSelect: (Country country) => setState(() => _selectedCountry = country),
); );
} }
} }

View File

@ -1,5 +1,6 @@
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:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
@ -9,40 +10,52 @@ import 'package:proxibuy/presentation/pages/offers_page.dart';
import 'package:proxibuy/presentation/pages/reserved_list_page.dart'; import 'package:proxibuy/presentation/pages/reserved_list_page.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/category_selection_card.dart'; import 'package:proxibuy/presentation/widgets/category_selection_card.dart';
import 'package:proxibuy/services/mqtt_service.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class NotificationPreferencesPage extends StatefulWidget { class NotificationPreferencesPage extends StatefulWidget {
// This parameter is used to decide whether to fetch favorite categories on start
final bool loadFavoritesOnStart; final bool loadFavoritesOnStart;
// The constructor now accepts the 'loadFavoritesOnStart' parameter const NotificationPreferencesPage(
const NotificationPreferencesPage({super.key, this.loadFavoritesOnStart = false}); {super.key, this.loadFavoritesOnStart = false});
static Route<bool> route({bool loadFavorites = false}) {
return MaterialPageRoute<bool>(
builder: (_) => BlocProvider(
create: (context) => NotificationPreferencesBloc(),
// The widget is created here, passing the parameter correctly
child: NotificationPreferencesPage(loadFavoritesOnStart: loadFavorites),
),
);
}
@override @override
State<NotificationPreferencesPage> createState() => State<NotificationPreferencesPage> createState() =>
_NotificationPreferencesPageState(); _NotificationPreferencesPageState();
} }
class _NotificationPreferencesPageState extends State<NotificationPreferencesPage> { class _NotificationPreferencesPageState
extends State<NotificationPreferencesPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// If the flag is true, dispatch the event to load favorites from the API
if (widget.loadFavoritesOnStart) { if (widget.loadFavoritesOnStart) {
context.read<NotificationPreferencesBloc>().add(LoadFavoriteCategories()); context.read<NotificationPreferencesBloc>().add(LoadFavoriteCategories());
} }
} }
void _showConnectingDialog(BuildContext context) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return const Dialog(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(width: 20),
Text("در حال اتصال..."),
],
),
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -117,19 +130,41 @@ class _NotificationPreferencesPageState extends State<NotificationPreferencesPag
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
), ),
body: BlocListener<NotificationPreferencesBloc, NotificationPreferencesState>( body: BlocListener<NotificationPreferencesBloc,
listener: (context, state) { NotificationPreferencesState>(
listener: (context, state) async {
if (state.submissionSuccess) { if (state.submissionSuccess) {
// Pop the page and return 'true' to signal a successful update
if (Navigator.canPop(context)) { if (Navigator.canPop(context)) {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
} else { } else {
Navigator.pushReplacement( _showConnectingDialog(context);
context,
MaterialPageRoute( try {
builder: (context) => const OffersPage(showDialogsOnLoad: true), final mqttService = context.read<MqttService>();
), const storage = FlutterSecureStorage();
); final token = await storage.read(key: 'accessToken');
if (token != null && token.isNotEmpty) {
await mqttService.connect(token);
if (mounted && mqttService.isConnected) {
Navigator.of(context).pop();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => OffersPage(),
),
);
}
} else {
if (mounted) Navigator.of(context).pop();
}
} catch (e) {
if (mounted) Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("خطا در اتصال: $e")),
);
}
} }
} }
if (state.errorMessage != null) { if (state.errorMessage != null) {
@ -168,7 +203,8 @@ class _NotificationPreferencesPageState extends State<NotificationPreferencesPag
TextSpan( TextSpan(
text: text:
'ترجیح می‌دی از کدام دسته‌بندی‌ها اعلان تخفیف دریافت کنی؟ ', 'ترجیح می‌دی از کدام دسته‌بندی‌ها اعلان تخفیف دریافت کنی؟ ',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
), ),
TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'), TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'),
], ],
@ -176,7 +212,8 @@ class _NotificationPreferencesPageState extends State<NotificationPreferencesPag
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Expanded( Expanded(
child: BlocBuilder<NotificationPreferencesBloc, NotificationPreferencesState>( child: BlocBuilder<NotificationPreferencesBloc,
NotificationPreferencesState>(
builder: (context, state) { builder: (context, state) {
if (state.categories.isEmpty && state.isLoading) { if (state.categories.isEmpty && state.isLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@ -186,7 +223,10 @@ class _NotificationPreferencesPageState extends State<NotificationPreferencesPag
final double crossAxisSpacing = 16.0; final double crossAxisSpacing = 16.0;
final int crossAxisCount = 3; final int crossAxisCount = 3;
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final itemWidth = (screenWidth - (horizontalPadding * 2) - (crossAxisSpacing * (crossAxisCount - 1))) / crossAxisCount; final itemWidth = (screenWidth -
(horizontalPadding * 2) -
(crossAxisSpacing * (crossAxisCount - 1))) /
crossAxisCount;
final itemHeight = itemWidth / 0.9; final itemHeight = itemWidth / 0.9;
return SingleChildScrollView( return SingleChildScrollView(
@ -204,11 +244,11 @@ class _NotificationPreferencesPageState extends State<NotificationPreferencesPag
name: category.name, name: category.name,
icon: category.icon, icon: category.icon,
isSelected: isSelected, isSelected: isSelected,
showSelectableIndicator: state.selectedCategoryIds.isNotEmpty, showSelectableIndicator:
state.selectedCategoryIds.isNotEmpty,
onTap: () { onTap: () {
context context.read<NotificationPreferencesBloc>().add(
.read<NotificationPreferencesBloc>() ToggleCategorySelection(category.id));
.add(ToggleCategorySelection(category.id));
}, },
), ),
); );
@ -218,9 +258,11 @@ class _NotificationPreferencesPageState extends State<NotificationPreferencesPag
}, },
), ),
), ),
BlocBuilder<NotificationPreferencesBloc, NotificationPreferencesState>( BlocBuilder<NotificationPreferencesBloc,
NotificationPreferencesState>(
builder: (context, state) { builder: (context, state) {
final areCategoriesSelected = state.selectedCategoryIds.isNotEmpty; final areCategoriesSelected =
state.selectedCategoryIds.isNotEmpty;
if (areCategoriesSelected) { if (areCategoriesSelected) {
return SizedBox( return SizedBox(
@ -228,16 +270,23 @@ class _NotificationPreferencesPageState extends State<NotificationPreferencesPag
child: ElevatedButton( child: ElevatedButton(
onPressed: !state.isLoading onPressed: !state.isLoading
? () async { ? () async {
final bloc = context.read<NotificationPreferencesBloc>(); final bloc =
context.read<NotificationPreferencesBloc>();
final selectedCategoryNames = bloc.state.categories
.where((cat) => bloc.state.selectedCategoryIds.contains(cat.id)) final selectedCategoryNames = bloc
.state.categories
.where((cat) => bloc.state
.selectedCategoryIds
.contains(cat.id))
.map((cat) => cat.name) .map((cat) => cat.name)
.toList(); .toList();
final prefs = await SharedPreferences.getInstance(); final prefs =
await prefs.setStringList('user_selected_categories', selectedCategoryNames); await SharedPreferences.getInstance();
await prefs.setStringList(
'user_selected_categories',
selectedCategoryNames);
bloc.add(SubmitPreferences()); bloc.add(SubmitPreferences());
} }
: null, : null,
@ -251,7 +300,8 @@ class _NotificationPreferencesPageState extends State<NotificationPreferencesPag
), ),
), ),
child: state.isLoading child: state.isLoading
? const CircularProgressIndicator(color: Colors.white) ? const CircularProgressIndicator(
color: Colors.white)
: const Text( : const Text(
'اعمال', 'اعمال',
style: TextStyle( style: TextStyle(

View File

@ -1,13 +1,14 @@
// lib/presentation/pages/offers_page.dart
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:proxibuy/core/config/api_config.dart';
import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/data/models/offer_model.dart';
@ -18,11 +19,12 @@ import 'package:proxibuy/presentation/offer/bloc/widgets/category_offers_row.dar
import 'package:proxibuy/presentation/pages/notification_preferences_page.dart'; import 'package:proxibuy/presentation/pages/notification_preferences_page.dart';
import 'package:proxibuy/presentation/pages/reserved_list_page.dart'; import 'package:proxibuy/presentation/pages/reserved_list_page.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/gps_dialog.dart';
import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart';
import 'package:proxibuy/services/mqtt_service.dart'; import 'package:proxibuy/services/mqtt_service.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class OffersPage extends StatefulWidget { class OffersPage extends StatefulWidget {
// این پارامتر دیگر استفاده نمیشود اما برای سازگاری باقی میماند
final bool showDialogsOnLoad; final bool showDialogsOnLoad;
const OffersPage({super.key, this.showDialogsOnLoad = false}); const OffersPage({super.key, this.showDialogsOnLoad = false});
@ -35,6 +37,7 @@ class _OffersPageState extends State<OffersPage> {
List<String> _selectedCategories = []; List<String> _selectedCategories = [];
StreamSubscription? _locationServiceSubscription; StreamSubscription? _locationServiceSubscription;
StreamSubscription? _mqttMessageSubscription; StreamSubscription? _mqttMessageSubscription;
StreamSubscription? _connectivitySubscription;
Timer? _locationTimer; Timer? _locationTimer;
bool _isSubscribedToOffers = false; bool _isSubscribedToOffers = false;
bool _isGpsEnabled = false; bool _isGpsEnabled = false;
@ -43,19 +46,99 @@ class _OffersPageState extends State<OffersPage> {
void initState() { void initState() {
super.initState(); super.initState();
_initializePage(); _initializePage();
_initConnectivityListener();
_fetchInitialReservations(); // <-- فراخوانی متد جدید
} }
// ******** شروع متد جدید ********
// این متد تعداد اولیه رزروها را برای نمایش روی آیکون دریافت میکند
Future<void> _fetchInitialReservations() async {
try {
const storage = FlutterSecureStorage();
final token = await storage.read(key: 'accessToken');
if (token == null) return; // کاربر لاگین نکرده است
final dio = Dio();
final response = await dio.get(
ApiConfig.baseUrl + ApiConfig.getReservations,
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
if (response.statusCode == 200 && mounted) {
final List<dynamic> reserves = response.data['reserves'];
final List<String> reservedIds =
reserves
.map(
(reserveData) =>
(reserveData['Discount']['ID'] as String?) ?? '',
)
.where((id) => id.isNotEmpty)
.toList();
// بهروزرسانی Cubit با لیست شناسهها
context.read<ReservationCubit>().setReservedIds(reservedIds);
}
} catch (e) {
// اگر خطایی رخ دهد، مشکلی نیست. فقط عدد نمایش داده نمیشود
debugPrint("Error fetching initial reservations: $e");
}
}
// ******** پایان متد جدید ********
Future<void> _initializePage() async { Future<void> _initializePage() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (mounted) {
await showNotificationPermissionDialog(context);
await showGPSDialog(context);
}
});
await _loadPreferences(); await _loadPreferences();
_subscribeToUserOffersOnLoad(); _checkAndConnectMqtt();
_initLocationListener(); _initLocationListener();
} }
void _initConnectivityListener() {
_connectivitySubscription = Connectivity().onConnectivityChanged.listen((
List<ConnectivityResult> results,
) {
if (!results.contains(ConnectivityResult.none)) {
print(" Network connection restored.");
_checkAndConnectMqtt();
} else {
print(" Network connection lost.");
}
});
}
Future<void> _checkAndConnectMqtt() async {
final mqttService = context.read<MqttService>();
if (!mqttService.isConnected) {
print("--- OffersPage: MQTT not connected. Attempting to connect...");
final storage = const FlutterSecureStorage();
final token = await storage.read(key: 'accessToken');
if (token != null && token.isNotEmpty) {
try {
await mqttService.connect(token);
if (mqttService.isConnected && mounted) {
_subscribeToUserOffersOnLoad();
}
} catch (e) {
print("❌ OffersPage: Error connecting to MQTT: $e");
}
}
} else {
print("--- OffersPage: MQTT already connected.");
_subscribeToUserOffersOnLoad();
}
}
@override @override
void dispose() { void dispose() {
_locationServiceSubscription?.cancel(); _locationServiceSubscription?.cancel();
_mqttMessageSubscription?.cancel(); _mqttMessageSubscription?.cancel();
_locationTimer?.cancel(); _locationTimer?.cancel();
_connectivitySubscription?.cancel();
super.dispose(); super.dispose();
} }
@ -69,8 +152,9 @@ class _OffersPageState extends State<OffersPage> {
void _initLocationListener() { void _initLocationListener() {
_checkInitialGpsStatus(); _checkInitialGpsStatus();
_locationServiceSubscription = _locationServiceSubscription = Geolocator.getServiceStatusStream().listen((
Geolocator.getServiceStatusStream().listen((status) { status,
) {
final isEnabled = status == ServiceStatus.enabled; final isEnabled = status == ServiceStatus.enabled;
if (mounted && _isGpsEnabled != isEnabled) { if (mounted && _isGpsEnabled != isEnabled) {
setState(() { setState(() {
@ -87,7 +171,7 @@ class _OffersPageState extends State<OffersPage> {
} }
}); });
} }
Future<void> _checkInitialGpsStatus() async { Future<void> _checkInitialGpsStatus() async {
final status = await Geolocator.isLocationServiceEnabled(); final status = await Geolocator.isLocationServiceEnabled();
if (mounted) { if (mounted) {
@ -103,7 +187,7 @@ class _OffersPageState extends State<OffersPage> {
void _startSendingLocationUpdates() { void _startSendingLocationUpdates() {
print("🚀 Starting periodic location updates."); print("🚀 Starting periodic location updates.");
_locationTimer?.cancel(); _locationTimer?.cancel();
_locationTimer = Timer.periodic(const Duration(seconds: 15), (timer) { _locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
_sendLocationUpdate(); _sendLocationUpdate();
}); });
_sendLocationUpdate(); _sendLocationUpdate();
@ -121,7 +205,7 @@ class _OffersPageState extends State<OffersPage> {
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
} }
if (permission == LocationPermission.denied || if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) { permission == LocationPermission.deniedForever) {
print("🚫 Location permission denied by user."); print("🚫 Location permission denied by user.");
@ -140,14 +224,9 @@ class _OffersPageState extends State<OffersPage> {
return; return;
} }
final payload = { final payload = {"userID": userID, "lat": 32.6685, "lng": 51.6826};
"userID": userID,
"lat": 32.6685,
"lng": 51.6826,
};
mqttService.publish("proxybuy/sendGps", payload); mqttService.publish("proxybuy/sendGps", payload);
} catch (e) { } catch (e) {
print("❌ Error sending location update in OffersPage: $e"); print("❌ Error sending location update in OffersPage: $e");
} }
@ -163,13 +242,22 @@ class _OffersPageState extends State<OffersPage> {
_mqttMessageSubscription = mqttService.messages.listen((message) { _mqttMessageSubscription = mqttService.messages.listen((message) {
final data = message['data']; final data = message['data'];
if (data != null && data is List) {
if (data == null) {
if (mounted) {
context.read<OffersBloc>().add(const OffersReceivedFromMqtt([]));
}
return;
}
if (data is List) {
try { try {
List<OfferModel> offers = data List<OfferModel> offers =
.whereType<Map<String, dynamic>>() data
.map((json) => OfferModel.fromJson(json)) .whereType<Map<String, dynamic>>()
.toList(); .map((json) => OfferModel.fromJson(json))
.toList();
if (mounted) { if (mounted) {
context.read<OffersBloc>().add(OffersReceivedFromMqtt(offers)); context.read<OffersBloc>().add(OffersReceivedFromMqtt(offers));
} }
@ -209,9 +297,14 @@ class _OffersPageState extends State<OffersPage> {
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final result = await Navigator.of(context).push<bool>( final result = await Navigator.of(context).push<bool>(
NotificationPreferencesPage.route(loadFavorites: true), MaterialPageRoute(
builder:
(context) => const NotificationPreferencesPage(
loadFavoritesOnStart: true,
),
),
); );
if (result == true && mounted) { if (result == true && mounted) {
_loadPreferences(); _loadPreferences();
} }
@ -348,7 +441,10 @@ class _OffersPageState extends State<OffersPage> {
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [_buildFavoriteCategoriesSection(), OffersView(isGpsEnabled: _isGpsEnabled)], children: [
_buildFavoriteCategoriesSection(),
OffersView(isGpsEnabled: _isGpsEnabled),
],
), ),
), ),
), ),
@ -384,7 +480,7 @@ class OffersView extends StatelessWidget {
), ),
); );
} }
if (state is OffersLoadSuccess) { if (state is OffersLoadSuccess) {
if (state.offers.isEmpty) { if (state.offers.isEmpty) {
return const SizedBox( return const SizedBox(
@ -426,14 +522,14 @@ class OffersView extends StatelessWidget {
}, },
); );
} }
if (state is OffersLoadFailure) { if (state is OffersLoadFailure) {
return SizedBox( return SizedBox(
height: 200, height: 200,
child: Center(child: Text("خطا در بارگذاری: ${state.error}")), child: Center(child: Text("خطا در بارگذاری: ${state.error}")),
); );
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
); );
@ -482,4 +578,4 @@ class OffersView extends StatelessWidget {
), ),
); );
} }
} }

View File

@ -23,12 +23,14 @@ final List<OnboardingEntity> onboardingPages = [
OnboardingEntity( OnboardingEntity(
imagePath: Assets.images.onboarding3.path, imagePath: Assets.images.onboarding3.path,
title: ' پیشنهادهای لحظه‌ای، تجربه‌ای نو از خرید', title: ' پیشنهادهای لحظه‌ای، تجربه‌ای نو از خرید',
description: 'Proxibuy همیشه باهاته؛ چه در مسیر رفتن به خونه، چه در مسیر کافه مورد علاقه‌ات. همین الان خریدت رو هوشمندتر کن!', description:
'Proxibuy همیشه باهاته؛ چه در مسیر رفتن به خونه، چه در مسیر کافه مورد علاقه‌ات. همین الان خریدت رو هوشمندتر کن!',
), ),
OnboardingEntity( OnboardingEntity(
imagePath: Assets.images.onboarding4.path, imagePath: Assets.images.onboarding4.path,
title: 'دنیایی از تخفیف‌های اطرافت رو کشف کن', title: 'دنیایی از تخفیف‌های اطرافت رو کشف کن',
description: 'می‌صرفه یه اپ هوشمنده که کمکت می‌کنه وقتی توی شهر حرکت می‌کنی، از تخفیف‌های اطرافت باخبر شی و به صرفه‌تر خرید کنی.', description:
'می‌صرفه یه اپ هوشمنده که کمکت می‌کنه وقتی توی شهر حرکت می‌کنی، از تخفیف‌های اطرافت باخبر شی و به صرفه‌تر خرید کنی.',
), ),
]; ];
@ -69,13 +71,15 @@ class _OnboardingPageState extends State<OnboardingPage> {
); );
} else { } else {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => BlocProvider( builder:
create: (context) => AuthBloc(), (context) => BlocProvider(
child: const LoginPage(), // This creates a NEW AuthBloc
create: (context) => AuthBloc(),
child: const LoginPage(),
),
), ),
), );
);
} }
} }
@ -120,42 +124,43 @@ class _OnboardingPageState extends State<OnboardingPage> {
left: 0, left: 0,
right: 0, right: 0,
child: SizedBox( child: SizedBox(
width: 80, width: 80,
height: 80, height: 80,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [
SizedBox(
width: 80,
height: 80,
child: CustomPaint(
painter: OnboardingIndicatorPainter(
pageCount: onboardingPages.length,
currentPage: _currentPage,
activeColor: AppColors.primary,
inactiveColor: AppColors.unselected,
),
),
),
Stack(
children: [ children: [
SizedBox( Center(
width: 80, child: GestureDetector(
height: 80, onTap: _onArrowTap,
child: CustomPaint( child: Assets.icons.ellipse1.svg(width: 60),
painter: OnboardingIndicatorPainter(
pageCount: onboardingPages.length,
currentPage: _currentPage,
activeColor: AppColors.primary,
inactiveColor: AppColors.unselected,
),
), ),
), ),
Stack( Center(
children: [ child: GestureDetector(
Center( onTap: _onArrowTap,
child: GestureDetector( child: Assets.icons.arrowRight2.svg(width: 45),
onTap: _onArrowTap, ),
child: Assets.icons.ellipse1.svg(width: 60),
),
),
Center(
child: GestureDetector(
onTap: _onArrowTap,
child: Assets.icons.arrowRight2.svg(width: 45),
),
),
],
), ),
], ],
), ),
),), ],
),
),
),
Positioned( Positioned(
bottom: 90, bottom: 90,
left: 24, left: 24,
@ -173,8 +178,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
).textTheme.headlineSmall?.copyWith( ).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 18, fontSize: 18,
color: color: Colors.white,
Colors.white
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@ -1,19 +1,13 @@
// lib/presentation/pages/otp_page.dart
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:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
// import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; // حذف شد
// import 'package:proxibuy/data/repositories/offer_repository.dart'; // حذف شد
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
import 'package:proxibuy/presentation/pages/user_info_page.dart'; import 'package:proxibuy/presentation/pages/user_info_page.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import '../../core/config/app_colors.dart'; import '../../core/config/app_colors.dart';
import '../../core/gen/assets.gen.dart'; import '../../core/gen/assets.gen.dart';
import '../utils/otp_timer_helper.dart'; import '../utils/otp_timer_helper.dart';
class OtpPage extends StatefulWidget { class OtpPage extends StatefulWidget {
final String phoneNumber; final String phoneNumber;
final String phone; final String phone;
@ -154,33 +148,13 @@ class _OtpPageState extends State<OtpPage> {
}); });
} }
if (state is AuthNeedsInfo) { if (state is AuthNeedsInfo) {
// **تغییر اصلی در این قسمت است** // ******** شروع تغییر ۲ ********
// دیگر نیازی به ساخت OfferRepository نیست // BlocProvider.value حذف شد
Navigator.of(context).pushAndRemoveUntil( Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute( MaterialPageRoute(builder: (_) => const UserInfoPage()),
builder:
(_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: context.read<AuthBloc>(),
),
// OffersBloc دیگر به ریپازیتوری نیاز ندارد
BlocProvider<OffersBloc>(
create: (_) => OffersBloc(),
),
BlocProvider<ReservationCubit>(
create: (_) => ReservationCubit(),
),
BlocProvider<NotificationPreferencesBloc>(
create:
(_) => NotificationPreferencesBloc(),
),
],
child: const UserInfoPage(),
),
),
(route) => false, (route) => false,
); );
// ******** پایان تغییر ۲ ********
} }
}, },
builder: (context, state) { builder: (context, state) {
@ -342,4 +316,4 @@ class _OtpPageState extends State<OtpPage> {
); );
_otpTimer.resetTimer(); _otpTimer.resetTimer();
} }
} }

View File

@ -1,17 +1,18 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:maps_launcher/maps_launcher.dart'; import 'package:maps_launcher/maps_launcher.dart';
import 'package:proxibuy/core/config/api_config.dart';
import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/data/models/offer_model.dart';
import 'package:proxibuy/data/repositories/offer_repository.dart';
import 'package:proxibuy/presentation/pages/add_photo_screen.dart'; import 'package:proxibuy/presentation/pages/add_photo_screen.dart';
import 'package:proxibuy/presentation/pages/reservation_details_screen.dart'; import 'package:proxibuy/presentation/pages/reservation_details_screen.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_bloc.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_event.dart';
import 'package:proxibuy/presentation/product_detail/bloc/product_detail_state.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/comments_section.dart'; import 'package:proxibuy/presentation/widgets/comments_section.dart';
import 'package:slide_countdown/slide_countdown.dart'; import 'package:slide_countdown/slide_countdown.dart';
@ -21,34 +22,138 @@ class ProductDetailPage extends StatelessWidget {
const ProductDetailPage({super.key, required this.offer}); const ProductDetailPage({super.key, required this.offer});
Future<String> _generateQrToken(BuildContext context) async {
const storage = FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
if (userID == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("خطا: کاربر شناسایی نشد.")),
);
throw Exception("User ID not found");
}
final payload = {
'userID': userID,
'discountID': offer.id,
'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
};
const secretKey = 'your_super_secret_key_for_qr';
final jwt = JWT(payload);
final token = jwt.sign(SecretKey(secretKey));
return token;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.white,
body: Stack( body: Stack(
children: [ children: [
// ویجت نمایش جزئیات مستقیما ساخته میشود ProductDetailView(offer: offer),
ProductDetailView(offer: offer)
.animate()
.fadeIn(duration: 400.ms, curve: Curves.easeOut)
.slideY(
begin: 0.2,
duration: 400.ms,
curve: Curves.easeOut,
),
Positioned( Positioned(
bottom: 30, bottom: 30,
left: 24, left: 24,
right: 24, right: 24,
// BlocBuilder حذف شد
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () async {
context.read<ReservationCubit>().reserveProduct(offer.id); showDialog(
Navigator.of(context).push( context: context,
MaterialPageRoute( barrierDismissible: false,
builder: (_) => builder: (context) =>
ReservationConfirmationPage(offer: offer), const Center(child: CircularProgressIndicator()),
),
); );
try {
const storage = FlutterSecureStorage();
final token = await storage.read(key: 'accessToken');
if (token == null) {
throw Exception("شما وارد حساب کاربری خود نشده‌اید.");
}
final dio = Dio();
final url = ApiConfig.baseUrl + ApiConfig.addReservation;
final data = {
'Discount': offer.id,
'Distance': offer.distanceInMeters.toString(),
};
final options = Options(
headers: {'Authorization': 'Bearer $token'},
);
debugPrint("----------- REQUEST-----------");
debugPrint("URL: POST $url");
debugPrint("Headers: ${options.headers}");
debugPrint("Body: $data");
debugPrint("-----------------------------");
final response =
await dio.post(url, data: data, options: options);
debugPrint("---------- RESPONSE-----------");
debugPrint("StatusCode: ${response.statusCode}");
debugPrint("Data: ${response.data}");
debugPrint("-----------------------------");
if (context.mounted) Navigator.of(context).pop();
if (response.statusCode == 200) {
final qrToken = await _generateQrToken(context);
context.read<ReservationCubit>().reserveProduct(offer.id);
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ReservationConfirmationPage(
offer: offer,
qrCodeData: qrToken,
),
),
);
}
} else {
final errorMessage =
response.data['message'] ?? 'خطا در رزرو تخفیف';
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
backgroundColor: Colors.red),
);
}
}
} on DioException catch (e) {
if (context.mounted) Navigator.of(context).pop();
debugPrint("---------- ERROR-----------");
debugPrint("StatusCode: ${e.response?.statusCode}");
debugPrint("Data: ${e.response?.data}");
debugPrint("--------------------------");
final errorMessage = e.response?.data?['message'] ??
'خطای سرور هنگام رزرو. لطفاً دوباره تلاش کنید.';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
backgroundColor: Colors.red),
);
} catch (e) {
if (context.mounted) Navigator.of(context).pop();
debugPrint("---------- GENERAL ERROR -----------");
debugPrint(e.toString());
debugPrint("------------------------------------");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
backgroundColor: Colors.red),
);
}
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.confirm, backgroundColor: AppColors.confirm,
@ -73,7 +178,12 @@ class ProductDetailPage extends StatelessWidget {
), ),
], ],
), ),
).animate().fadeIn(delay: 200.ms).slideY(begin: 2), ).animate().slideY(
begin: 2,
delay: 400.ms,
duration: 600.ms,
curve: Curves.easeOutCubic,
),
), ),
], ],
), ),
@ -81,6 +191,7 @@ class ProductDetailPage extends StatelessWidget {
} }
} }
// ... بقیه کدهای این فایل بدون تغییر باقی میماند ...
class ProductDetailView extends StatefulWidget { class ProductDetailView extends StatefulWidget {
final OfferModel offer; final OfferModel offer;
@ -120,9 +231,9 @@ class _ProductDetailViewState extends State<ProductDetailView> {
_buildMainImage(context), _buildMainImage(context),
const SizedBox(height: 52.5), const SizedBox(height: 52.5),
_buildProductInfo(), _buildProductInfo(),
const SizedBox(height: 100), const SizedBox(height: 120),
], ],
), ).animate().fadeIn(duration: 600.ms, curve: Curves.easeOut),
Positioned( Positioned(
top: 400 - 52.5, top: 400 - 52.5,
left: 0, left: 0,
@ -138,38 +249,39 @@ class _ProductDetailViewState extends State<ProductDetailView> {
Widget _buildMainImage(BuildContext context) { Widget _buildMainImage(BuildContext context) {
return Stack( return Stack(
children: [ children: [
SizedBox( Hero(
height: 400, tag: 'offer_image_${widget.offer.id}',
width: double.infinity, child: SizedBox(
child: AnimatedSwitcher( height: 400,
duration: const Duration(milliseconds: 300), width: double.infinity,
transitionBuilder: (child, animation) { child: AnimatedSwitcher(
return FadeTransition(opacity: animation, child: child); duration: const Duration(milliseconds: 300),
}, transitionBuilder: (child, animation) {
child: Image.network( return FadeTransition(opacity: animation, child: child);
selectedImage,
key: ValueKey<String>(selectedImage),
fit: BoxFit.cover,
width: double.infinity,
height: 400,
loadingBuilder: (context, child, progress) {
return progress == null
? child
: const Center(child: CircularProgressIndicator());
}, },
child: CachedNetworkImage(
imageUrl: selectedImage,
key: ValueKey<String>(selectedImage),
fit: BoxFit.cover,
width: double.infinity,
height: 400,
placeholder: (context, url) =>
const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) =>
const Center(child: Icon(Icons.error)),
),
), ),
), ),
), ),
Positioned( Positioned(
top: 40, top: 40,
left: 16, left: 16,
child: child: GestureDetector(
GestureDetector( child: SvgPicture.asset(Assets.icons.back.path),
child: SvgPicture.asset(Assets.icons.back.path), onTap: () {
onTap: () { Navigator.pop(context);
Navigator.pop(context); },
}, ).animate().fade(delay: 200.ms).scale(),
).animate().fade(delay: 100.ms).scale(),
), ),
], ],
); );
@ -189,9 +301,10 @@ class _ProductDetailViewState extends State<ProductDetailView> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final img = imageList[index]; final img = imageList[index];
if (img == _uploadKey) { if (img == _uploadKey) {
return _buildUploadButton().animate().fade().scale( return _buildUploadButton()
delay: (index * 50).ms, .animate()
); .fade(delay: (index * 80).ms)
.scale(delay: (index * 80).ms);
} }
final isSelected = selectedImage == img; final isSelected = selectedImage == img;
return _buildThumbnail(img, isSelected, index); return _buildThumbnail(img, isSelected, index);
@ -203,32 +316,19 @@ class _ProductDetailViewState extends State<ProductDetailView> {
Widget _buildThumbnail(String img, bool isSelected, int index) { Widget _buildThumbnail(String img, bool isSelected, int index) {
const grayscaleMatrix = <double>[ const grayscaleMatrix = <double>[
0.2126, 0.2126, 0.7152, 0.0722, 0, 0,
0.7152, 0.2126, 0.7152, 0.0722, 0, 0,
0.0722, 0.2126, 0.7152, 0.0722, 0, 0,
0, 0, 0, 0, 1, 0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0.2126,
0.7152,
0.0722,
0,
0,
0,
0,
0,
1,
0,
]; ];
return GestureDetector( return GestureDetector(
onTap: () => setState(() => selectedImage = img), onTap: () => setState(() => selectedImage = img),
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
transform: Matrix4.identity()..scale(isSelected ? 1.0 : 0.9),
transformAlignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: isSelected ? AppColors.selectedImg : Colors.transparent, color: isSelected ? AppColors.selectedImg : Colors.transparent,
@ -237,7 +337,6 @@ class _ProductDetailViewState extends State<ProductDetailView> {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
// ignore: deprecated_member_use
color: Colors.black.withOpacity(0.15), color: Colors.black.withOpacity(0.15),
blurRadius: 7, blurRadius: 7,
spreadRadius: 0, spreadRadius: 0,
@ -250,46 +349,40 @@ class _ProductDetailViewState extends State<ProductDetailView> {
colorFilter: ColorFilter.matrix( colorFilter: ColorFilter.matrix(
isSelected isSelected
? <double>[ ? <double>[
1, 1, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0,
0, 0, 0, 1, 0, 0,
0, 0, 0, 0, 1, 0,
0, ]
0,
1,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
1,
0,
]
: grayscaleMatrix, : grayscaleMatrix,
), ),
child: Image.network(img, width: 90, height: 90, fit: BoxFit.cover), child: CachedNetworkImage(
imageUrl: img,
width: 90,
height: 90,
fit: BoxFit.cover,
placeholder: (context, url) => Container(color: Colors.grey[200]),
errorWidget: (context, url, error) => const Icon(Icons.error),
),
), ),
), ),
), ),
).animate().fade().scale(delay: (index * 50).ms); ).animate().fade().scale(delay: (index * 80).ms);
} }
Widget _buildUploadButton() { Widget _buildUploadButton() {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => AddPhotoScreen(storeName: widget.offer.storeName, productId: widget.offer.id, offer: widget.offer,), builder: (context) => AddPhotoScreen(
), storeName: widget.offer.storeName,
); productId: widget.offer.id,
offer: widget.offer,
),
),
);
}, },
child: Container( child: Container(
width: 90, width: 90,
@ -308,47 +401,51 @@ class _ProductDetailViewState extends State<ProductDetailView> {
} }
Widget _buildProductInfo() { Widget _buildProductInfo() {
final remainingDuration = final remainingDuration = widget.offer.expiryTime.isAfter(DateTime.now())
widget.offer.expiryTime.isAfter(DateTime.now()) ? widget.offer.expiryTime.difference(DateTime.now())
? widget.offer.expiryTime.difference(DateTime.now()) : Duration.zero;
: Duration.zero;
final animationList = <Widget>[ return Padding(
Row( padding: const EdgeInsets.symmetric(horizontal: 24.0)
children: [ .copyWith(bottom: 5.0, top: 24.0),
SvgPicture.asset(Assets.icons.shop.path, height: 30), child: Column(
const SizedBox(width: 6), crossAxisAlignment: CrossAxisAlignment.start,
Expanded( children: <Widget>[
child: Text( Row(
widget.offer.storeName, children: [
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), SvgPicture.asset(Assets.icons.shop.path, height: 30),
overflow: TextOverflow.ellipsis, const SizedBox(width: 6),
), Expanded(
child: Text(
widget.offer.storeName,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
],
), ),
], const SizedBox(height: 16),
), _buildInfoRow(
const SizedBox(height: 16), icon: Assets.icons.location, text: widget.offer.address),
_buildInfoRow(icon: Assets.icons.location, text: widget.offer.address), const SizedBox(height: 12),
const SizedBox(height: 12), ExpandableInfoRow(
ExpandableInfoRow( icon: Assets.icons.clock,
icon: Assets.icons.clock, titleWidget: Row(
titleWidget: Row( children: [
children: [ Text(
Text( widget.offer.isOpen ? "باز است" : "بسته است",
widget.offer.isOpen ? "باز است" : "بسته است", style: TextStyle(
style: TextStyle( fontSize: 16,
fontSize: 16, color: widget.offer.isOpen
color:
widget.offer.isOpen
? Colors.green.shade700 ? Colors.green.shade700
: Colors.red.shade700, : Colors.red.shade700,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
),
],
), ),
], children: widget.offer.workingHours
),
children:
widget.offer.workingHours
.map( .map(
(wh) => Padding( (wh) => Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
@ -368,235 +465,228 @@ class _ProductDetailViewState extends State<ProductDetailView> {
), ),
wh.isOpen wh.isOpen
? Text( ? Text(
wh.shifts wh.shifts
.map((s) => '${s.openAt} - ${s.closeAt}') .map((s) => '${s.openAt} - ${s.closeAt}')
.join(' | '), .join(' | '),
style: const TextStyle( style: const TextStyle(
fontSize: 15, fontSize: 15,
color: AppColors.hint, color: AppColors.hint,
), ),
) )
: const Text( : const Text(
'تعطیل', 'تعطیل',
style: TextStyle(fontSize: 15, color: Colors.red), style:
), TextStyle(fontSize: 15, color: Colors.red),
),
], ],
), ),
), ),
) )
.toList(), .toList(),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
InkWell( InkWell(
onTap: onTap: () => _launchMaps(
() => _launchMaps(
widget.offer.latitude, widget.offer.latitude,
widget.offer.longitude, widget.offer.longitude,
widget.offer.storeName, widget.offer.storeName,
), ),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Padding( child: Padding(
padding: const EdgeInsets.all(4.0), padding: const EdgeInsets.all(4.0),
child: Row( child: Row(
children: [
SvgPicture.asset(
Assets.icons.map.path,
width: 25,
height: 25,
colorFilter: const ColorFilter.mode(
AppColors.button,
BlendMode.srcIn,
),
),
const SizedBox(width: 8),
const Text(
'مسیر فروشگاه روی نقشه',
style:
TextStyle(fontSize: 17, color: AppColors.button),
),
],
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
SvgPicture.asset( Container(
Assets.icons.map.path, width: 60,
width: 25, padding:
height: 25, const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
colorFilter: const ColorFilter.mode( decoration: const BoxDecoration(
AppColors.button, color: AppColors.selectedImg,
BlendMode.srcIn, borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.offer.rating.toString(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
const SizedBox(width: 2),
SvgPicture.asset(
Assets.icons.star.path,
colorFilter: const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
),
),
],
), ),
), ),
const SizedBox(width: 8), Container(
const Text( height: 30,
'مسیر فروشگاه روی نقشه', width: 60,
style: TextStyle(fontSize: 17, color: AppColors.button), padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
child: Center(
child: Text(
'${widget.offer.ratingCount} نفر',
style: const TextStyle(
color: Colors.black,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
), ),
], ],
), ),
), ],
), ),
Column( const SizedBox(height: 30),
crossAxisAlignment: CrossAxisAlignment.end, Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Container( Container(
width: 60, padding:
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), const EdgeInsets.symmetric(horizontal: 20, vertical: 11),
decoration: const BoxDecoration( decoration: BoxDecoration(
color: AppColors.selectedImg, color: AppColors.singleOfferType,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.circular(20),
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
), ),
child: Row( child: Text(
mainAxisAlignment: MainAxisAlignment.center, "تخفیف ${widget.offer.discountType}",
children: [ style: const TextStyle(
Text( color: Colors.white,
widget.offer.rating.toString(), fontWeight: FontWeight.bold,
style: const TextStyle( fontSize: 17,
color: Colors.white, ),
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
const SizedBox(width: 2),
SvgPicture.asset(
Assets.icons.star.path,
colorFilter: const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
),
),
],
), ),
), ),
Container( Column(
height: 30, crossAxisAlignment: CrossAxisAlignment.end,
width: 60, children: [
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), Row(
decoration: BoxDecoration( mainAxisAlignment: MainAxisAlignment.end,
color: Colors.grey[200], crossAxisAlignment: CrossAxisAlignment.center,
borderRadius: const BorderRadius.only( children: [
bottomLeft: Radius.circular(8), Text(
bottomRight: Radius.circular(8), '(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)',
style: const TextStyle(
fontSize: 16,
color: AppColors.singleOfferType,
fontWeight: FontWeight.normal,
),
),
const SizedBox(width: 8),
Text(
widget.offer.originalPrice.toStringAsFixed(0),
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
decoration: TextDecoration.lineThrough,
),
),
],
), ),
), const SizedBox(height: 1),
child: Center( Text(
child: Text( '${widget.offer.finalPrice.toStringAsFixed(0)} تومان',
'${widget.offer.ratingCount} نفر',
style: const TextStyle( style: const TextStyle(
color: Colors.black, color: AppColors.singleOfferType,
fontSize: 11, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
),
),
],
),
],
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 11),
decoration: BoxDecoration(
color: AppColors.singleOfferType,
borderRadius: BorderRadius.circular(20),
),
child: Text(
"تخفیف ${widget.offer.discountType}",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'(${widget.offer.discount})',
style: const TextStyle(
fontSize: 16,
color: AppColors.singleOfferType,
fontWeight: FontWeight.normal,
),
),
const SizedBox(width: 8),
Text(
widget.offer.originalPrice.toStringAsFixed(0),
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
decoration: TextDecoration.lineThrough,
),
),
], ],
), ),
const SizedBox(height: 1),
Text(
'${widget.offer.finalPrice.toStringAsFixed(0)} تومان',
style: const TextStyle(
color: AppColors.singleOfferType,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
], ],
), ),
], const SizedBox(height: 24),
), _buildInfoRow(
const SizedBox(height: 24), icon: Assets.icons.timerPause,
_buildInfoRow( text: "مهلت استفاده از تخفیف",
icon: Assets.icons.timerPause, ),
text: "مهلت استفاده از تخفیف", const SizedBox(height: 10),
), if (remainingDuration > Duration.zero)
const SizedBox(height: 10), Column(
if (remainingDuration > Duration.zero) children: [
Column( Localizations.override(
children: [ context: context,
Localizations.override( locale: const Locale('en'),
context: context, child: SlideCountdown(
locale: const Locale('en'), duration: remainingDuration,
child: SlideCountdown( slideDirection: SlideDirection.up,
duration: remainingDuration, separator: ':',
slideDirection: SlideDirection.up, style: const TextStyle(
separator: ':', fontSize: 50,
style: const TextStyle( fontWeight: FontWeight.bold,
fontSize: 70, color: AppColors.countdown,
fontWeight: FontWeight.bold, ),
color: AppColors.countdown, separatorStyle: const TextStyle(
fontSize: 40,
color: AppColors.countdown,
),
decoration: const BoxDecoration(color: Colors.white),
shouldShowDays: (d) => d.inDays > 0,
shouldShowHours: (d) => d.inHours > 0,
shouldShowMinutes: (d) => d.inSeconds > 0,
),
), ),
separatorStyle: const TextStyle( const SizedBox(height: 4),
fontSize: 40, _buildTimerLabels(remainingDuration),
color: AppColors.countdown, ],
), ),
decoration: const BoxDecoration(color: Colors.white), const SizedBox(height: 24),
shouldShowDays: (d) => d.inDays > 0, _buildFeaturesSection(),
shouldShowHours: (d) => d.inHours > 0, const SizedBox(height: 24),
shouldShowMinutes: _buildDiscountTypeSection(),
(d) => const SizedBox(height: 24),
d.inSeconds > CommentsSection(comments: widget.offer.comments),
0, ].animate(interval: 80.ms).slideX(begin: -0.05).fadeIn(
), duration: 400.ms,
curve: Curves.easeOut,
), ),
const SizedBox(height: 4),
_buildTimerLabels(remainingDuration),
],
),
const SizedBox(height: 24),
_buildFeaturesSection(),
const SizedBox(height: 24),
_buildDiscountTypeSection(),
const SizedBox(height: 24),
CommentsSection(comments: widget.offer.comments),
].animate(interval: 80.ms).fade(duration: 300.ms).slideX(begin: -0.1);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
).copyWith(bottom: 24.0, top: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: animationList
.animate(interval: 80.ms)
.fade(duration: 300.ms)
.slideX(begin: -0.1),
), ),
); );
} }
@ -684,7 +774,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
width: 22, width: 22,
height: 22, height: 22,
colorFilter: const ColorFilter.mode( colorFilter: const ColorFilter.mode(
AppColors.confirm, AppColors.hint,
BlendMode.srcIn, BlendMode.srcIn,
), ),
), ),
@ -741,7 +831,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
"${widget.offer.discount} تخفیف ${info.name}", '${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}% تخفیف ${widget.offer.discountType}',
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
height: 1.4, height: 1.4,
@ -760,12 +850,12 @@ class _ProductDetailViewState extends State<ProductDetailView> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Center( Center(
child: SvgPicture.asset(Assets.icons.warning2.path, height: 24), child:
), SvgPicture.asset(Assets.icons.warning2.path, height: 24)),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
info.description, widget.offer.discountInfo,
style: const TextStyle(fontSize: 14, color: AppColors.hint), style: const TextStyle(fontSize: 14, color: AppColors.hint),
), ),
), ),
@ -858,13 +948,12 @@ class _ExpandableInfoRowState extends State<ExpandableInfoRow> {
AnimatedCrossFade( AnimatedCrossFade(
firstChild: Container(), firstChild: Container(),
secondChild: Column(children: widget.children), secondChild: Column(children: widget.children),
crossFadeState: crossFadeState: _isExpanded
_isExpanded ? CrossFadeState.showSecond
? CrossFadeState.showSecond : CrossFadeState.showFirst,
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
), ),
], ],
); );
} }
} }

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
@ -11,27 +12,48 @@ import 'package:flutter_animate/flutter_animate.dart';
class ReservationConfirmationPage extends StatefulWidget { class ReservationConfirmationPage extends StatefulWidget {
final OfferModel offer; final OfferModel offer;
final String qrCodeData;
const ReservationConfirmationPage({super.key, required this.offer}); const ReservationConfirmationPage({
super.key,
required this.offer,
required this.qrCodeData,
});
@override @override
State<ReservationConfirmationPage> createState() => State<ReservationConfirmationPage> createState() =>
_ReservationConfirmationPageState(); _ReservationConfirmationPageState();
} }
class _ReservationConfirmationPageState extends State<ReservationConfirmationPage> { class _ReservationConfirmationPageState
extends State<ReservationConfirmationPage> {
Timer? _timer; Timer? _timer;
Duration _remaining = Duration.zero; Duration _remaining = Duration.zero;
final AudioPlayer _audioPlayer = AudioPlayer();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_playSound();
// ******** پایان تغییر اصلی ********
_calculateRemainingTime(); _calculateRemainingTime();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) { _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_calculateRemainingTime(); _calculateRemainingTime();
}); });
} }
// متد جدید برای پخش صدا
void _playSound() async {
try {
// فایل صدایی که در پوشه assets قرار دادهاید
await _audioPlayer.play(AssetSource('sounds/positive-notification-alert-351299.mp3'));
} catch (e) {
debugPrint("Error playing sound: $e");
}
}
void _calculateRemainingTime() { void _calculateRemainingTime() {
final now = DateTime.now(); final now = DateTime.now();
if (widget.offer.expiryTime.isAfter(now)) { if (widget.offer.expiryTime.isAfter(now)) {
@ -53,9 +75,11 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
@override @override
void dispose() { void dispose() {
_timer?.cancel(); _timer?.cancel();
_audioPlayer.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Directionality( return Directionality(
@ -65,39 +89,48 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
appBar: _buildCustomAppBar(context), appBar: _buildCustomAppBar(context),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0), padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 32.0,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'تخفیف ${widget.offer.discountType} رزرو شد!', 'تخفیف ${widget.offer.discountType} رزرو شد!',
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87, color: Colors.black87,
), ),
).animate().fadeIn(delay: 300.ms, duration: 500.ms).slideY(begin: -0.2, end: 0), )
.animate()
.fadeIn(delay: 300.ms, duration: 500.ms)
.slideY(begin: -0.2, end: 0),
const SizedBox(height: 8), const SizedBox(height: 8),
const Divider(thickness: 1.5) const Divider(thickness: 1.5)
.animate() .animate()
.fadeIn(delay: 400.ms) .fadeIn(delay: 400.ms)
.scaleX(begin: 0, duration: 600.ms, curve: Curves.easeInOut), .scaleX(
begin: 0,
duration: 600.ms,
curve: Curves.easeInOut,
),
const SizedBox(height: 18), const SizedBox(height: 18),
_buildOfferDetailsCard() _buildOfferDetailsCard()
.animate() .animate()
.fadeIn(delay: 600.ms, duration: 500.ms) .fadeIn(delay: 600.ms, duration: 500.ms)
.slideX(begin: 0.5, end: 0, curve: Curves.easeOutCubic), .slideX(begin: 0.5, end: 0, curve: Curves.easeOutCubic),
const SizedBox(height: 18), const SizedBox(height: 18),
_buildTimerCard() _buildTimerCard()
.animate() .animate()
.fadeIn(delay: 800.ms, duration: 500.ms) .fadeIn(delay: 800.ms, duration: 500.ms)
.scale(begin: const Offset(0.8, 0.8), curve: Curves.easeOutBack), .scale(
begin: const Offset(0.8, 0.8),
curve: Curves.easeOutBack,
),
const SizedBox(height: 18), const SizedBox(height: 18),
_buildQrCodeCard() _buildQrCodeCard() // نمایش QR Code با استفاده از توکن
.animate() .animate()
.fadeIn(delay: 1000.ms, duration: 500.ms) .fadeIn(delay: 1000.ms, duration: 500.ms)
.flipV(begin: -0.5, end: 0, curve: Curves.easeOut), .flipV(begin: -0.5, end: 0, curve: Curves.easeOut),
@ -120,7 +153,6 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
// ignore: deprecated_member_use
color: Colors.black.withOpacity(0.08), color: Colors.black.withOpacity(0.08),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 4),
@ -132,7 +164,7 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
children: [ children: [
SizedBox(height: 15), const SizedBox(height: 15),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -198,7 +230,7 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
SvgPicture.asset(Assets.icons.ticketDiscount.path), SvgPicture.asset(Assets.icons.ticketDiscount.path),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
'(${widget.offer.discount})', '(${(100-widget.offer.finalPrice/widget.offer.originalPrice*100).toInt()}%)',
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
color: AppColors.singleOfferType, color: AppColors.singleOfferType,
@ -219,9 +251,12 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
const SizedBox(height: 10), const SizedBox(height: 10),
Row( Row(
children: [ children: [
// ignore: deprecated_member_use SvgPicture.asset(
SvgPicture.asset(Assets.icons.cardPos.path,height: 22,color: Color.fromARGB(255, 157, 157, 155),), Assets.icons.cardPos.path,
SizedBox(width: 6,), height: 22,
color: const Color.fromARGB(255, 157, 157, 155),
),
const SizedBox(width: 6),
Text( Text(
'${widget.offer.finalPrice.toStringAsFixed(0)} تومان', '${widget.offer.finalPrice.toStringAsFixed(0)} تومان',
style: const TextStyle( style: const TextStyle(
@ -250,7 +285,7 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
), ),
); );
} }
String days = _remaining.inDays.toString(); String days = _remaining.inDays.toString();
String hours = (_remaining.inHours % 24).toString(); String hours = (_remaining.inHours % 24).toString();
String minutes = (_remaining.inMinutes % 60).toString(); String minutes = (_remaining.inMinutes % 60).toString();
@ -259,7 +294,7 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 20), padding: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color.fromARGB(255, 246, 246, 246), color: const Color.fromARGB(255, 246, 246, 246),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
@ -267,76 +302,90 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Column( const Column(
children: [ children: [
Text("مدت",style: TextStyle(color: AppColors.expiryReserve,fontSize: 15),), Text(
SizedBox(height: 7,), "مدت",
Text("اعتبار",style: TextStyle(color: AppColors.expiryReserve,fontSize: 15),), style: TextStyle(
color: AppColors.expiryReserve,
fontSize: 15,
),
),
SizedBox(height: 7),
Text(
"اعتبار",
style: TextStyle(
color: AppColors.expiryReserve,
fontSize: 15,
),
),
], ],
), ),
SizedBox(width: 25,), const SizedBox(width: 15),
_buildTimeBlock(seconds, 'ثانیه'), _buildTimeBlock(seconds, 'ثانیه'),
SizedBox(width: 20,), const SizedBox(width: 10),
_buildTimeBlock(minutes, 'دقیقه'), _buildTimeBlock(minutes, 'دقیقه'),
SizedBox(width: 20,), const SizedBox(width: 10),
_buildTimeBlock(hours, 'ساعت'), _buildTimeBlock(hours, 'ساعت'),
SizedBox(width: 20,), if (_remaining.inDays > 0) ...[
if (_remaining.inDays > 0) _buildTimeBlock(days, 'روز'), const SizedBox(width: 10),
_buildTimeBlock(days, 'روز'),
],
], ],
), ),
SizedBox(height: 20 ,), const SizedBox(height: 20),
Text("لطفا QR Code زیر رو به فروشنده نشون بده.",style: TextStyle(fontSize: 15),) const Text(
"لطفا QR Code زیر رو به فروشنده نشون بده.",
style: TextStyle(fontSize: 15),
),
], ],
), ),
); );
} }
Widget _buildTimeBlock(String value, String label) { Widget _buildTimeBlock(String value, String label) {
return Row( return Container(
children: [ padding: const EdgeInsets.symmetric(
Container( horizontal: 12,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 3), vertical: 5,
decoration: BoxDecoration( ),
border: Border.all(color: AppColors.countdownBorderRserve, width: 1.5), decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.countdownBorderRserve, width: 1.5),
), borderRadius: BorderRadius.circular(12),
child: Padding( ),
padding: const EdgeInsets.all(4.0), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Text(
Text( value,
value, style: const TextStyle(
style: const TextStyle( fontSize: 20, // فونت کمی کوچکتر شد
fontSize: 20, fontFamily: 'Dana',
fontFamily: 'Dana', fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, color: Colors.black,
color: Colors.black,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Dana',
color: Colors.black,
),
),
],
), ),
), ),
), const SizedBox(height: 4),
], Text(
label,
style: const TextStyle(
fontSize: 12,
fontFamily: 'Dana',
color: Colors.black,
),
),
],
),
); );
} }
Widget _buildQrCodeCard() { Widget _buildQrCodeCard() {
return Center( return Center(
child: Container( child: Container(
width: 500, width: 500,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color.fromARGB(255, 246, 246, 246), color: const Color.fromARGB(255, 246, 246, 246),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
@ -344,16 +393,15 @@ class _ReservationConfirmationPageState extends State<ReservationConfirmationPag
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: QrImageView( child: QrImageView(
data: widget.offer.qrCodeData, data: widget.qrCodeData,
version: QrVersions.auto, version: QrVersions.auto,
size: 280.0, size: 280.0,
), ),
), ),
SizedBox(height: 10,), const SizedBox(height: 10),
Text(widget.offer.qrCodeData,style: TextStyle(fontWeight: FontWeight.bold,fontSize: 20),)
], ],
), ),
), ),
); );
} }
} }

View File

@ -1,13 +1,11 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; // import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:proxibuy/core/config/api_config.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/data/models/offer_model.dart';
import 'package:proxibuy/data/repositories/offer_repository.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
import 'package:proxibuy/presentation/offer/bloc/offer_state.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/presentation/widgets/reserved_list_item_card.dart'; import 'package:proxibuy/presentation/widgets/reserved_list_item_card.dart';
class ReservedListPage extends StatefulWidget { class ReservedListPage extends StatefulWidget {
@ -18,30 +16,55 @@ class ReservedListPage extends StatefulWidget {
} }
class _ReservedListPageState extends State<ReservedListPage> { class _ReservedListPageState extends State<ReservedListPage> {
late final List<String> _reservedIds; late Future<List<OfferModel>> _reservedOffersFuture;
// دیگر نیازی به Future نیست
List<OfferModel> _reservedOffers = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_reservedIds = context.read<ReservationCubit>().state.reservedProductIds; _reservedOffersFuture = _fetchReservedOffers();
// اطلاعات مستقیما از BLoC خوانده میشود
_fetchReservedOffersFromBloc();
} }
void _fetchReservedOffersFromBloc() { Future<List<OfferModel>> _fetchReservedOffers() async {
final offersState = context.read<OffersBloc>().state; try {
// بررسی میکند که آیا پیشنهادها قبلا بارگذاری شدهاند یا خیر const storage = FlutterSecureStorage();
if (offersState is OffersLoadSuccess) { final token = await storage.read(key: 'accessToken');
final allOffers = offersState.offers; if (token == null) {
if (mounted) { throw Exception('شما وارد حساب کاربری خود نشده‌اید.');
setState(() {
_reservedOffers = allOffers
.where((offer) => _reservedIds.contains(offer.id))
.toList();
});
} }
final dio = Dio();
final response = await dio.get(
ApiConfig.baseUrl + ApiConfig.getReservations,
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
if (response.statusCode == 200) {
final List<dynamic> reserves = response.data['reserves'];
// ******** شروع تغییر ۲ ********
// تبدیل ساختار جدید JSON به فرمت مورد انتظار OfferModel
return reserves.map((reserveData) {
final discountData = reserveData['Discount'] as Map<String, dynamic>;
final shopData = reserveData['Shop'] as Map<String, dynamic>;
// ترکیب اطلاعات برای سازگاری با مدل
final fullOfferData = {
...discountData,
'shopData': shopData,
'Distance': reserveData['Distance'], // پاس دادن فاصله به مدل
};
return OfferModel.fromJson(fullOfferData);
}).toList();
// ******** پایان تغییر ۲ ********
} else {
throw Exception('خطا در دریافت اطلاعات از سرور');
}
} on DioException catch (e) {
final errorMessage = e.response?.data?['message'] ?? 'خطای شبکه';
throw Exception(errorMessage);
} catch (e) {
throw Exception('خطایی رخ داد: $e');
} }
} }
@ -51,17 +74,36 @@ class _ReservedListPageState extends State<ReservedListPage> {
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: Scaffold( child: Scaffold(
appBar: _buildCustomAppBar(context), appBar: _buildCustomAppBar(context),
// FutureBuilder با یک ویجت ساده جایگزین شد body: FutureBuilder<List<OfferModel>>(
body: _reservedOffers.isEmpty future: _reservedOffersFuture,
? const Center( builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('خطا: ${snapshot.error}', textAlign: TextAlign.center),
),
);
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(
child: Text('هیچ آیتم رزرو شده‌ای یافت نشد.'), child: Text('هیچ آیتم رزرو شده‌ای یافت نشد.'),
) );
: ListView.builder( }
padding: const EdgeInsets.all(16.0),
itemCount: _reservedOffers.length, final reservedOffers = snapshot.data!;
itemBuilder: (context, index) {
final offer = _reservedOffers[index]; return ListView.builder(
return Padding( padding: const EdgeInsets.all(16.0),
itemCount: reservedOffers.length,
itemBuilder: (context, index) {
final offer = reservedOffers[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16.0), padding: const EdgeInsets.only(bottom: 16.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@ -79,9 +121,7 @@ class _ReservedListPageState extends State<ReservedListPage> {
const Divider(thickness: 1.5), const Divider(thickness: 1.5),
const SizedBox(height: 2), const SizedBox(height: 2),
ReservedListItemCard(offer: offer), ReservedListItemCard(offer: offer),
SizedBox( const SizedBox(height: 15),
height: 15,
)
], ],
), ),
).animate().fadeIn(delay: (100 * index).ms).slideY( ).animate().fadeIn(delay: (100 * index).ms).slideY(
@ -90,9 +130,11 @@ class _ReservedListPageState extends State<ReservedListPage> {
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
); );
}, },
) );
) ); } },
),
),
);
} }
PreferredSizeWidget _buildCustomAppBar(BuildContext context) { PreferredSizeWidget _buildCustomAppBar(BuildContext context) {
@ -106,7 +148,6 @@ class _ReservedListPageState extends State<ReservedListPage> {
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
// ignore: deprecated_member_use
color: Colors.black.withOpacity(0.08), color: Colors.black.withOpacity(0.08),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 4),
@ -118,7 +159,7 @@ class _ReservedListPageState extends State<ReservedListPage> {
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
children: [ children: [
SizedBox(height: 15), const SizedBox(height: 15),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -147,3 +188,4 @@ class _ReservedListPageState extends State<ReservedListPage> {
), ),
); );
} }
}

View File

@ -1,3 +1,5 @@
// lib/presentation/pages/user_info_page.dart
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:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
@ -22,6 +24,18 @@ class _UserInfoPageState extends State<UserInfoPage> {
super.dispose(); super.dispose();
} }
// ******** شروع تغییر ۱ ********
// متد ناوبری را ساده میکنیم.
// این متد دیگر Provider جدیدی نمیسازد و از Provider های سراسری استفاده میکند.
void _navigateToNextPage() {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const NotificationPreferencesPage(),
),
);
}
// ******** پایان تغییر ۱ ********
Widget _buildGenderRadio(String title, String value) { Widget _buildGenderRadio(String title, String value) {
return InkWell( return InkWell(
onTap: () => setState(() => _selectedGender = value), onTap: () => setState(() => _selectedGender = value),
@ -69,7 +83,6 @@ class _UserInfoPageState extends State<UserInfoPage> {
Positioned.fill( Positioned.fill(
child: Image.asset(Assets.images.userinfo.path, fit: BoxFit.cover), child: Image.asset(Assets.images.userinfo.path, fit: BoxFit.cover),
), ),
DraggableScrollableSheet( DraggableScrollableSheet(
initialChildSize: 0.50, initialChildSize: 0.50,
minChildSize: 0.50, minChildSize: 0.50,
@ -91,7 +104,6 @@ class _UserInfoPageState extends State<UserInfoPage> {
width: 50, width: 50,
height: 5, height: 5,
decoration: BoxDecoration( decoration: BoxDecoration(
// ignore: deprecated_member_use
color: Colors.grey.withOpacity(0.5), color: Colors.grey.withOpacity(0.5),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@ -115,7 +127,6 @@ class _UserInfoPageState extends State<UserInfoPage> {
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
"جنسیت", "جنسیت",
style: textTheme.titleMedium?.copyWith( style: textTheme.titleMedium?.copyWith(
@ -133,9 +144,13 @@ class _UserInfoPageState extends State<UserInfoPage> {
], ],
), ),
const SizedBox(height: 55), const SizedBox(height: 55),
BlocConsumer<AuthBloc, AuthState>( BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) { listener: (context, state) {
// ******** لاگ: state جدید دریافت شد ********
debugPrint(
"UserInfoPage: 🟠 Listener یک State جدید دریافت کرد: ${state.runtimeType}",
);
if (state is AuthFailure) { if (state is AuthFailure) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@ -144,10 +159,16 @@ class _UserInfoPageState extends State<UserInfoPage> {
), ),
); );
} }
if (state is AuthSuccess) {
// ******** لاگ: state موفقیت آمیز بود ********
debugPrint(
"UserInfoPage: ✅ State از نوع AuthSuccess است. فراخوانی _navigateToNextPage...",
);
_navigateToNextPage();
}
}, },
builder: (context, state) { builder: (context, state) {
final isLoading = state is AuthLoading; final isLoading = state is AuthLoading;
return Column( return Column(
children: [ children: [
SizedBox( SizedBox(
@ -173,14 +194,9 @@ class _UserInfoPageState extends State<UserInfoPage> {
}, },
), ),
const SizedBox(height: 9), const SizedBox(height: 9),
Center( Center(
child: TextButton( child: TextButton(
onPressed: () { onPressed: _navigateToNextPage,
Navigator.of(context).pushReplacement(
NotificationPreferencesPage.route(),
);
},
child: const Text( child: const Text(
"رد شدن", "رد شدن",
style: TextStyle(color: Colors.black), style: TextStyle(color: Colors.black),

View File

@ -1,3 +1,5 @@
// lib/presentation/reservation/cubit/reservation_cubit.dart
// ignore: depend_on_referenced_packages // ignore: depend_on_referenced_packages
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
@ -13,6 +15,11 @@ class ReservationCubit extends Cubit<ReservationState> {
emit(state.copyWith(reservedProductIds: updatedList)); emit(state.copyWith(reservedProductIds: updatedList));
} }
// متد جدید برای تنظیم کردن همه شناسههای رزرو شده
void setReservedIds(List<String> productIds) {
emit(state.copyWith(reservedProductIds: productIds));
}
bool isProductReserved(String productId) { bool isProductReserved(String productId) {
return state.reservedProductIds.contains(productId); return state.reservedProductIds.contains(productId);
} }

View File

@ -1,8 +1,8 @@
// lib/presentation/widgets/reserved_list_item_card.dart import 'dart:async';
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
@ -23,6 +23,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
bool _isExpanded = false; bool _isExpanded = false;
Timer? _timer; Timer? _timer;
Duration _remaining = Duration.zero; Duration _remaining = Duration.zero;
Future<String>? _qrTokenFuture;
@override @override
void initState() { void initState() {
@ -33,6 +34,38 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
}); });
} }
Future<String> _generateQrToken() async {
const storage = FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
if (userID == null) {
throw Exception("User ID not found");
}
final payload = {
'userID': userID,
'discountID': widget.offer.id,
'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
};
const secretKey = 'your_super_secret_key_for_qr';
final jwt = JWT(payload);
final token = jwt.sign(SecretKey(secretKey));
return token;
}
void _toggleExpansion() {
setState(() {
_isExpanded = !_isExpanded;
// توکن فقط زمانی ساخته میشود که کاربر پنل را باز میکند
if (_isExpanded && _qrTokenFuture == null) {
_qrTokenFuture = _generateQrToken();
}
});
}
void _calculateRemainingTime() { void _calculateRemainingTime() {
final now = DateTime.now(); final now = DateTime.now();
if (widget.offer.expiryTime.isAfter(now)) { if (widget.offer.expiryTime.isAfter(now)) {
@ -55,18 +88,8 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
super.dispose(); super.dispose();
} }
// ignore: unused_element
String _formatDuration(Duration duration) {
if (duration.inSeconds <= 0) return "پایان یافته";
final hours = duration.inHours.toString().padLeft(2, '0');
final minutes = (duration.inMinutes % 60).toString().padLeft(2, '0');
final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
return '$hours:$minutes:$seconds';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
Card( Card(
@ -194,7 +217,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
), ),
SizedBox(width: 10), SizedBox(width: 10),
TextButton( TextButton(
onPressed: () => setState(() => _isExpanded = !_isExpanded), onPressed: _toggleExpansion,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -314,7 +337,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
'(${widget.offer.discount})', '(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: AppColors.singleOfferType, color: AppColors.singleOfferType,
@ -345,34 +368,37 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
), ),
], ],
), ),
SizedBox( const SizedBox(height: 20),
height: 20,
),
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color.fromARGB(255, 246, 246, 246), color: Color.fromARGB(255, 246, 246, 246),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: FutureBuilder<String>(
children: [ future: _qrTokenFuture,
Padding( builder: (context, snapshot) {
padding: const EdgeInsets.all(15.0), if (snapshot.connectionState == ConnectionState.waiting) {
child: QrImageView( return const SizedBox(
data: widget.offer.qrCodeData, height: 280.0,
child: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasError) {
return const SizedBox(
height: 280.0,
child: Center(child: Text("خطا در ساخت کد QR")),
);
}
if (snapshot.hasData) {
return QrImageView(
data: snapshot.data!,
version: QrVersions.auto, version: QrVersions.auto,
size: 280.0, size: 280.0,
), );
), }
SizedBox(height: 10), return const SizedBox.shrink();
Text( },
widget.offer.qrCodeData,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
],
), ),
), ),
], ],

View File

@ -28,7 +28,7 @@ class MqttService {
client = MqttServerClient.withPort(server, clientId, port); client = MqttServerClient.withPort(server, clientId, port);
client.logging(on: true); client.logging(on: true);
client.keepAlivePeriod = 60; client.keepAlivePeriod = 60;
client.autoReconnect = false; client.autoReconnect = true; // فعالسازی اتصال مجدد خودکار
client.setProtocolV311(); client.setProtocolV311();
debugPrint('--- [MQTT] Attempting to connect...'); debugPrint('--- [MQTT] Attempting to connect...');
@ -64,9 +64,20 @@ class MqttService {
}); });
}; };
// این callback زمانی فراخوانی میشود که اتصال قطع شود
client.onDisconnected = () { client.onDisconnected = () {
debugPrint('❌ [MQTT] Disconnected.'); debugPrint('❌ [MQTT] Disconnected.');
}; };
// این callback زمانی فراخوانی میشود که کلاینت در حال تلاش برای اتصال مجدد خودکار است
client.onAutoReconnect = () {
debugPrint('↪️ [MQTT] Auto-reconnecting...');
};
// این callback زمانی فراخوانی میشود که اتصال مجدد خودکار موفقیتآمیز باشد
client.onAutoReconnected = () {
debugPrint('✅ [MQTT] Auto-reconnected successfully.');
};
client.onSubscribed = (String topic) { client.onSubscribed = (String topic) {
debugPrint('✅ [MQTT] Subscribed to topic: $topic'); debugPrint('✅ [MQTT] Subscribed to topic: $topic');

View File

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <flutter_localization/flutter_localization_plugin.h> #include <flutter_localization/flutter_localization_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
@ -13,6 +14,9 @@
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); file_selector_plugin_register_with_registrar(file_selector_linux_registrar);

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
file_selector_linux file_selector_linux
flutter_localization flutter_localization
flutter_secure_storage_linux flutter_secure_storage_linux

View File

@ -5,7 +5,9 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import audioplayers_darwin
import cloud_firestore import cloud_firestore
import connectivity_plus
import file_selector_macos import file_selector_macos
import firebase_auth import firebase_auth
import firebase_core import firebase_core
@ -20,7 +22,9 @@ import sqflite_darwin
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))

View File

@ -17,6 +17,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.59" version: "1.3.59"
adaptive_number:
dependency: transitive
description:
name: adaptive_number
sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@ -49,6 +57,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.12.0"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: e653f162ddfcec1da2040ba2d8553fff1662b5c2a5c636f4c21a3b11bee497de
url: "https://pub.dev"
source: hosted
version: "6.5.0"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
url: "https://pub.dev"
source: hosted
version: "5.2.1"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
url: "https://pub.dev"
source: hosted
version: "4.2.1"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
bloc: bloc:
dependency: transitive dependency: transitive
description: description:
@ -225,6 +289,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
connectivity_plus:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -265,6 +345,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
dart_jsonwebtoken:
dependency: "direct main"
description:
name: dart_jsonwebtoken
sha256: "21ce9f8a8712f741e8d6876a9c82c0f8a257fe928c4378a91d8527b92a3fd413"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -281,6 +369,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@ -297,6 +393,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
ed25519_edwards:
dependency: transitive
description:
name: ed25519_edwards
sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
equatable: equatable:
dependency: "direct main" dependency: "direct main"
description: description:
@ -917,6 +1021,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
octo_image: octo_image:
dependency: transitive dependency: transitive
description: description:
@ -1077,6 +1189,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
pool: pool:
dependency: transitive dependency: transitive
description: description:

View File

@ -58,6 +58,9 @@ dependencies:
cloud_firestore: ^5.6.12 cloud_firestore: ^5.6.12
firebase_storage: ^12.4.10 firebase_storage: ^12.4.10
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
connectivity_plus: ^6.1.4
dart_jsonwebtoken: ^3.2.0
audioplayers: ^6.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -87,6 +90,7 @@ flutter:
assets: assets:
- assets/images/ - assets/images/
- assets/icons/ - assets/icons/
- assets/sounds/
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see

View File

@ -6,7 +6,9 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <cloud_firestore/cloud_firestore_plugin_c_api.h> #include <cloud_firestore/cloud_firestore_plugin_c_api.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <firebase_auth/firebase_auth_plugin_c_api.h> #include <firebase_auth/firebase_auth_plugin_c_api.h>
#include <firebase_core/firebase_core_plugin_c_api.h> #include <firebase_core/firebase_core_plugin_c_api.h>
@ -19,8 +21,12 @@
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
CloudFirestorePluginCApiRegisterWithRegistrar( CloudFirestorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseAuthPluginCApiRegisterWithRegistrar( FirebaseAuthPluginCApiRegisterWithRegistrar(

View File

@ -3,7 +3,9 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
cloud_firestore cloud_firestore
connectivity_plus
file_selector_windows file_selector_windows
firebase_auth firebase_auth
firebase_core firebase_core