Compare commits

...

4 Commits

Author SHA1 Message Date
mohamadmahdi jebeli 60c91e8fbb fix category and change comment ui 2025-08-04 14:44:50 +03:30
mohamadmahdi jebeli ce62c567c5 added comment 2025-08-04 12:15:17 +03:30
mohamadmahdi jebeli 608222e8a3 add refresh offer_page 2025-08-04 10:35:28 +03:30
mohamadmahdi jebeli f4cd446cde location background activity 2025-08-03 15:44:57 +03:30
29 changed files with 1635 additions and 537 deletions

View File

@ -11,32 +11,29 @@ plugins {
android { android {
namespace = "com.example.proxibuy" namespace = "com.example.proxibuy"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = "27.0.12077973"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString() jvmTarget = "1.8"
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.proxibuy" applicationId = "com.example.proxibuy"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23 minSdk = 23
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
multiDexEnabled = true
} }
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
} }
} }
@ -45,3 +42,8 @@ android {
flutter { flutter {
source = "../.." source = "../.."
} }
dependencies {
// این نسخه به آخرین نسخه مورد نیاز آپدیت شد
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

View File

@ -1,18 +1,20 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application <application
android:label="Proxibuy" android:label="Proxibuy"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<!-- android:networkSecurityConfig="@xml/network_security_config"> -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@ -22,10 +24,7 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
@ -35,17 +34,16 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> <service
android:name="id.flutter.flutter_background_service.BackgroundService"
android:foregroundServiceType="location" />
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>

View File

@ -7,4 +7,7 @@ class ApiConfig {
static const String getFavoriteCategories = "/user/getfavoriteCategory"; static const String getFavoriteCategories = "/user/getfavoriteCategory";
static const String addReservation = "/reservation/add"; static const String addReservation = "/reservation/add";
static const String getReservations = "/reservation/get"; static const String getReservations = "/reservation/get";
static const String updateFcmToken = "/user/firebaseUpdate";
static const String addComment = "/comment/add";
static const String getComments = "/comment/get/";
} }

View File

@ -23,13 +23,18 @@ class CommentModel extends Equatable {
List<Object?> get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls]; List<Object?> get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls];
factory CommentModel.fromJson(Map<String, dynamic> json) { factory CommentModel.fromJson(Map<String, dynamic> json) {
final List<String> images = (json['UserImages'] as List<dynamic>?)
?.map((image) => image['Url'] as String)
.toList() ??
[];
return CommentModel( return CommentModel(
id: json['id'], id: json['ID'] ?? '',
userName: json['userName'], userName: json['User']?['Name'] ?? 'کاربر ناشناس',
rating: (json['rating'] as num).toDouble(), rating: (json['Score'] as num?)?.toDouble() ?? 0.0,
comment: json['comment'], comment: json['Text'] ?? '',
publishedAt: DateTime.parse(json['publishedAt']), publishedAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
uploadedImageUrls: List<String>.from(json['uploadedImageUrls'] ?? []), uploadedImageUrls: images,
); );
} }
} }

View File

@ -149,7 +149,7 @@ class OfferModel extends Equatable {
rating: 0.0, rating: 0.0,
ratingCount: 0, ratingCount: 0,
comments: [], comments: [],
discountInfo: json['Description'], discountInfo: json['Description'] ?? 'توضیحات موجود نیست',
); );
} }
@ -157,7 +157,29 @@ class OfferModel extends Equatable {
imageUrls.isNotEmpty ? imageUrls.first : 'https://via.placeholder.com/400x200.png?text=No+Image'; imageUrls.isNotEmpty ? imageUrls.first : 'https://via.placeholder.com/400x200.png?text=No+Image';
@override @override
List<Object?> get props => [id]; List<Object?> get props => [
id,
storeName,
title,
discount,
imageUrls,
category,
distanceInMeters,
expiryTime,
address,
workingHours,
discountType,
isOpen,
rating,
ratingCount,
latitude,
longitude,
originalPrice,
finalPrice,
features,
discountInfo,
comments,
];
String get distanceAsString { String get distanceAsString {
if (distanceInMeters < 1000) { if (distanceInMeters < 1000) {

View File

@ -1,7 +1,6 @@
// lib/main.dart
import 'dart:io'; import 'dart:io';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.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';
@ -10,18 +9,29 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:proxibuy/core/config/http_overrides.dart'; import 'package:proxibuy/core/config/http_overrides.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/comment/bloc/comment_bloc.dart'; // این ایمپورت اضافه شد
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_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/offer/bloc/offer_bloc.dart';
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
import 'package:proxibuy/services/background_service.dart';
import 'package:proxibuy/services/mqtt_service.dart'; import 'package:proxibuy/services/mqtt_service.dart';
import 'core/config/app_colors.dart'; import 'core/config/app_colors.dart';
import 'package:proxibuy/presentation/pages/splash_screen.dart'; import 'package:proxibuy/presentation/pages/splash_screen.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized();
HttpOverrides.global = MyHttpOverrides();
Future<String?> getFcmToken() async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
String? token = await messaging.getToken();
print("🔥 Firebase Messaging Token: $token");
return token;
}
WidgetsFlutterBinding.ensureInitialized();
await initializeService();
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());
@ -34,22 +44,18 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
RepositoryProvider<MqttService>( RepositoryProvider<MqttService>(create: (context) => MqttService()),
create: (context) => MqttService(),
),
BlocProvider<AuthBloc>( BlocProvider<AuthBloc>(
create: (context) => AuthBloc()..add(CheckAuthStatusEvent()), create: (context) => AuthBloc()..add(CheckAuthStatusEvent()),
), ),
BlocProvider<ReservationCubit>( BlocProvider<ReservationCubit>(create: (context) => ReservationCubit()),
create: (context) => ReservationCubit(), BlocProvider<OffersBloc>(create: (context) => OffersBloc()),
),
BlocProvider<OffersBloc>(
create: (context) => OffersBloc(),
),
BlocProvider<NotificationPreferencesBloc>( BlocProvider<NotificationPreferencesBloc>(
create: (context) => NotificationPreferencesBloc(), create: (context) => NotificationPreferencesBloc(),
), ),
BlocProvider<CommentBloc>(
create: (context) => CommentBloc(),
),
], ],
child: MaterialApp( child: MaterialApp(
title: 'Proxibuy', title: 'Proxibuy',

View File

@ -1,5 +1,3 @@
// lib/presentation/auth/bloc/auth_bloc.dart
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';
@ -15,7 +13,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
late final Dio _dio; late final Dio _dio;
final _storage = const FlutterSecureStorage(); final _storage = const FlutterSecureStorage();
AuthBloc() : super(AuthInitial()) { AuthBloc() : super(AuthUnknown()) {
_dio = Dio(); _dio = Dio();
_dio.interceptors.add( _dio.interceptors.add(
LogInterceptor( LogInterceptor(
@ -31,10 +29,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
on<VerifyOTPEvent>(_onVerifyOTP); on<VerifyOTPEvent>(_onVerifyOTP);
on<UpdateUserInfoEvent>(_onUpdateUserInfo); on<UpdateUserInfoEvent>(_onUpdateUserInfo);
on<LogoutEvent>(_onLogout); on<LogoutEvent>(_onLogout);
on<SendFcmTokenEvent>(_onSendFcmToken);
} }
Future<void> _onCheckAuthStatus( Future<void> _onCheckAuthStatus(
CheckAuthStatusEvent event, Emitter<AuthState> emit) async { CheckAuthStatusEvent event,
Emitter<AuthState> emit,
) async {
final token = await _storage.read(key: 'accessToken'); final token = await _storage.read(key: 'accessToken');
if (token != null && token.isNotEmpty) { if (token != null && token.isNotEmpty) {
emit(AuthSuccess()); emit(AuthSuccess());
@ -43,6 +44,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
} }
} }
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async { Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading()); emit(AuthLoading());
try { try {
@ -52,10 +54,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
); );
if (isClosed) return; if (isClosed) return;
if (response.statusCode == 200) { if (response.statusCode == 200) {
emit(AuthCodeSentSuccess( emit(
AuthCodeSentSuccess(
phone: event.phoneNumber, phone: event.phoneNumber,
countryCode: event.countryCode, countryCode: event.countryCode,
)); ),
);
} else { } else {
emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد')); emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد'));
} }
@ -65,7 +69,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
} }
} }
Future<void> _onVerifyOTP(VerifyOTPEvent event, Emitter<AuthState> emit) async { Future<void> _onVerifyOTP(
VerifyOTPEvent event,
Emitter<AuthState> emit,
) async {
emit(AuthLoading()); emit(AuthLoading());
try { try {
final response = await _dio.post( final response = await _dio.post(
@ -97,8 +104,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
} }
Future<void> _onUpdateUserInfo( Future<void> _onUpdateUserInfo(
UpdateUserInfoEvent event, Emitter<AuthState> emit) async { UpdateUserInfoEvent event,
debugPrint("AuthBloc: 🔵 ایونت UpdateUserInfoEvent دریافت شد با نام: ${event.name}"); 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');
@ -115,7 +126,9 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
options: Options(headers: {'Authorization': 'Bearer $token'}), options: Options(headers: {'Authorization': 'Bearer $token'}),
); );
debugPrint("AuthBloc: 🟠 پاسخ سرور دریافت شد. StatusCode: ${response.statusCode}"); debugPrint(
"AuthBloc: 🟠 پاسخ سرور دریافت شد. StatusCode: ${response.statusCode}",
);
if (isClosed) { if (isClosed) {
debugPrint("AuthBloc: 🔴 خطا: BLoC قبل از اتمام عملیات بسته شده است."); debugPrint("AuthBloc: 🔴 خطا: BLoC قبل از اتمام عملیات بسته شده است.");
@ -123,22 +136,49 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
} }
if (response.statusCode == 200) { if (response.statusCode == 200) {
debugPrint("AuthBloc: ✅ درخواست موفق بود. در حال emit کردن AuthSuccess..."); debugPrint(
"AuthBloc: ✅ درخواست موفق بود. در حال emit کردن AuthSuccess...",
);
emit(AuthSuccess()); emit(AuthSuccess());
} else { } else {
debugPrint("AuthBloc: 🔴 سرور پاسخ ناموفق داد: ${response.data['message']}"); debugPrint(
"AuthBloc: 🔴 سرور پاسخ ناموفق داد: ${response.data['message']}",
);
emit(AuthFailure(response.data['message'] ?? 'خطا در ثبت اطلاعات')); emit(AuthFailure(response.data['message'] ?? 'خطا در ثبت اطلاعات'));
} }
} on DioException catch (e) { } on DioException catch (e) {
debugPrint("AuthBloc: 🔴 خطای DioException رخ داد: ${e.response?.data['message']}"); debugPrint(
"AuthBloc: 🔴 خطای DioException رخ داد: ${e.response?.data['message']}",
);
if (isClosed) return; 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());
} }
Future<void> _onSendFcmToken(
SendFcmTokenEvent event,
Emitter<AuthState> emit,
) async {
try {
final token = await _storage.read(key: 'accessToken');
if (token == null) {
emit(const AuthFailure("شما وارد نشده‌اید."));
return;
}
await _dio.post(
ApiConfig.baseUrl + ApiConfig.updateFcmToken,
data: {'Token': event.fcmToken},
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
print("Firebase token: ${event.fcmToken}");
} on DioException catch (e) {
debugPrint("Error sending FCM token: ${e.response?.data['message']}");
}
}
} }

View File

@ -26,3 +26,8 @@ class UpdateUserInfoEvent extends AuthEvent {
UpdateUserInfoEvent({required this.name, required this.gender}); UpdateUserInfoEvent({required this.name, required this.gender});
} }
class SendFcmTokenEvent extends AuthEvent {
final String fcmToken;
SendFcmTokenEvent({required this.fcmToken});
}

View File

@ -7,6 +7,8 @@ abstract class AuthState extends Equatable {
List<Object?> get props => []; List<Object?> get props => [];
} }
class AuthUnknown extends AuthState {}
class AuthInitial extends AuthState {} class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {} class AuthLoading extends AuthState {}

View File

@ -0,0 +1,81 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:proxibuy/core/config/api_config.dart';
import 'package:proxibuy/presentation/comment/bloc/comment_event.dart';
import 'package:proxibuy/presentation/comment/bloc/comment_state.dart';
import 'package:http_parser/http_parser.dart';
import 'package:flutter/foundation.dart';
class CommentBloc extends Bloc<CommentEvent, CommentState> {
late final Dio _dio;
final FlutterSecureStorage _storage = const FlutterSecureStorage();
CommentBloc() : super(CommentInitial()) {
_dio = Dio();
_dio.interceptors.add(
LogInterceptor(
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
logPrint: (obj) => debugPrint(obj.toString()),
),
);
on<SubmitComment>(_onSubmitComment);
}
Future<void> _onSubmitComment(SubmitComment event, Emitter<CommentState> emit) async {
if (event.text.isEmpty && event.score == 0) {
emit(const CommentSubmissionFailure("لطفا امتیاز یا نظری برای این تخفیف ثبت کنید."));
return;
}
emit(CommentSubmitting());
try {
final token = await _storage.read(key: 'accessToken');
if (token == null) {
emit(const CommentSubmissionFailure("شما وارد نشده‌اید."));
return;
}
final formData = FormData.fromMap({
'Discount': event.discountId,
'Text': event.text,
'Score': event.score,
});
for (File imageFile in event.images) {
formData.files.add(MapEntry(
'Images',
await MultipartFile.fromFile(
imageFile.path,
filename: imageFile.path.split('/').last,
contentType: MediaType('image', 'jpeg'),
),
));
}
final response = await _dio.post(
ApiConfig.baseUrl + ApiConfig.addComment,
data: formData,
options: Options(
headers: {'Authorization': 'Bearer $token'},
),
);
if (response.statusCode == 200 || response.statusCode == 201) {
emit(CommentSubmissionSuccess());
} else {
emit(CommentSubmissionFailure(response.data['message'] ?? 'خطا در ارسال نظر'));
}
} on DioException catch (e) {
emit(CommentSubmissionFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
} catch (e) {
emit(CommentSubmissionFailure('خطایی ناشناخته رخ داد: $e'));
}
}
}

View File

@ -0,0 +1,26 @@
import 'dart:io';
import 'package:equatable/equatable.dart';
abstract class CommentEvent extends Equatable {
const CommentEvent();
@override
List<Object> get props => [];
}
class SubmitComment extends CommentEvent {
final String discountId;
final String text;
final double score;
final List<File> images;
const SubmitComment({
required this.discountId,
required this.text,
required this.score,
required this.images,
});
@override
List<Object> get props => [discountId, text, score, images];
}

View File

@ -0,0 +1,23 @@
import 'package:equatable/equatable.dart';
abstract class CommentState extends Equatable {
const CommentState();
@override
List<Object> get props => [];
}
class CommentInitial extends CommentState {}
class CommentSubmitting extends CommentState {}
class CommentSubmissionSuccess extends CommentState {}
class CommentSubmissionFailure extends CommentState {
final String error;
const CommentSubmissionFailure(this.error);
@override
List<Object> get props => [error];
}

View File

@ -1,3 +1,5 @@
// lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart
import 'package:dio/dio.dart'; import 'package:dio/dio.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';
@ -26,12 +28,12 @@ class NotificationPreferencesBloc
LoadCategories event, Emitter<NotificationPreferencesState> emit) { LoadCategories event, Emitter<NotificationPreferencesState> emit) {
final categories = [ final categories = [
CategoryEntity(id: "e33dd7f9-5b20-4273-8eea-59da6ca5f206", name: 'لوازم دیجیتال', icon: Assets.icons.digital), CategoryEntity(id: "e33dd7f9-5b20-4273-8eea-59da6ca5f206", name: 'لوازم دیجیتال', icon: Assets.icons.digital),
CategoryEntity(id: "b73a868a-a2d2-4d96-8fd4-615327ed9629", name: 'کافیشاپ', icon: Assets.icons.coffeeshop), CategoryEntity(id: "b73a868a-a2d2-4d96-8fd4-615327ed9629", name: 'کافی شاپ', icon: Assets.icons.coffeeshop), // Change Here
CategoryEntity(id: "b5881239-bfd5-4c27-967a-187316a7e0b7", name: 'رستوران', icon: Assets.icons.resturan), CategoryEntity(id: "b5881239-bfd5-4c27-967a-187316a7e0b7", name: 'رستوران', icon: Assets.icons.resturan),
CategoryEntity(id: "6803b940-3e19-48cd-9190-28d9f25421ff", name: 'فستفود', icon: Assets.icons.fastfood), CategoryEntity(id: "6803b940-3e19-48cd-9190-28d9f25421ff", name: 'فست فود', icon: Assets.icons.fastfood), // Change Here
CategoryEntity(id: "71e371f8-a47a-4a58-aee6-4ed0f26bf29b", name: 'پوشاک', icon: Assets.icons.pooshak), CategoryEntity(id: "71e371f8-a47a-4a58-aee6-4ed0f26bf29b", name: 'پوشاک', icon: Assets.icons.pooshak),
CategoryEntity(id: "42acff41-1165-4e62-89b9-58db7329ec3a", name: 'تریا', icon: Assets.icons.teria), CategoryEntity(id: "42acff41-1165-4e62-89b9-58db7329ec3a", name: 'تریا', icon: Assets.icons.teria),
CategoryEntity(id: "2f38918c-5566-4aec-a0a9-2c7c48b1e878", name: 'کیف‌وکفش', icon: Assets.icons.kafsh), CategoryEntity(id: "2f38918c-5566-4aec-a0a9-2c7c48b1e878", name: 'کیف و کفش', icon: Assets.icons.kafsh), // Change Here
CategoryEntity(id: "52c51010-3a63-4264-a350-e011c889f3dd", name: 'سینما', icon: Assets.icons.cinama), CategoryEntity(id: "52c51010-3a63-4264-a350-e011c889f3dd", name: 'سینما', icon: Assets.icons.cinama),
CategoryEntity(id: "34185954-f79f-4b9e-8eb2-1702679c40a0", name: 'لوازم آرایشی', icon: Assets.icons.arayesh), CategoryEntity(id: "34185954-f79f-4b9e-8eb2-1702679c40a0", name: 'لوازم آرایشی', icon: Assets.icons.arayesh),
CategoryEntity(id: "e4517b0c-aacf-4758-94bd-85f45062980f", name: 'طلا و زیورآلات', icon: Assets.icons.tala), CategoryEntity(id: "e4517b0c-aacf-4758-94bd-85f45062980f", name: 'طلا و زیورآلات', icon: Assets.icons.tala),
@ -127,5 +129,3 @@ class NotificationPreferencesBloc
emit(state.copyWith(submissionSuccess: false)); emit(state.copyWith(submissionSuccess: false));
} }
} }

View File

@ -25,7 +25,6 @@ class AddPhotoScreen extends StatelessWidget {
required this.offer, required this.offer,
}); });
// متد ساخت توکن
Future<String> _generateQrToken(BuildContext context) async { Future<String> _generateQrToken(BuildContext context) async {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
final userID = await storage.read(key: 'userID'); final userID = await storage.read(key: 'userID');

View File

@ -0,0 +1,414 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/presentation/comment/bloc/comment_bloc.dart';
import 'package:proxibuy/presentation/comment/bloc/comment_event.dart';
import 'package:proxibuy/presentation/comment/bloc/comment_state.dart';
import 'package:proxibuy/presentation/pages/offers_page.dart';
import 'package:flutter_animate/flutter_animate.dart';
class CommentPage extends StatefulWidget {
final String discountId;
const CommentPage({super.key, required this.discountId});
@override
State<CommentPage> createState() => _CommentPageState();
}
class _CommentPageState extends State<CommentPage> {
final _commentController = TextEditingController();
double _rating = 0.0;
final List<File> _images = [];
final ImagePicker _picker = ImagePicker();
@override
void dispose() {
_commentController.dispose();
super.dispose();
}
void _pickImage(ImageSource source) async {
if (_images.length >= 2) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('شما فقط می‌توانید ۲ عکس اضافه کنید.')),
);
return;
}
final pickedFile = await _picker.pickImage(
source: source,
imageQuality: 80,
);
if (pickedFile != null) {
setState(() {
_images.add(File(pickedFile.path));
});
}
}
void _removeImage(int index) {
setState(() {
_images.removeAt(index);
});
}
void _showImageSourceActionSheet() {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
),
builder: (context) {
return SafeArea(
child: Wrap(
children: <Widget>[
ListTile(
leading: const Icon(
Icons.photo_library,
color: AppColors.primary,
),
title: const Text('انتخاب از گالری'),
onTap: () {
Navigator.of(context).pop();
_pickImage(ImageSource.gallery);
},
),
ListTile(
leading: const Icon(Icons.camera_alt, color: AppColors.primary),
title: const Text('گرفتن عکس با دوربین'),
onTap: () {
Navigator.of(context).pop();
_pickImage(ImageSource.camera);
},
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CommentBloc(),
child: Scaffold(
backgroundColor: Colors.grey[50],
appBar: _buildCustomAppBar(),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 24),
Center(child: _buildRatingCard()),
const SizedBox(height: 16),
_buildCommentCard(),
const SizedBox(height: 16),
_buildImagePickerSection(),
const SizedBox(height: 32),
_buildActionButtons(),
]
.animate(interval: 100.ms)
.fadeIn(duration: 400.ms)
.slideY(begin: 0.2, curve: Curves.easeOut),
),
),
),
);
}
PreferredSizeWidget _buildCustomAppBar() {
return PreferredSize(
preferredSize: const Size.fromHeight(70.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(15),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Text(
'ثبت نظر',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.normal,
fontSize: 18,
),
),
IconButton(
icon: SvgPicture.asset(Assets.icons.arrowLeft.path),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
),
),
);
}
Widget _buildHeader() {
return Center(
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.confirm.withOpacity(0.1),
),
child: Icon(
Icons.check_circle_outline,
color: AppColors.confirm,
size: 50,
),
),
const SizedBox(height: 16),
const Text(
'خریدت با موفقیت انجام شد. منتظر دیدار دوباره‌ات هستیم. لطفا نظرت رو در مورد این تخفیف بهمون بگو.',
style: TextStyle(fontSize: 16, color: Colors.black, height: 1.5),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildRatingCard() {
return Card(
elevation: 1,
shadowColor: Colors.black12,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text(
"امتیاز شما چقدره؟",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
RatingBar.builder(
initialRating: 0,
minRating: 1,
direction: Axis.horizontal,
itemCount: 5,
itemPadding: const EdgeInsets.symmetric(horizontal: 7.0),
itemBuilder:
(context, _) =>
Icon(Icons.star, color: Colors.amber.shade700),
onRatingUpdate: (rating) => setState(() => _rating = rating),
),
],
),
),
);
}
Widget _buildCommentCard() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _commentController,
maxLines: 4,
textAlign: TextAlign.right,
decoration: InputDecoration(
labelText: "گوشمون به شماست",
hintText: "نظراتت رو بگو...",
alignLabelWithHint: true,
suffixIcon: Padding(padding: const EdgeInsets.all(12.0)),
),
),
);
}
Widget _buildImagePickerSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"افزودن عکس (اختیاری)",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
"می‌تونی عکس تجربه خریدت رو باهامون به اشتراک بذاری!",
style: TextStyle(fontSize: 14, color: AppColors.hint),
),
const SizedBox(height: 16),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
...List.generate(_images.length, (index) {
return Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Image.file(
_images[index],
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
Positioned(
top: -8,
right: -8,
child: GestureDetector(
onTap: () => _removeImage(index),
child: Container(
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 16,
),
),
),
),
],
),
);
}),
if (_images.length < 2)
GestureDetector(
onTap: _showImageSourceActionSheet,
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade400,
width: 1.5,
style: BorderStyle.solid,
),
),
child: const Icon(
Icons.add_a_photo_outlined,
color: Colors.grey,
size: 30,
),
),
),
],
),
),
],
);
}
Widget _buildActionButtons() {
return BlocConsumer<CommentBloc, CommentState>(
listener: (context, state) {
if (state is CommentSubmissionSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('نظر شما با موفقیت ثبت شد. ممنونیم!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const OffersPage()),
(route) => false,
);
} else if (state is CommentSubmissionFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.error),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
),
);
}
},
builder: (context, state) {
final isLoading = state is CommentSubmitting;
return Column(
children: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed:
isLoading
? null
: () {
context.read<CommentBloc>().add(
SubmitComment(
discountId: widget.discountId,
text: _commentController.text,
score: _rating,
images: _images,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.confirm,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
),
child:
isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(color: Colors.white),
)
: const Text('ارسال'),
),
),
const SizedBox(height: 12),
TextButton(
onPressed:
isLoading
? null
: () {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const OffersPage()),
(route) => false,
);
},
child: const Text(
'رد شدن',
style: TextStyle(color: Colors.black),
),
),
],
);
},
);
}
}

View File

@ -1,3 +1,4 @@
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:flutter_secure_storage/flutter_secure_storage.dart';
@ -130,7 +131,7 @@ class _NotificationPreferencesPageState
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
), ),
body: BlocListener<NotificationPreferencesBloc, body: BlocConsumer<NotificationPreferencesBloc,
NotificationPreferencesState>( NotificationPreferencesState>(
listener: (context, state) async { listener: (context, state) async {
if (state.submissionSuccess) { if (state.submissionSuccess) {
@ -176,7 +177,10 @@ class _NotificationPreferencesPageState
); );
} }
}, },
child: Padding( builder: (context, state) {
return Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -212,9 +216,8 @@ class _NotificationPreferencesPageState
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Expanded( Expanded(
child: BlocBuilder<NotificationPreferencesBloc, child: Builder(
NotificationPreferencesState>( builder: (context) {
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());
} }
@ -258,18 +261,13 @@ class _NotificationPreferencesPageState
}, },
), ),
), ),
BlocBuilder<NotificationPreferencesBloc, if (state.selectedCategoryIds.isNotEmpty)
NotificationPreferencesState>( SizedBox(
builder: (context, state) {
final areCategoriesSelected =
state.selectedCategoryIds.isNotEmpty;
if (areCategoriesSelected) {
return SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: !state.isLoading onPressed: state.isLoading
? () async { ? null
: () async {
final bloc = final bloc =
context.read<NotificationPreferencesBloc>(); context.read<NotificationPreferencesBloc>();
@ -288,21 +286,17 @@ class _NotificationPreferencesPageState
selectedCategoryNames); selectedCategoryNames);
bloc.add(SubmitPreferences()); bloc.add(SubmitPreferences());
} },
: null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.confirm, backgroundColor: AppColors.confirm,
foregroundColor: Colors.white, foregroundColor: Colors.white,
disabledBackgroundColor: Colors.grey, disabledBackgroundColor: Colors.grey.withOpacity(0.5),
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
), ),
), ),
child: state.isLoading child: const Text(
? const CircularProgressIndicator(
color: Colors.white)
: const Text(
'اعمال', 'اعمال',
style: TextStyle( style: TextStyle(
fontFamily: 'Dana', fontFamily: 'Dana',
@ -311,16 +305,23 @@ class _NotificationPreferencesPageState
), ),
), ),
), ),
);
} else {
return const SizedBox.shrink();
}
},
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
), ),
), ),
if (state.isLoading)
Container(
color: Colors.black.withOpacity(0.4),
child: const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
),
],
);
},
), ),
); );
} }

View File

@ -1,3 +1,4 @@
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:connectivity_plus/connectivity_plus.dart';
@ -5,6 +6,7 @@ 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_background_service/flutter_background_service.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';
@ -23,7 +25,6 @@ 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/gps_dialog.dart';
import 'package:proxibuy/presentation/widgets/notification_permission_dialog.dart'; import 'package:proxibuy/presentation/widgets/notification_permission_dialog.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 {
@ -38,10 +39,7 @@ class OffersPage extends StatefulWidget {
class _OffersPageState extends State<OffersPage> { class _OffersPageState extends State<OffersPage> {
List<String> _selectedCategories = []; List<String> _selectedCategories = [];
StreamSubscription? _locationServiceSubscription; StreamSubscription? _locationServiceSubscription;
StreamSubscription? _mqttMessageSubscription;
StreamSubscription? _connectivitySubscription; StreamSubscription? _connectivitySubscription;
Timer? _locationTimer;
bool _isSubscribedToOffers = false;
bool _isGpsEnabled = false; bool _isGpsEnabled = false;
bool _isConnectedToInternet = true; bool _isConnectedToInternet = true;
@ -57,8 +55,6 @@ class _OffersPageState extends State<OffersPage> {
@override @override
void dispose() { void dispose() {
_locationServiceSubscription?.cancel(); _locationServiceSubscription?.cancel();
_mqttMessageSubscription?.cancel();
_locationTimer?.cancel();
_connectivitySubscription?.cancel(); _connectivitySubscription?.cancel();
super.dispose(); super.dispose();
} }
@ -67,7 +63,8 @@ class _OffersPageState extends State<OffersPage> {
final connectivityResult = await Connectivity().checkConnectivity(); final connectivityResult = await Connectivity().checkConnectivity();
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_isConnectedToInternet = !connectivityResult.contains(ConnectivityResult.none); _isConnectedToInternet =
!connectivityResult.contains(ConnectivityResult.none);
}); });
} }
@ -80,10 +77,37 @@ class _OffersPageState extends State<OffersPage> {
} }
}); });
} }
await _loadPreferences(); await _loadPreferences();
_initLocationListener(); _initLocationListener();
_subscribeToUserOffersOnLoad(); _listenToBackgroundService();
}
void _listenToBackgroundService() {
FlutterBackgroundService().on('update').listen((event) {
if (event == null || event['offers'] == null) return;
final data = event['offers']['data'];
if (data == null || data is! List) {
if (mounted) {
context.read<OffersBloc>().add(const OffersReceivedFromMqtt([]));
}
return;
}
try {
List<OfferModel> offers = data
.whereType<Map<String, dynamic>>()
.map((json) => OfferModel.fromJson(json))
.toList();
if (mounted) {
context.read<OffersBloc>().add(OffersReceivedFromMqtt(offers));
}
} catch (e, stackTrace) {
debugPrint("❌ Error parsing offers from Background Service: $e");
debugPrint(stackTrace.toString());
}
});
} }
void _initConnectivityListener() { void _initConnectivityListener() {
@ -143,14 +167,6 @@ class _OffersPageState extends State<OffersPage> {
} }
} }
Future<void> _subscribeToUserOffersOnLoad() async {
final storage = const FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
if (userID != null && mounted) {
_subscribeToUserOffers(userID);
}
}
void _initLocationListener() { void _initLocationListener() {
_checkInitialGpsStatus(); _checkInitialGpsStatus();
_locationServiceSubscription = _locationServiceSubscription =
@ -160,11 +176,7 @@ class _OffersPageState extends State<OffersPage> {
setState(() { setState(() {
_isGpsEnabled = isEnabled; _isGpsEnabled = isEnabled;
}); });
if (isEnabled) { if (!isEnabled) {
_startSendingLocationUpdates();
} else {
debugPrint("❌ Location Service Disabled. Stopping updates.");
_locationTimer?.cancel();
context.read<OffersBloc>().add(ClearOffers()); context.read<OffersBloc>().add(ClearOffers());
} }
} }
@ -177,104 +189,8 @@ class _OffersPageState extends State<OffersPage> {
setState(() { setState(() {
_isGpsEnabled = status; _isGpsEnabled = status;
}); });
if (_isGpsEnabled) {
_startSendingLocationUpdates();
} }
} }
}
void _startSendingLocationUpdates() {
debugPrint("🚀 Starting periodic location updates.");
_locationTimer?.cancel();
_locationTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
_sendLocationUpdate();
});
_sendLocationUpdate();
}
Future<void> _sendLocationUpdate() async {
if (!_isConnectedToInternet || !_isGpsEnabled) return;
final mqttService = context.read<MqttService>();
if (!mqttService.isConnected) {
debugPrint("⚠️ MQTT not connected in OffersPage. Cannot send location.");
return;
}
try {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
debugPrint("🚫 Location permission denied by user.");
return;
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
const storage = FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
if (userID == null) {
debugPrint("⚠️ UserID not found. Cannot send location.");
return;
}
final payload = {
"userID": userID,
"lat":32.6685,
"lng": 51.6826
};
mqttService.publish("proxybuy/sendGps", payload);
} catch (e) {
debugPrint("❌ Error sending location update in OffersPage: $e");
}
}
void _subscribeToUserOffers(String userID) {
if (_isSubscribedToOffers) return;
final mqttService = context.read<MqttService>();
if (!mqttService.isConnected) {
debugPrint("⚠️ Cannot subscribe. MQTT client is not connected.");
return;
}
final topic = 'user-proxybuy/$userID';
mqttService.subscribe(topic);
_isSubscribedToOffers = true;
_mqttMessageSubscription = mqttService.messages.listen((message) {
final data = message['data'];
if (data == null || data is! List) {
if (mounted) {
context.read<OffersBloc>().add(const OffersReceivedFromMqtt([]));
}
return;
}
try {
List<OfferModel> offers = data
.whereType<Map<String, dynamic>>()
.map((json) => OfferModel.fromJson(json))
.toList();
if (mounted) {
context.read<OffersBloc>().add(OffersReceivedFromMqtt(offers));
}
} catch (e, stackTrace) {
debugPrint("❌ Error parsing offers from MQTT: $e");
debugPrint(stackTrace.toString());
}
});
}
Future<void> _loadPreferences() async { Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -288,6 +204,39 @@ class _OffersPageState extends State<OffersPage> {
} }
} }
Future<void> _handleRefresh() {
final completer = Completer<void>();
final service = FlutterBackgroundService();
final timeout = Timer(const Duration(seconds: 20), () {
if (!completer.isCompleted) {
completer.completeError('Request timed out.');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Request timed out. Please try again.')),
);
}
}
});
final StreamSubscription<Map<String, dynamic>?> subscription =
service.on('update').listen((event) {
if (!completer.isCompleted) {
completer.complete();
}
});
completer.future.whenComplete(() {
subscription.cancel();
timeout.cancel();
});
service.invoke('force_refresh');
return completer.future;
}
Widget _buildFavoriteCategoriesSection() { Widget _buildFavoriteCategoriesSection() {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0),
@ -303,7 +252,7 @@ class _OffersPageState extends State<OffersPage> {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await Navigator.of(context).push<bool>( final result = await Navigator.of(context).push<bool>(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const NotificationPreferencesPage( builder: (context) => const NotificationPreferencesPage(
loadFavoritesOnStart: true, loadFavoritesOnStart: true,
@ -317,7 +266,11 @@ class _OffersPageState extends State<OffersPage> {
.read<NotificationPreferencesBloc>() .read<NotificationPreferencesBloc>()
.add(ResetSubmissionStatus()); .add(ResetSubmissionStatus());
_loadPreferences(); await _loadPreferences();
if (result == true) {
_handleRefresh();
}
}, },
child: Row( child: Row(
children: [ children: [
@ -445,7 +398,10 @@ class _OffersPageState extends State<OffersPage> {
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
), ),
body: SingleChildScrollView( body: RefreshIndicator(
onRefresh: _handleRefresh,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -453,11 +409,13 @@ class _OffersPageState extends State<OffersPage> {
OffersView( OffersView(
isGpsEnabled: _isGpsEnabled, isGpsEnabled: _isGpsEnabled,
isConnectedToInternet: _isConnectedToInternet, isConnectedToInternet: _isConnectedToInternet,
selectedCategories: _selectedCategories,
), ),
], ],
), ),
), ),
), ),
),
); );
} }
} }
@ -465,11 +423,13 @@ class _OffersPageState extends State<OffersPage> {
class OffersView extends StatelessWidget { class OffersView extends StatelessWidget {
final bool isGpsEnabled; final bool isGpsEnabled;
final bool isConnectedToInternet; final bool isConnectedToInternet;
final List<String> selectedCategories;
const OffersView({ const OffersView({
super.key, super.key,
required this.isGpsEnabled, required this.isGpsEnabled,
required this.isConnectedToInternet, required this.isConnectedToInternet,
required this.selectedCategories,
}); });
@override @override
@ -501,7 +461,13 @@ class OffersView extends StatelessWidget {
} }
if (state is OffersLoadSuccess) { if (state is OffersLoadSuccess) {
if (state.offers.isEmpty) { final filteredOffers = selectedCategories.isEmpty
? state.offers
: state.offers
.where((offer) => selectedCategories.contains(offer.category))
.toList();
if (filteredOffers.isEmpty) {
return const SizedBox( return const SizedBox(
height: 300, height: 300,
child: Center( child: Center(
@ -517,7 +483,7 @@ class OffersView extends StatelessWidget {
} }
final groupedOffers = groupBy( final groupedOffers = groupBy(
state.offers, filteredOffers,
(OfferModel offer) => offer.category, (OfferModel offer) => offer.category,
); );
final categories = groupedOffers.keys.toList(); final categories = groupedOffers.keys.toList();

View File

@ -1,3 +1,4 @@
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:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@ -11,6 +12,7 @@ import 'package:maps_launcher/maps_launcher.dart';
import 'package:proxibuy/core/config/api_config.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/comment_model.dart';
import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/data/models/offer_model.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';
@ -87,20 +89,9 @@ class ProductDetailPage extends StatelessWidget {
headers: {'Authorization': 'Bearer $token'}, headers: {'Authorization': 'Bearer $token'},
); );
debugPrint("----------- REQUEST-----------");
debugPrint("URL: POST $url");
debugPrint("Headers: ${options.headers}");
debugPrint("Body: $data");
debugPrint("-----------------------------");
final response = final response =
await dio.post(url, data: data, options: options); 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 (context.mounted) Navigator.of(context).pop();
if (response.statusCode == 200) { if (response.statusCode == 200) {
@ -131,11 +122,6 @@ class ProductDetailPage extends StatelessWidget {
} on DioException catch (e) { } on DioException catch (e) {
if (context.mounted) Navigator.of(context).pop(); 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'] ?? final errorMessage = e.response?.data?['message'] ??
'خطای سرور هنگام رزرو. لطفاً دوباره تلاش کنید.'; 'خطای سرور هنگام رزرو. لطفاً دوباره تلاش کنید.';
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -145,10 +131,6 @@ class ProductDetailPage extends StatelessWidget {
); );
} catch (e) { } catch (e) {
if (context.mounted) Navigator.of(context).pop(); if (context.mounted) Navigator.of(context).pop();
debugPrint("---------- GENERAL ERROR -----------");
debugPrint(e.toString());
debugPrint("------------------------------------");
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(e.toString()), content: Text(e.toString()),
@ -205,14 +187,56 @@ class _ProductDetailViewState extends State<ProductDetailView> {
late List<String> imageList; late List<String> imageList;
late String selectedImage; late String selectedImage;
final String _uploadKey = 'upload_image'; final String _uploadKey = 'upload_image';
late Future<List<CommentModel>> _commentsFuture;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
imageList = List.from(widget.offer.imageUrls)..add(_uploadKey); imageList = List.from(widget.offer.imageUrls)..add(_uploadKey);
selectedImage = imageList.first; selectedImage = imageList.isNotEmpty ? imageList.first : 'https://via.placeholder.com/400x200.png?text=No+Image';
_commentsFuture = _fetchComments();
} }
Future<List<CommentModel>> _fetchComments() async {
// 1. توکن را از حافظه امن بخوان
const storage = FlutterSecureStorage();
final token = await storage.read(key: 'accessToken');
// 2. اگر توکن وجود نداشت، خطا برگردان
// هرچند کاربر لاگین نکرده معمولا به این صفحه دسترسی ندارد
if (token == null) {
throw Exception('Authentication token not found!');
}
try {
final dio = Dio();
// 3. هدر Authorization را به درخواست اضافه کن
final response = await dio.get(
ApiConfig.baseUrl + ApiConfig.getComments + widget.offer.id,
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
if (response.statusCode == 200) {
final List<dynamic> commentsJson = response.data['data']['comments'];
return commentsJson.map((json) => CommentModel.fromJson(json)).toList();
} else {
// خطاهای دیگر سرور
throw Exception('Failed to load comments with status code: ${response.statusCode}');
}
} on DioException catch (e) {
// چاپ خطای کامل Dio برای دیباگ بهتر
debugPrint("DioException fetching comments: $e");
if (e.response != null) {
debugPrint("Response data: ${e.response?.data}");
}
throw Exception('Failed to load comments: ${e.message}');
} catch (e) {
debugPrint("Error fetching comments: $e");
throw Exception('An unknown error occurred: $e');
}
}
// ############ END: FIX SECTION ############
void _launchMaps(double lat, double lon, String title) { void _launchMaps(double lat, double lon, String title) {
MapsLauncher.launchCoordinates(lat, lon, title); MapsLauncher.launchCoordinates(lat, lon, title);
} }
@ -405,8 +429,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
? widget.offer.expiryTime.difference(DateTime.now()) ? widget.offer.expiryTime.difference(DateTime.now())
: Duration.zero; : Duration.zero;
final formatCurrency = final formatCurrency = NumberFormat.decimalPattern('fa_IR');
NumberFormat.decimalPattern('fa_IR'); // Or 'en_US'
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0) padding: const EdgeInsets.symmetric(horizontal: 24.0)
@ -687,7 +710,26 @@ class _ProductDetailViewState extends State<ProductDetailView> {
const SizedBox(height: 24), const SizedBox(height: 24),
_buildDiscountTypeSection(), _buildDiscountTypeSection(),
const SizedBox(height: 24), const SizedBox(height: 24),
CommentsSection(comments: widget.offer.comments), FutureBuilder<List<CommentModel>>(
future: _commentsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('خطا در بارگذاری نظرات. لطفاً صفحه را رفرش کنید.'));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 32.0),
child: Text('هنوز نظری برای این تخفیف ثبت نشده است.'),
),
);
}
return CommentsSection(comments: snapshot.data!);
},
),
].animate(interval: 80.ms).slideX(begin: -0.05).fadeIn( ].animate(interval: 80.ms).slideX(begin: -0.05).fadeIn(
duration: 400.ms, duration: 400.ms,
curve: Curves.easeOut, curve: Curves.easeOut,

View File

@ -1,12 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'package:audioplayers/audioplayers.dart'; 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_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/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/presentation/pages/comment_page.dart';
import 'package:proxibuy/services/mqtt_service.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
@ -30,17 +33,17 @@ class _ReservationConfirmationPageState
Timer? _timer; Timer? _timer;
Duration _remaining = Duration.zero; Duration _remaining = Duration.zero;
final AudioPlayer _audioPlayer = AudioPlayer(); final AudioPlayer _audioPlayer = AudioPlayer();
StreamSubscription? _mqttSubscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_playSound(); _playSound();
_calculateRemainingTime(); _calculateRemainingTime();
_timer = Timer.periodic(const Duration(seconds: 1), (timer) { _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_calculateRemainingTime(); _calculateRemainingTime();
}); });
_listenToMqtt();
} }
void _playSound() async { void _playSound() async {
@ -69,14 +72,46 @@ class _ReservationConfirmationPageState
} }
} }
void _listenToMqtt() async {
final mqttService = context.read<MqttService>();
const storage = FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
final discountId = widget.offer.id;
if (userID == null) {
debugPrint("MQTT Listener: UserID not found, cannot subscribe.");
return;
}
final topic = 'user-order/$userID/$discountId';
mqttService.subscribe(topic);
debugPrint("✅ Subscribed to MQTT topic: $topic");
_mqttSubscription = mqttService.messages.listen((message) {
debugPrint("✅ MQTT Message received on details page: $message");
final receivedDiscountId = message['Discount'];
if (receivedDiscountId == discountId) {
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => CommentPage(discountId: discountId),
),
);
});
}
}
});
}
@override @override
void dispose() { void dispose() {
_timer?.cancel(); _timer?.cancel();
_audioPlayer.dispose(); _audioPlayer.dispose();
_mqttSubscription?.cancel();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Directionality( return Directionality(

View File

@ -3,12 +3,14 @@ import 'package:connectivity_plus/connectivity_plus.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_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
import 'package:proxibuy/presentation/pages/onboarding_page.dart'; import 'package:proxibuy/presentation/pages/onboarding_page.dart';
import 'package:proxibuy/presentation/pages/offers_page.dart'; import 'package:proxibuy/presentation/pages/offers_page.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/services/mqtt_service.dart'; import 'package:proxibuy/services/mqtt_service.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
class SplashScreen extends StatefulWidget { class SplashScreen extends StatefulWidget {
const SplashScreen({super.key}); const SplashScreen({super.key});
@ -28,9 +30,24 @@ class _SplashScreenState extends State<SplashScreen> {
Future.delayed(const Duration(seconds: 2), _startProcess); Future.delayed(const Duration(seconds: 2), _startProcess);
} }
Future<String?> getFcmToken() async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
try {
String? token = await messaging.getToken();
debugPrint("🔥 Firebase Messaging Token: $token");
return token;
} catch(e) {
debugPrint("Error getting FCM token: $e");
return null;
}
}
Future<void> _startProcess() async { Future<void> _startProcess() async {
if (!mounted) return; if (!mounted) return;
await _requestPermissions();
final hasInternet = await _checkInternet(); final hasInternet = await _checkInternet();
if (!hasInternet) { if (!hasInternet) {
setState(() { setState(() {
@ -60,11 +77,18 @@ class _SplashScreenState extends State<SplashScreen> {
return; return;
} }
final String? fcmToken = await getFcmToken();
final mqttService = context.read<MqttService>(); final mqttService = context.read<MqttService>();
final storage = const FlutterSecureStorage(); final storage = const FlutterSecureStorage();
final token = await storage.read(key: 'accessToken'); final token = await storage.read(key: 'accessToken');
if (token != null && token.isNotEmpty) { if (token != null && token.isNotEmpty) {
if (fcmToken != null) {
context.read<AuthBloc>().add(SendFcmTokenEvent(fcmToken: fcmToken));
}
if (mqttService.isConnected) { if (mqttService.isConnected) {
_navigateToOffers(); _navigateToOffers();
return; return;
@ -186,4 +210,13 @@ class _SplashScreenState extends State<SplashScreen> {
), ),
); );
} }
Future<void> _requestPermissions() async {
await Permission.notification.request();
var status = await Permission.location.request();
if (status.isGranted) {
await Permission.locationAlways.request();
}
}
} }

View File

@ -1,11 +1,15 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
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:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/presentation/pages/comment_page.dart';
import 'package:proxibuy/services/mqtt_service.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/config/app_colors.dart';
import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/data/models/offer_model.dart';
@ -25,6 +29,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
Timer? _timer; Timer? _timer;
Duration _remaining = Duration.zero; Duration _remaining = Duration.zero;
Future<String>? _qrTokenFuture; Future<String>? _qrTokenFuture;
StreamSubscription? _mqttSubscription; // برای مدیریت لیسنر MQTT
@override @override
void initState() { void initState() {
@ -57,11 +62,53 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
} }
void _toggleExpansion() { void _toggleExpansion() {
final isExpired = _remaining <= Duration.zero;
if (isExpired) return;
setState(() { setState(() {
_isExpanded = !_isExpanded; _isExpanded = !_isExpanded;
if (_isExpanded && _qrTokenFuture == null) { if (_isExpanded) {
if (_qrTokenFuture == null) {
_qrTokenFuture = _generateQrToken(); _qrTokenFuture = _generateQrToken();
} }
_listenToMqtt();
} else {
_mqttSubscription?.cancel();
}
});
}
void _listenToMqtt() async {
final mqttService = context.read<MqttService>();
const storage = FlutterSecureStorage();
final userID = await storage.read(key: 'userID');
final discountId = widget.offer.id;
if (userID == null) {
debugPrint("MQTT Listener: UserID not found, cannot subscribe.");
return;
}
final topic = 'user-order/$userID/$discountId';
mqttService.subscribe(topic);
debugPrint("✅ Subscribed to MQTT topic: $topic");
_mqttSubscription = mqttService.messages.listen((message) {
debugPrint("✅ MQTT Message received on reserved card: $message");
final receivedDiscountId = message['Discount'];
if (receivedDiscountId == discountId) {
if (mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => CommentPage(discountId: discountId),
),
);
});
}
}
}); });
} }
@ -84,6 +131,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
@override @override
void dispose() { void dispose() {
_timer?.cancel(); _timer?.cancel();
_mqttSubscription?.cancel();
super.dispose(); super.dispose();
} }
@ -104,13 +152,15 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: _buildOfferPrimaryDetails(), child: _buildOfferPrimaryDetails(),
), ),
_buildActionsRow(), _buildActionsRow(isExpired),
_buildExpansionPanel(), if (!isExpired) _buildExpansionPanel(),
], ],
); );
if (isExpired) { return Stack(
return ColorFiltered( children: [
if (isExpired)
ColorFiltered(
colorFilter: const ColorFilter.matrix(<double>[ colorFilter: const ColorFilter.matrix(<double>[
0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0,
0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0,
@ -118,12 +168,33 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
0, 0, 0, 1, 0, 0, 0, 0, 1, 0,
]), ]),
child: cardContent, child: cardContent,
)
else
cardContent,
if (isExpired)
Positioned(
top: 12,
left: -35,
child: Transform.rotate(
angle: -45 * (3.1415926535 / 180),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 4),
color: Colors.red,
child: const Text(
'منقضی شده',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
),
],
); );
} }
return cardContent;
}
Widget _buildOfferPrimaryDetails() { Widget _buildOfferPrimaryDetails() {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(15, 25, 15, 25), padding: const EdgeInsets.fromLTRB(15, 25, 15, 25),
@ -195,13 +266,13 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
); );
} }
Widget _buildActionsRow() { Widget _buildActionsRow(bool isExpired) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0), padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (_remaining > Duration.zero) if (!isExpired)
Column( Column(
children: [ children: [
Localizations.override( Localizations.override(
@ -220,7 +291,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
fontSize: 20, fontSize: 20,
color: AppColors.countdown, color: AppColors.countdown,
), ),
decoration: const BoxDecoration(color: Colors.white), decoration: const BoxDecoration(color: Colors.transparent),
shouldShowDays: (d) => d.inDays > 0, shouldShowDays: (d) => d.inDays > 0,
shouldShowHours: (d) => d.inHours > 0, shouldShowHours: (d) => d.inHours > 0,
shouldShowMinutes: (d) => d.inSeconds > 0, shouldShowMinutes: (d) => d.inSeconds > 0,
@ -231,15 +302,9 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
], ],
) )
else else
const Text( const SizedBox(height: 0),
'منقضی شده',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
SizedBox(width: 10), SizedBox(width: 10),
if (!isExpired)
TextButton( TextButton(
onPressed: _toggleExpansion, onPressed: _toggleExpansion,
child: Row( child: Row(
@ -324,6 +389,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
Widget _buildExpansionPanel() { Widget _buildExpansionPanel() {
final formatCurrency = NumberFormat.decimalPattern('fa_IR'); final formatCurrency = NumberFormat.decimalPattern('fa_IR');
final isExpired = _remaining <= Duration.zero;
return AnimatedCrossFade( return AnimatedCrossFade(
firstChild: Container(), firstChild: Container(),
@ -339,8 +405,8 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 20, horizontal: 15,
vertical: 9, vertical: 12,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.singleOfferType, color: AppColors.singleOfferType,
@ -351,7 +417,8 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
fontSize: 17, fontSize: 13,
overflow: TextOverflow.ellipsis
), ),
), ),
), ),
@ -365,7 +432,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
Text( Text(
'(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)', '(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 12,
color: AppColors.singleOfferType, color: AppColors.singleOfferType,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
@ -374,7 +441,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
Text( Text(
formatCurrency.format(widget.offer.originalPrice), formatCurrency.format(widget.offer.originalPrice),
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 13,
color: Colors.grey.shade600, color: Colors.grey.shade600,
decoration: TextDecoration.lineThrough, decoration: TextDecoration.lineThrough,
), ),
@ -386,7 +453,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
'${formatCurrency.format(widget.offer.finalPrice)} تومان', '${formatCurrency.format(widget.offer.finalPrice)} تومان',
style: const TextStyle( style: const TextStyle(
color: AppColors.singleOfferType, color: AppColors.singleOfferType,
fontSize: 18, fontSize: 15,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@ -394,6 +461,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
), ),
], ],
), ),
if (!isExpired) ...[
const SizedBox(height: 20), const SizedBox(height: 20),
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
@ -428,6 +496,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
), ),
), ),
], ],
],
), ),
), ),
), ),

View File

@ -31,7 +31,7 @@ class CustomStarRating extends StatelessWidget {
stars.add(_buildStar(Assets.icons.star2.path)); stars.add(_buildStar(Assets.icons.star2.path));
remaining = 0; remaining = 0;
} else { } else {
stars.add(_buildStar(Assets.icons.starHalf.path,)); stars.add(_buildStar(Assets.icons.star2.path,));
} }
} }
return Row(mainAxisSize: MainAxisSize.min, children: stars); return Row(mainAxisSize: MainAxisSize.min, children: stars);

View File

@ -0,0 +1,115 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:proxibuy/services/mqtt_service.dart';
const notificationChannelId = 'proxibuy_foreground_service';
const notificationId = 888;
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
final MqttService mqttService = MqttService();
const storage = FlutterSecureStorage();
if (service is AndroidServiceInstance) {
service.setForegroundNotificationInfo(
title: "ProxiBuy فعال است",
content: "در حال جستجو برای تخفیف های اطراف شما...",
);
service.on('stopService').listen((event) {
service.stopSelf();
});
}
Future<void> sendGpsData() async {
var locationStatus = await Permission.location.status;
if (!locationStatus.isGranted) {
debugPrint("Background Service: Location permission not granted.");
return;
}
try {
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
final token = await storage.read(key: 'accessToken');
final userID = await storage.read(key: 'userID');
if (token != null && userID != null) {
if (!mqttService.isConnected) {
await mqttService.connect(token);
final topic = 'user-proxybuy/$userID';
mqttService.subscribe(topic);
}
if (mqttService.isConnected) {
final payload = {
"userID": userID,
"lat": position.latitude,
"lng": position.longitude
};
mqttService.publish("proxybuy/sendGps", payload);
debugPrint("Background Service: GPS sent successfully.");
}
}
} catch (e) {
debugPrint("❌ Background Service Error: $e");
}
}
mqttService.messages.listen((data) {
service.invoke('update', {'offers': data});
});
service.on('force_refresh').listen((event) async {
debugPrint("✅ Background Service: Received force_refresh event.");
await sendGpsData();
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
debugPrint("✅ Background Service: Sending location via periodic timer...");
await sendGpsData();
});
}
Future<void> initializeService() async {
final service = FlutterBackgroundService();
const AndroidNotificationChannel channel = AndroidNotificationChannel(
notificationChannelId,
'ProxiBuy Background Service',
description: 'This channel is used for location service notifications.',
importance: Importance.low,
);
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onStart,
isForegroundMode: true,
autoStart: true,
notificationChannelId: notificationChannelId,
initialNotificationTitle: 'ProxiBuy فعال است',
initialNotificationContent: 'در حال جستجو برای تخفیف‌های اطراف شما...',
foregroundServiceNotificationId: notificationId,
),
iosConfiguration: IosConfiguration(
autoStart: true,
onForeground: onStart,
),
);
}

View File

@ -0,0 +1,100 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:proxibuy/services/mqtt_service.dart';
const notificationChannelId = 'proxibuy_foreground_service';
const notificationId = 888;
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
DartPluginRegistrant.ensureInitialized();
final MqttService mqttService = MqttService();
const storage = FlutterSecureStorage();
if (service is AndroidServiceInstance) {
service.setForegroundNotificationInfo(
title: "ProxiBuy فعال است",
content: "در حال یافتن تخفیف های اطراف شما...",
);
service.on('setAsForeground').listen((event) {
service.setAsForegroundService();
});
service.on('setAsBackground').listen((event) {
service.setAsBackgroundService();
});
}
service.on('stopService').listen((event) {
service.stopSelf();
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
debugPrint("✅ Background Service is running and sending location...");
var locationStatus = await Permission.location.status;
var locationAlwaysStatus = await Permission.locationAlways.status;
if (!locationStatus.isGranted || !locationAlwaysStatus.isGranted) {
debugPrint("Background Service: Permissions not granted. Task skipped.");
return;
}
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
final token = await storage.read(key: 'accessToken');
final userID = await storage.read(key: 'userID');
if (token != null && token.isNotEmpty && userID != null) {
if (!mqttService.isConnected) {
await mqttService.connect(token);
}
if (mqttService.isConnected) {
final payload = {"userID": userID, "lat": position.latitude, "lng": position.longitude};
mqttService.publish("proxybuy/sendGps", payload);
debugPrint("Background Service: GPS sent successfully.");
}
}
});
}
Future<void> initializeService() async {
final service = FlutterBackgroundService();
const AndroidNotificationChannel channel = AndroidNotificationChannel(
notificationChannelId,
'ProxiBuy Background Service',
description: 'This channel is used for location service notifications.',
importance: Importance.low,
);
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onStart,
isForegroundMode: true,
autoStart: true,
notificationChannelId: notificationChannelId,
initialNotificationTitle: 'ProxiBuy فعال است',
initialNotificationContent: 'در حال جستجو برای تخفیف‌های اطراف شما...',
foregroundServiceNotificationId: notificationId,
),
iosConfiguration: IosConfiguration(
autoStart: true,
onForeground: onStart,
),
);
}

View File

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_client.dart';
@ -10,18 +9,21 @@ class MqttService {
MqttServerClient? client; MqttServerClient? client;
final String server = '5.75.197.180'; final String server = '5.75.197.180';
final int port = 1883; final int port = 1883;
final StreamController<Map<String, dynamic>> _messageStreamController =
StreamController.broadcast();
final StreamController<Map<String, dynamic>> _messageStreamController = StreamController.broadcast();
Stream<Map<String, dynamic>> get messages => _messageStreamController.stream; Stream<Map<String, dynamic>> get messages => _messageStreamController.stream;
bool get isConnected { Completer<Map<String, dynamic>>? _firstMessageCompleter;
return client?.connectionStatus?.state == MqttConnectionState.connected;
bool get isConnected => client?.connectionStatus?.state == MqttConnectionState.connected;
Future<Map<String, dynamic>> awaitFirstMessage() {
_firstMessageCompleter = Completer<Map<String, dynamic>>();
return _firstMessageCompleter!.future;
} }
Future<void> connect(String token) async { Future<void> connect(String token) async {
final String clientId = final String clientId = 'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0');
'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0');
final String username = 'ignored'; final String username = 'ignored';
final String password = token; final String password = token;
@ -31,10 +33,6 @@ class MqttService {
client!.autoReconnect = true; client!.autoReconnect = true;
client!.setProtocolV311(); client!.setProtocolV311();
debugPrint('--- [MQTT] Attempting to connect...');
debugPrint('--- [MQTT] Server: $server:$port');
debugPrint('--- [MQTT] ClientID: $clientId');
final connMessage = MqttConnectMessage() final connMessage = MqttConnectMessage()
.withClientIdentifier(clientId) .withClientIdentifier(clientId)
.startClean() .startClean()
@ -46,86 +44,57 @@ class MqttService {
debugPrint('✅ [MQTT] Connected successfully.'); debugPrint('✅ [MQTT] Connected successfully.');
client!.updates!.listen((List<MqttReceivedMessage<MqttMessage>> c) { client!.updates!.listen((List<MqttReceivedMessage<MqttMessage>> c) {
final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage; final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage;
final String payload = MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
final String payload =
MqttPublishPayload.bytesToStringAsString(recMess.payload.message);
debugPrint('<<<<< [MQTT] Received Data <<<<<');
debugPrint('<<<<< [MQTT] Topic: ${c[0].topic}');
debugPrint('<<<<< [MQTT] Payload as String: $payload');
debugPrint('<<<<< ======================== <<<<<');
try { try {
final Map<String, dynamic> jsonPayload = json.decode(payload); final Map<String, dynamic> jsonPayload = json.decode(payload);
if (!_messageStreamController.isClosed) {
_messageStreamController.add(jsonPayload); _messageStreamController.add(jsonPayload);
}
if (_firstMessageCompleter != null && !_firstMessageCompleter!.isCompleted) {
_firstMessageCompleter!.complete(jsonPayload);
}
} catch (e) { } catch (e) {
debugPrint("❌ [MQTT] Error decoding received JSON: $e"); debugPrint("❌ [MQTT] Error decoding JSON: $e");
if (_firstMessageCompleter != null && !_firstMessageCompleter!.isCompleted) {
_firstMessageCompleter!.completeError(e);
}
} }
}); });
}; };
client!.onDisconnected = () { client!.onDisconnected = () => debugPrint('❌ [MQTT] Disconnected.');
debugPrint('❌ [MQTT] Disconnected.'); client!.onSubscribed = (String topic) => debugPrint('✅ [MQTT] Subscribed to topic: $topic');
};
client!.onAutoReconnect = () {
debugPrint('↪️ [MQTT] Auto-reconnecting...');
};
client!.onAutoReconnected = () {
debugPrint('✅ [MQTT] Auto-reconnected successfully.');
};
client!.onSubscribed = (String topic) {
debugPrint('✅ [MQTT] Subscribed to topic: $topic');
};
client!.pongCallback = () {
debugPrint('🏓 [MQTT] Ping response received');
};
try { try {
await client!.connect(); await client!.connect();
} on NoConnectionException catch (e) {
debugPrint('❌ [MQTT] Connection failed - No Connection Exception: $e');
client?.disconnect();
} on SocketException catch (e) {
debugPrint('❌ [MQTT] Connection failed - Socket Exception: $e');
client?.disconnect();
} catch (e) { } catch (e) {
debugPrint('❌ [MQTT] Connection failed - General Exception: $e'); debugPrint('❌ [MQTT] Connection failed: $e');
client?.disconnect(); client?.disconnect();
if (_firstMessageCompleter != null && !_firstMessageCompleter!.isCompleted) {
_firstMessageCompleter!.completeError(e);
}
} }
} }
void subscribe(String topic) { void subscribe(String topic) {
if (isConnected) { if (isConnected) {
client?.subscribe(topic, MqttQos.atLeastOnce); client?.subscribe(topic, MqttQos.atLeastOnce);
} else {
debugPrint("⚠️ [MQTT] Cannot subscribe. Client is not connected.");
} }
} }
void publish(String topic, Map<String, dynamic> message) { void publish(String topic, Map<String, dynamic> message) {
if (isConnected) { if (isConnected) {
final builder = MqttClientPayloadBuilder(); final builder = MqttClientPayloadBuilder();
final payloadString = json.encode(message); builder.addString(json.encode(message));
builder.addString(payloadString);
debugPrint('>>>>> [MQTT] Publishing Data >>>>>');
debugPrint('>>>>> [MQTT] Topic: $topic');
debugPrint('>>>>> [MQTT] Payload: $payloadString');
debugPrint('>>>>> ======================= >>>>>');
client?.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!); client?.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
} else {
debugPrint("⚠️ [MQTT] Cannot publish. Client is not connected.");
} }
} }
void dispose() { void dispose() {
debugPrint("--- [MQTT] Disposing MQTT Service.");
_messageStreamController.close();
client?.disconnect(); client?.disconnect();
_messageStreamController.close();
} }
} }

View File

@ -11,7 +11,10 @@ import connectivity_plus
import file_selector_macos import file_selector_macos
import firebase_auth import firebase_auth
import firebase_core import firebase_core
import firebase_crashlytics
import firebase_messaging
import firebase_storage import firebase_storage
import flutter_local_notifications
import flutter_localization import flutter_localization
import flutter_secure_storage_macos import flutter_secure_storage_macos
import geolocator_apple import geolocator_apple
@ -28,7 +31,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
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"))
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))

View File

@ -521,6 +521,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.24.1" version: "2.24.1"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "662ae6443da91bca1fb0be8aeeac026fa2975e8b7ddfca36e4d90ebafa35dde1"
url: "https://pub.dev"
source: hosted
version: "4.3.10"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: "7222a8a40077c79f6b8b3f3439241c9f2b34e9ddfde8381ffc512f7b2e61f7eb"
url: "https://pub.dev"
source: hosted
version: "3.8.10"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc"
url: "https://pub.dev"
source: hosted
version: "15.2.10"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754"
url: "https://pub.dev"
source: hosted
version: "4.6.10"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390"
url: "https://pub.dev"
source: hosted
version: "3.10.10"
firebase_storage: firebase_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -566,6 +606,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.2" version: "4.5.2"
flutter_background_service:
dependency: "direct main"
description:
name: flutter_background_service
sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
flutter_background_service_android:
dependency: "direct main"
description:
name: flutter_background_service_android
sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87
url: "https://pub.dev"
source: hosted
version: "6.3.1"
flutter_background_service_ios:
dependency: transitive
description:
name: flutter_background_service_ios
sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
flutter_background_service_platform_interface:
dependency: transitive
description:
name: flutter_background_service_platform_interface
sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41
url: "https://pub.dev"
source: hosted
version: "5.1.2"
flutter_bloc: flutter_bloc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -614,6 +686,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "20ca0a9c82ce0c855ac62a2e580ab867f3fbea82680a90647f7953832d0850ae"
url: "https://pub.dev"
source: hosted
version: "19.4.0"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
url: "https://pub.dev"
source: hosted
version: "9.1.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
url: "https://pub.dev"
source: hosted
version: "1.0.2"
flutter_localization: flutter_localization:
dependency: "direct main" dependency: "direct main"
description: description:
@ -635,6 +739,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.28" version: "2.0.28"
flutter_rating_bar:
dependency: "direct main"
description:
name: flutter_rating_bar
sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93
url: "https://pub.dev"
source: hosted
version: "4.0.1"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1121,10 +1233,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f" sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.0.0+1" version: "12.0.1"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
@ -1466,6 +1578,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.1.5"
timezone:
dependency: transitive
description:
name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
version: "0.10.1"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -1642,6 +1762,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" version: "5.13.0"
workmanager:
dependency: "direct main"
description:
name: workmanager
sha256: "746a50c535af15b6dc225abbd9b52ab272bcd292c535a104c54b5bc02609c38a"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@ -42,7 +42,7 @@ dependencies:
flutter_gen: ^5.10.0 flutter_gen: ^5.10.0
country_picker: ^2.0.27 country_picker: ^2.0.27
geolocator: ^14.0.1 geolocator: ^14.0.1
permission_handler: ^12.0.0+1 permission_handler: ^12.0.1
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
collection: ^1.19.1 collection: ^1.19.1
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
@ -62,6 +62,13 @@ dependencies:
dart_jsonwebtoken: ^3.2.0 dart_jsonwebtoken: ^3.2.0
audioplayers: ^6.5.0 audioplayers: ^6.5.0
intl: ^0.19.0 intl: ^0.19.0
flutter_background_service: ^5.1.0
flutter_background_service_android: ^6.3.1
flutter_local_notifications: ^19.4.0
workmanager: ^0.7.0
firebase_messaging: ^15.2.10
firebase_crashlytics: ^4.3.10
flutter_rating_bar: ^4.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)