From 608222e8a32b340e2c53b4f2cb1b1de3096345ea Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Mon, 4 Aug 2025 10:35:28 +0330 Subject: [PATCH] add refresh offer_page --- android/app/build.gradle.kts | 2 +- android/app/src/main/AndroidManifest.xml | 1 + lib/presentation/auth/bloc/auth_bloc.dart | 3 +- lib/presentation/auth/bloc/auth_state.dart | 2 + .../pages/notification_preferences_page.dart | 281 +++++++++--------- lib/presentation/pages/offers_page.dart | 79 ++++- lib/services/background_service.dart | 22 +- 7 files changed, 226 insertions(+), 164 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ed0357e..4048b18 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -11,7 +11,7 @@ plugins { android { namespace = "com.example.proxibuy" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { isCoreLibraryDesugaringEnabled = true diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7b2fd80..28af001 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + diff --git a/lib/presentation/auth/bloc/auth_bloc.dart b/lib/presentation/auth/bloc/auth_bloc.dart index ec73fb1..99a9c78 100644 --- a/lib/presentation/auth/bloc/auth_bloc.dart +++ b/lib/presentation/auth/bloc/auth_bloc.dart @@ -13,7 +13,7 @@ class AuthBloc extends Bloc { late final Dio _dio; final _storage = const FlutterSecureStorage(); - AuthBloc() : super(AuthInitial()) { + AuthBloc() : super(AuthUnknown()) { _dio = Dio(); _dio.interceptors.add( LogInterceptor( @@ -44,6 +44,7 @@ class AuthBloc extends Bloc { } } + Future _onSendOTP(SendOTPEvent event, Emitter emit) async { emit(AuthLoading()); try { diff --git a/lib/presentation/auth/bloc/auth_state.dart b/lib/presentation/auth/bloc/auth_state.dart index 5eff7bc..90271b2 100644 --- a/lib/presentation/auth/bloc/auth_state.dart +++ b/lib/presentation/auth/bloc/auth_state.dart @@ -7,6 +7,8 @@ abstract class AuthState extends Equatable { List get props => []; } +class AuthUnknown extends AuthState {} + class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} diff --git a/lib/presentation/pages/notification_preferences_page.dart b/lib/presentation/pages/notification_preferences_page.dart index 210712c..0e27f2c 100644 --- a/lib/presentation/pages/notification_preferences_page.dart +++ b/lib/presentation/pages/notification_preferences_page.dart @@ -1,3 +1,4 @@ + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -130,7 +131,7 @@ class _NotificationPreferencesPageState const SizedBox(width: 8), ], ), - body: BlocListener( listener: (context, state) async { if (state.submissionSuccess) { @@ -157,7 +158,7 @@ class _NotificationPreferencesPageState ); } } else { - if (mounted) Navigator.of(context).pop(); + if (mounted) Navigator.of(context).pop(); } } catch (e) { if (mounted) Navigator.of(context).pop(); @@ -176,151 +177,151 @@ class _NotificationPreferencesPageState ); } }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + builder: (context, state) { + return Stack( children: [ - const Text( - 'دریافت اعلان', - style: TextStyle( - fontFamily: 'Dana', - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - const Divider(), - RichText( - text: TextSpan( - style: TextStyle( - fontFamily: 'Dana', - fontSize: 14, - color: AppColors.hint, - height: 1.5, - ), - children: const [ - TextSpan( - text: - 'ترجیح می‌دی از کدام دسته‌بندی‌ها اعلان تخفیف دریافت کنی؟ ', - style: - TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'دریافت اعلان', + style: TextStyle( + fontFamily: 'Dana', + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), - TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'), + const SizedBox(height: 4), + const Divider(), + RichText( + text: TextSpan( + style: TextStyle( + fontFamily: 'Dana', + fontSize: 14, + color: AppColors.hint, + height: 1.5, + ), + children: const [ + TextSpan( + text: + 'ترجیح می‌دی از کدام دسته‌بندی‌ها اعلان تخفیف دریافت کنی؟ ', + style: + TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + ), + TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'), + ], + ), + ), + const SizedBox(height: 24), + Expanded( + child: Builder( + builder: (context) { + if (state.categories.isEmpty && state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + final double horizontalPadding = 24.0; + final double crossAxisSpacing = 16.0; + final int crossAxisCount = 3; + final screenWidth = MediaQuery.of(context).size.width; + final itemWidth = (screenWidth - + (horizontalPadding * 2) - + (crossAxisSpacing * (crossAxisCount - 1))) / + crossAxisCount; + final itemHeight = itemWidth / 0.9; + + return SingleChildScrollView( + child: Wrap( + spacing: crossAxisSpacing, + runSpacing: 24.0, + alignment: WrapAlignment.center, + children: state.categories.map((category) { + final isSelected = + state.selectedCategoryIds.contains(category.id); + return SizedBox( + width: itemWidth, + height: itemHeight, + child: CategorySelectionCard( + name: category.name, + icon: category.icon, + isSelected: isSelected, + showSelectableIndicator: + state.selectedCategoryIds.isNotEmpty, + onTap: () { + context.read().add( + ToggleCategorySelection(category.id)); + }, + ), + ); + }).toList(), + ), + ); + }, + ), + ), + if (state.selectedCategoryIds.isNotEmpty) + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: state.isLoading + ? null + : () async { + final bloc = + context.read(); + + final selectedCategoryNames = bloc + .state.categories + .where((cat) => bloc.state + .selectedCategoryIds + .contains(cat.id)) + .map((cat) => cat.name) + .toList(); + + final prefs = + await SharedPreferences.getInstance(); + await prefs.setStringList( + 'user_selected_categories', + selectedCategoryNames); + + bloc.add(SubmitPreferences()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.confirm, + foregroundColor: Colors.white, + disabledBackgroundColor: Colors.grey.withOpacity(0.5), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + ), + child: const Text( + 'اعمال', + style: TextStyle( + fontFamily: 'Dana', + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(height: 20), ], ), ), - const SizedBox(height: 24), - Expanded( - child: BlocBuilder( - builder: (context, state) { - if (state.categories.isEmpty && state.isLoading) { - return const Center(child: CircularProgressIndicator()); - } - - final double horizontalPadding = 24.0; - final double crossAxisSpacing = 16.0; - final int crossAxisCount = 3; - final screenWidth = MediaQuery.of(context).size.width; - final itemWidth = (screenWidth - - (horizontalPadding * 2) - - (crossAxisSpacing * (crossAxisCount - 1))) / - crossAxisCount; - final itemHeight = itemWidth / 0.9; - - return SingleChildScrollView( - child: Wrap( - spacing: crossAxisSpacing, - runSpacing: 24.0, - alignment: WrapAlignment.center, - children: state.categories.map((category) { - final isSelected = - state.selectedCategoryIds.contains(category.id); - return SizedBox( - width: itemWidth, - height: itemHeight, - child: CategorySelectionCard( - name: category.name, - icon: category.icon, - isSelected: isSelected, - showSelectableIndicator: - state.selectedCategoryIds.isNotEmpty, - onTap: () { - context.read().add( - ToggleCategorySelection(category.id)); - }, - ), - ); - }).toList(), - ), - ); - }, + if (state.isLoading) + Container( + color: Colors.black.withOpacity(0.4), + child: const Center( + child: CircularProgressIndicator( + color: Colors.white, + ), + ), ), - ), - BlocBuilder( - builder: (context, state) { - final areCategoriesSelected = - state.selectedCategoryIds.isNotEmpty; - - if (areCategoriesSelected) { - return SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: !state.isLoading - ? () async { - final bloc = - context.read(); - - final selectedCategoryNames = bloc - .state.categories - .where((cat) => bloc.state - .selectedCategoryIds - .contains(cat.id)) - .map((cat) => cat.name) - .toList(); - - final prefs = - await SharedPreferences.getInstance(); - await prefs.setStringList( - 'user_selected_categories', - selectedCategoryNames); - - bloc.add(SubmitPreferences()); - } - : null, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.confirm, - foregroundColor: Colors.white, - disabledBackgroundColor: Colors.grey, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - ), - child: state.isLoading - ? const CircularProgressIndicator( - color: Colors.white) - : const Text( - 'اعمال', - style: TextStyle( - fontFamily: 'Dana', - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } else { - return const SizedBox.shrink(); - } - }, - ), - const SizedBox(height: 20), ], - ), - ), + ); + }, ), ); } diff --git a/lib/presentation/pages/offers_page.dart b/lib/presentation/pages/offers_page.dart index 6f6e36b..0ba60b7 100644 --- a/lib/presentation/pages/offers_page.dart +++ b/lib/presentation/pages/offers_page.dart @@ -1,4 +1,3 @@ -// lib/presentation/pages/offers_page.dart import 'dart:async'; import 'package:collection/collection.dart'; @@ -205,6 +204,39 @@ class _OffersPageState extends State { } } + Future _handleRefresh() { + final completer = Completer(); + 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?> 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() { return Padding( padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0), @@ -220,7 +252,7 @@ class _OffersPageState extends State { ), TextButton( onPressed: () async { - await Navigator.of(context).push( + final result = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => const NotificationPreferencesPage( loadFavoritesOnStart: true, @@ -234,7 +266,11 @@ class _OffersPageState extends State { .read() .add(ResetSubmissionStatus()); - _loadPreferences(); + await _loadPreferences(); + + if (result == true) { + _handleRefresh(); + } }, child: Row( children: [ @@ -362,16 +398,21 @@ class _OffersPageState extends State { const SizedBox(width: 8), ], ), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildFavoriteCategoriesSection(), - OffersView( - isGpsEnabled: _isGpsEnabled, - isConnectedToInternet: _isConnectedToInternet, - ), - ], + body: RefreshIndicator( + onRefresh: _handleRefresh, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildFavoriteCategoriesSection(), + OffersView( + isGpsEnabled: _isGpsEnabled, + isConnectedToInternet: _isConnectedToInternet, + selectedCategories: _selectedCategories, + ), + ], + ), ), ), ), @@ -382,11 +423,13 @@ class _OffersPageState extends State { class OffersView extends StatelessWidget { final bool isGpsEnabled; final bool isConnectedToInternet; + final List selectedCategories; const OffersView({ super.key, required this.isGpsEnabled, required this.isConnectedToInternet, + required this.selectedCategories, }); @override @@ -418,7 +461,13 @@ class OffersView extends StatelessWidget { } 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( height: 300, child: Center( @@ -434,7 +483,7 @@ class OffersView extends StatelessWidget { } final groupedOffers = groupBy( - state.offers, + filteredOffers, (OfferModel offer) => offer.category, ); final categories = groupedOffers.keys.toList(); diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index d71abf6..0ea7f04 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -30,13 +30,7 @@ void onStart(ServiceInstance service) async { }); } - mqttService.messages.listen((data) { - service.invoke('update', {'offers': data}); - }); - - Timer.periodic(const Duration(seconds: 30), (timer) async { - debugPrint("✅ Background Service: Sending location..."); - + Future sendGpsData() async { var locationStatus = await Permission.location.status; if (!locationStatus.isGranted) { debugPrint("Background Service: Location permission not granted."); @@ -69,6 +63,20 @@ void onStart(ServiceInstance service) async { } 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(); }); }