From 690813829deae09b8222f457401169fcd81f6a84 Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Tue, 22 Jul 2025 15:06:25 +0330 Subject: [PATCH] auth api --- android/app/build.gradle.kts | 5 +- android/app/google-services.json | 48 +++ android/app/src/main/AndroidManifest.xml | 1 + android/settings.gradle.kts | 3 + firebase.json | 1 + lib/core/config/api_config.dart | 7 + .../models/datasources/offer_data_source.dart | 12 +- lib/data/repositories/offer_repository.dart | 1 - lib/domain/entities/category_entity.dart | 2 +- lib/domain/entities/onboarding_entity.dart | 1 - .../add_photo/cubit/add_photo_cubit.dart | 1 - lib/firebase_options.dart | 62 ++++ lib/main.dart | 227 +++++++------ lib/presentation/auth/bloc/auth_bloc.dart | 139 ++++++-- lib/presentation/auth/bloc/auth_event.dart | 20 +- lib/presentation/auth/bloc/auth_state.dart | 25 +- .../bloc/notification_preferences_bloc.dart | 65 +++- .../bloc/notification_preferences_event.dart | 6 +- .../bloc/notification_preferences_state.dart | 26 +- lib/presentation/offer/bloc/offer_event.dart | 1 - .../bloc/widgets/category_offers_row.dart | 2 +- .../offer/bloc/widgets/offer_card.dart | 6 +- lib/presentation/pages/add_photo_screen.dart | 4 - lib/presentation/pages/login_page.dart | 80 +++-- .../pages/notification_preferences_page.dart | 319 ++++++++---------- lib/presentation/pages/offers_page.dart | 14 +- lib/presentation/pages/otp_page.dart | 101 ++++-- lib/presentation/pages/splash_screen.dart | 53 +++ lib/presentation/pages/user_info_page.dart | 100 +++--- .../bloc/product_detail_event.dart | 1 - lib/presentation/utils/otp_timer_helper.dart | 1 - .../widgets/category_selection_card.dart | 13 +- .../widgets/flutter_staggered_grid_view.dart | 4 +- lib/presentation/widgets/gps_dialog.dart | 38 +-- .../widgets/onboarding_indicator_painter.dart | 15 +- .../widgets/reserved_list_item_card.dart | 8 +- lib/services/mqtt_service.dart | 60 ++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 10 + pubspec.lock | 180 +++++++++- pubspec.yaml | 6 + .../flutter/generated_plugin_registrant.cc | 15 + windows/flutter/generated_plugins.cmake | 5 + 44 files changed, 1176 insertions(+), 517 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 firebase.json create mode 100644 lib/core/config/api_config.dart create mode 100644 lib/firebase_options.dart create mode 100644 lib/presentation/pages/splash_screen.dart create mode 100644 lib/services/mqtt_service.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7753291..eaf1789 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,5 +1,8 @@ plugins { id("com.android.application") + // START: FlutterFire Configuration + id("com.google.gms.google-services") + // END: FlutterFire Configuration id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") @@ -24,7 +27,7 @@ android { 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 = flutter.minSdkVersion + minSdk = 23 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..9f23e46 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "800272350428", + "project_id": "proxibuy-3b5e0", + "storage_bucket": "proxibuy-3b5e0.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:800272350428:android:d6af1e013bae09d9c78819", + "android_client_info": { + "package_name": "com.example.business_panel" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCMGweIbZBsFNXabKRJJJcVLPwcmqhuSwg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:800272350428:android:6cbc063052753bc9c78819", + "android_client_info": { + "package_name": "com.example.proxibuy" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCMGweIbZBsFNXabKRJJJcVLPwcmqhuSwg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 987642b..01490dd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + > getNearbyOffers(); - Future getOfferById(String id); // <<<<<<< جدید + Future getOfferById(String id); } class MockOfferDataSource implements OfferDataSource { @@ -71,7 +71,7 @@ class MockOfferDataSource implements OfferDataSource { name: "رفیق‌بازی", description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", ), - comments: [ // <-- بخش نظرات اضافه شد + comments: [ CommentModel( id: 'c1', userName: 'سارا رضایی', @@ -154,7 +154,7 @@ class MockOfferDataSource implements OfferDataSource { name: "رفیق‌بازی", description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", ), - comments: [ // <-- بخش نظرات اضافه شد + comments: [ CommentModel( id: 'c1', userName: 'سارا رضایی', @@ -218,7 +218,7 @@ class MockOfferDataSource implements OfferDataSource { Shift(openAt: '۵ عصر', closeAt: '۱۱ شب'), ], ), - WorkingHours(day: 'جمعه', shifts: []), // تعطیل + WorkingHours(day: 'جمعه', shifts: []), ], discountType: 'رفیق‌بازی', isOpen: true, @@ -237,7 +237,7 @@ class MockOfferDataSource implements OfferDataSource { name: "رفیق‌بازی", description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", ), - comments: [ // <-- بخش نظرات اضافه شد + comments: [ CommentModel( id: 'c1', userName: 'سارا رضایی', @@ -320,7 +320,7 @@ class MockOfferDataSource implements OfferDataSource { name: "رفیق‌بازی", description: "یه مهمونی دو نفره، یه تخفیف دوتایی؛ پس رفیق‌تو بیار و تخفیف‌تو ببر. دوستای بیشتر، تخفیف بیشتر.", ), - comments: [ // <-- بخش نظرات اضافه شد + comments: [ CommentModel( id: 'c1', userName: 'سارا رضایی', diff --git a/lib/data/repositories/offer_repository.dart b/lib/data/repositories/offer_repository.dart index 165b18f..7b2557d 100644 --- a/lib/data/repositories/offer_repository.dart +++ b/lib/data/repositories/offer_repository.dart @@ -21,7 +21,6 @@ class OfferRepository { return filteredOffers; } Future fetchOfferById(String id) async { - // در آینده این متد می‌تواند یک درخواست API برای گرفتن اطلاعات یک محصول خاص ارسال کند return _offerDataSource.getOfferById(id); } } \ No newline at end of file diff --git a/lib/domain/entities/category_entity.dart b/lib/domain/entities/category_entity.dart index 3f3a050..cca25ac 100644 --- a/lib/domain/entities/category_entity.dart +++ b/lib/domain/entities/category_entity.dart @@ -1,7 +1,7 @@ import 'package:proxibuy/core/gen/assets.gen.dart'; class CategoryEntity { - final int id; + final String id; final String name; final SvgGenImage icon; diff --git a/lib/domain/entities/onboarding_entity.dart b/lib/domain/entities/onboarding_entity.dart index 8e50291..8f44575 100644 --- a/lib/domain/entities/onboarding_entity.dart +++ b/lib/domain/entities/onboarding_entity.dart @@ -1,4 +1,3 @@ -// lib/domain/entities/onboarding_entity.dart class OnboardingEntity { final String imagePath; final String title; diff --git a/lib/features/add_photo/cubit/add_photo_cubit.dart b/lib/features/add_photo/cubit/add_photo_cubit.dart index c23f7ec..3773e3f 100644 --- a/lib/features/add_photo/cubit/add_photo_cubit.dart +++ b/lib/features/add_photo/cubit/add_photo_cubit.dart @@ -28,7 +28,6 @@ class AddPhotoCubit extends Cubit { } } - // ✅ متد را طوری تغییر دادیم که منبع عکس را به عنوان ورودی بگیرد Future pickImage(ImageSource source) async { final currentState = state; if (currentState is AddPhotoLoaded) { diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..402b9ac --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,62 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for ios - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCMGweIbZBsFNXabKRJJJcVLPwcmqhuSwg', + appId: '1:800272350428:android:6cbc063052753bc9c78819', + messagingSenderId: '800272350428', + projectId: 'proxibuy-3b5e0', + storageBucket: 'proxibuy-3b5e0.firebasestorage.app', + ); +} diff --git a/lib/main.dart b/lib/main.dart index bada8f1..3ca6bde 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -5,17 +6,23 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; import 'package:proxibuy/data/repositories/offer_repository.dart'; +import 'package:proxibuy/firebase_options.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; -import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; +import 'package:proxibuy/presentation/pages/offers_page.dart'; +import 'package:proxibuy/presentation/pages/otp_page.dart'; +import 'package:proxibuy/presentation/pages/user_info_page.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; +// import 'package:proxibuy/services/mqtt_service.dart'; import 'core/config/app_colors.dart'; import 'presentation/pages/onboarding_page.dart'; +import 'package:proxibuy/presentation/pages/splash_screen.dart'; // <--- ایمپورت جدید -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); Animate.restartOnHotReload = true; - runApp(const MyApp()); } @@ -24,118 +31,136 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiRepositoryProvider( + return MultiBlocProvider( providers: [ + BlocProvider( + create: (context) => AuthBloc()..add(CheckAuthStatusEvent()), + ), RepositoryProvider( - create: - (context) => - OfferRepository(offerDataSource: MockOfferDataSource()), + create: (context) => + OfferRepository(offerDataSource: MockOfferDataSource()), + ), + BlocProvider( + create: (context) => ReservationCubit(), + ), + BlocProvider( + create: (context) => OffersBloc( + offerRepository: context.read(), + ), + ), + BlocProvider( + create: (context) => NotificationPreferencesBloc(), ), ], - child: MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => AuthBloc()), - BlocProvider( - create: - (context) => - NotificationPreferencesBloc()..add(LoadCategories()), - ), - BlocProvider( - create: - (context) => OffersBloc( - offerRepository: context.read(), - ), - ), - BlocProvider( - create: (context) => ReservationCubit(), - ), + child: MaterialApp( + title: 'Proxibuy', + debugShowCheckedModeBanner: false, + home: const SplashScreen(), // <--- استفاده از صفحه اسپلش + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, ], - child: MaterialApp( - title: 'Proxibuy', - debugShowCheckedModeBanner: false, - - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [Locale('fa')], - locale: const Locale('fa'), - - theme: ThemeData( - fontFamily: 'Dana', - scaffoldBackgroundColor: Colors.white, - colorScheme: ColorScheme.fromSeed( - seedColor: AppColors.primary, - primary: AppColors.primary, - surface: Colors.white, + supportedLocales: const [Locale('fa')], + locale: const Locale('fa'), + theme: ThemeData( + fontFamily: 'Dana', + scaffoldBackgroundColor: Colors.white, + colorScheme: ColorScheme.fromSeed( + seedColor: AppColors.primary, + primary: AppColors.primary, + surface: Colors.white, + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: Colors.white, + floatingLabelBehavior: FloatingLabelBehavior.always, + contentPadding: const EdgeInsets.symmetric( + vertical: 18, + horizontal: 20, ), - appBarTheme: const AppBarTheme( - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - elevation: 0, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.border), ), - inputDecorationTheme: InputDecorationTheme( - filled: true, - fillColor: Colors.white, - floatingLabelBehavior: FloatingLabelBehavior.always, - contentPadding: const EdgeInsets.symmetric( - vertical: 18, - horizontal: 20, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.border), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: AppColors.border), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: AppColors.primary, - width: 2, - ), - ), - labelStyle: const TextStyle(color: Colors.black), - // ignore: deprecated_member_use - hintStyle: TextStyle(color: Colors.black.withOpacity(0.8)), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.border), ), - outlinedButtonTheme: OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - foregroundColor: Colors.black, // رنگ متن دکمه Outlined - padding: const EdgeInsets.symmetric(vertical: 16), - side: const BorderSide(color: Colors.grey), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - textStyle: const TextStyle( - fontFamily: 'Dana', - fontSize: 16, - color: Colors.black, - ), - ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: AppColors.primary, width: 2), ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.button, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - textStyle: const TextStyle( - fontFamily: 'Dana', - fontSize: 16, - fontWeight: FontWeight.bold, - ), + labelStyle: const TextStyle(color: Colors.black), + // ignore: deprecated_member_use + hintStyle: TextStyle(color: Colors.black.withOpacity(0.8)), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(vertical: 16), + side: const BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + textStyle: const TextStyle( + fontFamily: 'Dana', + fontSize: 16, + color: Colors.black, + ), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.button, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + textStyle: const TextStyle( + fontFamily: 'Dana', + fontSize: 16, + fontWeight: FontWeight.bold, ), ), ), - home: const OnboardingPage(), ), ), ); } } + +class AppRouter extends StatelessWidget { + const AppRouter({super.key}); + + @override + Widget build(BuildContext context) { + final authState = context.select((AuthBloc bloc) => bloc.state); + + if (authState is AuthCodeSentSuccess) { + return OtpPage( + phoneNumber: "+${authState.countryCode}${authState.phone}", + phone: authState.phone, + countryCode: authState.countryCode, + ); + } + + if (authState is AuthLoading) { + final currentState = context.read().state; + if (currentState is! AuthCodeSentSuccess) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + } + + if (authState is AuthSuccess) { + return const OffersPage(); + } + + if (authState is AuthNeedsInfo) { + return const UserInfoPage(); + } + + return const OnboardingPage(); + } +} diff --git a/lib/presentation/auth/bloc/auth_bloc.dart b/lib/presentation/auth/bloc/auth_bloc.dart index 7518fd0..c74894e 100644 --- a/lib/presentation/auth/bloc/auth_bloc.dart +++ b/lib/presentation/auth/bloc/auth_bloc.dart @@ -1,43 +1,116 @@ -// ignore: depend_on_referenced_packages import 'package:bloc/bloc.dart'; -// ignore: depend_on_referenced_packages +import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:meta/meta.dart'; -import 'dart:async'; +import 'package:proxibuy/core/config/api_config.dart'; part 'auth_event.dart'; part 'auth_state.dart'; class AuthBloc extends Bloc { + late final Dio _dio; + final _storage = const FlutterSecureStorage(); + AuthBloc() : super(AuthInitial()) { - on((event, emit) async { - emit(AuthLoading()); - await Future.delayed(const Duration(seconds: 1)); - if (event.phoneNumber.isNotEmpty) { - emit(AuthCodeSentSuccess()); - } else { - emit(AuthFailure('شماره موبایل معتبر نیست.')); - } - }); + _dio = Dio(); + _dio.interceptors.add( + LogInterceptor( + requestHeader: true, + requestBody: true, + responseBody: true, + error: true, + ), + ); - on((event, emit) async { - emit(AuthLoading()); - await Future.delayed(const Duration(seconds: 1)); - if (event.otp == '12345') { - emit(AuthVerified()); - } else { - emit(AuthFailure('کد تایید صحیح نمی‌باشد.')); - } - }); - - on((event, emit) async { - emit(AuthLoading()); - await Future.delayed(const Duration(milliseconds: 500)); - - if (event.name.trim().isEmpty) { - emit(AuthFailure('لطفاً نام خود را وارد کنید.')); - } else { - emit(UserInfoSaved()); - } - }); + on(_onCheckAuthStatus); + on(_onSendOTP); + on(_onVerifyOTP); + on(_onUpdateUserInfo); + on(_onLogout); } -} + + Future _onCheckAuthStatus( + CheckAuthStatusEvent event, Emitter emit) async { + final token = await _storage.read(key: 'accessToken'); + if (token != null && token.isNotEmpty) { + emit(AuthSuccess()); + } else { + emit(AuthInitial()); + } + } + + Future _onSendOTP(SendOTPEvent event, Emitter emit) async { + emit(AuthLoading()); + try { + final response = await _dio.post( + ApiConfig.baseUrl + ApiConfig.sendCode, + data: {'Phone': event.phoneNumber, 'Code': event.countryCode}, + ); + if (response.statusCode == 200) { + emit(AuthCodeSentSuccess( + phone: event.phoneNumber, + countryCode: event.countryCode, + )); + } else { + emit(AuthFailure(response.data['message'] ?? 'خطایی رخ داد')); + } + } on DioException catch (e) { + emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); + } + } + + Future _onVerifyOTP(VerifyOTPEvent event, Emitter emit) async { + emit(AuthLoading()); + try { + final response = await _dio.post( + ApiConfig.baseUrl + ApiConfig.verifyCode, + data: { + 'Phone': event.phoneNumber, + 'Code': event.countryCode, + 'OTP': event.otp, + }, + ); + if (response.statusCode == 200) { + final accessToken = response.data['data']['accessToken']; + final refreshToken = response.data['data']['refreshToken']; + await _storage.write(key: 'accessToken', value: accessToken); + await _storage.write(key: 'refreshToken', value: refreshToken); + emit(AuthNeedsInfo()); + } else { + emit(AuthFailure(response.data['message'] ?? 'کد صحیح نیست')); + } + } on DioException catch (e) { + emit(AuthFailure(e.response?.data['message'] ?? 'خطایی در سرور رخ داد')); + } + } + + Future _onUpdateUserInfo( + UpdateUserInfoEvent event, Emitter emit) async { + emit(AuthLoading()); + try { + final token = await _storage.read(key: 'accessToken'); + if (token == null) { + emit(const AuthFailure("شما وارد نشده‌اید.")); + return; + } + final response = await _dio.post( + ApiConfig.baseUrl + ApiConfig.updateUser, + data: {'Name': event.name, 'Gender': event.gender}, + options: Options(headers: {'Authorization': 'Bearer $token'}), + ); + if (response.statusCode == 200) { + emit(AuthSuccess()); + } else { + emit(AuthFailure(response.data['message'] ?? 'خطا در ثبت اطلاعات')); + } + } on DioException catch (e) { + emit(AuthFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); + } + } + + Future _onLogout(LogoutEvent event, Emitter emit) async { + await _storage.deleteAll(); + emit(AuthInitial()); + } +} \ No newline at end of file diff --git a/lib/presentation/auth/bloc/auth_event.dart b/lib/presentation/auth/bloc/auth_event.dart index ae15595..e7ca537 100644 --- a/lib/presentation/auth/bloc/auth_event.dart +++ b/lib/presentation/auth/bloc/auth_event.dart @@ -1,4 +1,3 @@ - part of 'auth_bloc.dart'; @immutable @@ -6,19 +5,24 @@ abstract class AuthEvent {} class SendOTPEvent extends AuthEvent { final String phoneNumber; - - SendOTPEvent({required this.phoneNumber}); + final String countryCode; + SendOTPEvent({required this.phoneNumber, required this.countryCode}); } class VerifyOTPEvent extends AuthEvent { + final String phoneNumber; + final String countryCode; final String otp; - - VerifyOTPEvent({required this.otp}); + VerifyOTPEvent({required this.phoneNumber, required this.countryCode, required this.otp}); } -class SaveUserInfoEvent extends AuthEvent { +class CheckAuthStatusEvent extends AuthEvent {} + +class LogoutEvent extends AuthEvent {} + +class UpdateUserInfoEvent extends AuthEvent { final String name; final String gender; - SaveUserInfoEvent({required this.name, required this.gender}); -} + UpdateUserInfoEvent({required this.name, required this.gender}); +} \ No newline at end of file diff --git a/lib/presentation/auth/bloc/auth_state.dart b/lib/presentation/auth/bloc/auth_state.dart index 497c253..5eff7bc 100644 --- a/lib/presentation/auth/bloc/auth_state.dart +++ b/lib/presentation/auth/bloc/auth_state.dart @@ -1,21 +1,34 @@ - part of 'auth_bloc.dart'; @immutable -abstract class AuthState {} +abstract class AuthState extends Equatable { + const AuthState(); + @override + List get props => []; +} class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} -class AuthCodeSentSuccess extends AuthState {} +class AuthCodeSentSuccess extends AuthState { + final String phone; + final String countryCode; -class AuthVerified extends AuthState {} + const AuthCodeSentSuccess({required this.phone, required this.countryCode}); -class UserInfoSaved extends AuthState {} + @override + List get props => [phone, countryCode]; +} + +class AuthNeedsInfo extends AuthState {} + +class AuthSuccess extends AuthState {} class AuthFailure extends AuthState { final String message; + const AuthFailure(this.message); - AuthFailure(this.message); + @override + List get props => [message]; } \ No newline at end of file diff --git a/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart b/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart index 29ed792..dd2e96f 100644 --- a/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart +++ b/lib/presentation/notification_preferences/bloc/notification_preferences_bloc.dart @@ -1,4 +1,7 @@ +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/core/gen/assets.gen.dart'; import 'package:proxibuy/domain/entities/category_entity.dart'; import 'notification_preferences_event.dart'; @@ -6,31 +9,37 @@ import 'notification_preferences_state.dart'; class NotificationPreferencesBloc extends Bloc { + final Dio _dio = Dio(); + final FlutterSecureStorage _storage = const FlutterSecureStorage(); + NotificationPreferencesBloc() : super(const NotificationPreferencesState()) { on(_onLoadCategories); on(_onToggleCategorySelection); + on(_onSubmitPreferences); + + add(LoadCategories()); } void _onLoadCategories( LoadCategories event, Emitter emit) { final categories = [ - CategoryEntity(id: 1, name: 'تریا', icon: Assets.icons.teria), - CategoryEntity(id: 2, name: 'پوشاک', icon: Assets.icons.pooshak), - CategoryEntity(id: 3, name: 'فست‌فود', icon: Assets.icons.fastfood), - CategoryEntity(id: 4, name: 'کافی‌شاپ', icon: Assets.icons.coffeeshop), - CategoryEntity(id: 5, name: 'رستوران', icon: Assets.icons.resturan), - CategoryEntity(id: 6, name: 'لوازم دیجیتال', icon: Assets.icons.digital), - CategoryEntity(id: 7, name: 'کیف‌وکفش', icon: Assets.icons.kafsh), - CategoryEntity(id: 8, name: 'سینما', icon: Assets.icons.cinama), - CategoryEntity(id: 9, name: 'لوازم آرایشی', icon: Assets.icons.arayesh), - CategoryEntity(id: 10, name: 'طلا و زیورآلات', icon: Assets.icons.tala), + 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: "b5881239-bfd5-4c27-967a-187316a7e0b7", name: 'رستوران', icon: Assets.icons.resturan), + CategoryEntity(id: "6803b940-3e19-48cd-9190-28d9f25421ff", name: 'فست‌فود', icon: Assets.icons.fastfood), + 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: "2f38918c-5566-4aec-a0a9-2c7c48b1e878", name: 'کیف‌وکفش', icon: Assets.icons.kafsh), + 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: "e4517b0c-aacf-4758-94bd-85f45062980f", name: 'طلا و زیورآلات', icon: Assets.icons.tala), ]; emit(state.copyWith(categories: categories)); } - void _onToggleCategorySelection(ToggleCategorySelection event, - Emitter emit) { - final selectedIds = Set.from(state.selectedCategoryIds); + void _onToggleCategorySelection( + ToggleCategorySelection event, Emitter emit) { + final selectedIds = Set.from(state.selectedCategoryIds); if (selectedIds.contains(event.categoryId)) { selectedIds.remove(event.categoryId); } else { @@ -38,4 +47,34 @@ class NotificationPreferencesBloc } emit(state.copyWith(selectedCategoryIds: selectedIds)); } + + Future _onSubmitPreferences( + SubmitPreferences event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null, submissionSuccess: false)); + try { + final token = await _storage.read(key: 'accessToken'); + if (token == null) { + emit(state.copyWith(isLoading: false, errorMessage: "شما وارد نشده‌اید.")); + return; + } + + final response = await _dio.post( + ApiConfig.baseUrl + ApiConfig.updateCategories, + data: {"FCategory": state.selectedCategoryIds.toList()}, + options: Options(headers: {'Authorization': 'Bearer $token'}), + ); + + if (response.statusCode == 200) { + emit(state.copyWith(isLoading: false, submissionSuccess: true)); + } else { + emit(state.copyWith( + isLoading: false, + errorMessage: response.data['message'] ?? 'خطا در ثبت اطلاعات')); + } + } on DioException catch (e) { + emit(state.copyWith( + isLoading: false, + errorMessage: e.response?.data['message'] ?? 'خطا در ارتباط با سرور')); + } + } } \ No newline at end of file diff --git a/lib/presentation/notification_preferences/bloc/notification_preferences_event.dart b/lib/presentation/notification_preferences/bloc/notification_preferences_event.dart index d54197b..1fc014e 100644 --- a/lib/presentation/notification_preferences/bloc/notification_preferences_event.dart +++ b/lib/presentation/notification_preferences/bloc/notification_preferences_event.dart @@ -10,10 +10,12 @@ abstract class NotificationPreferencesEvent extends Equatable { class LoadCategories extends NotificationPreferencesEvent {} class ToggleCategorySelection extends NotificationPreferencesEvent { - final int categoryId; + final String categoryId; const ToggleCategorySelection(this.categoryId); @override List get props => [categoryId]; -} \ No newline at end of file +} + +class SubmitPreferences extends NotificationPreferencesEvent {} \ No newline at end of file diff --git a/lib/presentation/notification_preferences/bloc/notification_preferences_state.dart b/lib/presentation/notification_preferences/bloc/notification_preferences_state.dart index 0e346d6..8806e09 100644 --- a/lib/presentation/notification_preferences/bloc/notification_preferences_state.dart +++ b/lib/presentation/notification_preferences/bloc/notification_preferences_state.dart @@ -1,27 +1,43 @@ - import 'package:equatable/equatable.dart'; import 'package:proxibuy/domain/entities/category_entity.dart'; class NotificationPreferencesState extends Equatable { final List categories; - final Set selectedCategoryIds; + final Set selectedCategoryIds; + final bool isLoading; + final String? errorMessage; + final bool submissionSuccess; const NotificationPreferencesState({ this.categories = const [], this.selectedCategoryIds = const {}, + this.isLoading = false, + this.errorMessage, + this.submissionSuccess = false, }); NotificationPreferencesState copyWith({ List? categories, - Set? selectedCategoryIds, + Set? selectedCategoryIds, + bool? isLoading, + String? errorMessage, + bool? submissionSuccess, }) { return NotificationPreferencesState( categories: categories ?? this.categories, selectedCategoryIds: selectedCategoryIds ?? this.selectedCategoryIds, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage, + submissionSuccess: submissionSuccess ?? this.submissionSuccess, ); } - @override - List get props => [categories, selectedCategoryIds]; + List get props => [ + categories, + selectedCategoryIds, + isLoading, + errorMessage, + submissionSuccess, + ]; } \ No newline at end of file diff --git a/lib/presentation/offer/bloc/offer_event.dart b/lib/presentation/offer/bloc/offer_event.dart index b656e96..f325f20 100644 --- a/lib/presentation/offer/bloc/offer_event.dart +++ b/lib/presentation/offer/bloc/offer_event.dart @@ -9,7 +9,6 @@ abstract class OffersEvent extends Equatable { } class OffersFetchRequested extends OffersEvent { - // ✅ ۱. یک فیلد برای نگهداری دسته‌بندی‌ها اضافه کنید final List selectedCategories; const OffersFetchRequested({required this.selectedCategories}); diff --git a/lib/presentation/offer/bloc/widgets/category_offers_row.dart b/lib/presentation/offer/bloc/widgets/category_offers_row.dart index eb38f0c..b200ce1 100644 --- a/lib/presentation/offer/bloc/widgets/category_offers_row.dart +++ b/lib/presentation/offer/bloc/widgets/category_offers_row.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:proxibuy/data/models/offer_model.dart'; import 'package:proxibuy/presentation/offer/bloc/widgets/offer_card.dart'; -import 'package:proxibuy/presentation/pages/product_detail_page.dart'; // <-- این خط را اضافه کن +import 'package:proxibuy/presentation/pages/product_detail_page.dart'; class CategoryOffersRow extends StatelessWidget { final String categoryTitle; diff --git a/lib/presentation/offer/bloc/widgets/offer_card.dart b/lib/presentation/offer/bloc/widgets/offer_card.dart index 5552257..8e03cc9 100644 --- a/lib/presentation/offer/bloc/widgets/offer_card.dart +++ b/lib/presentation/offer/bloc/widgets/offer_card.dart @@ -118,23 +118,19 @@ class _OfferCardState extends State { ), const SizedBox(height: 10), - // ================== شروع تغییرات ================== Row( children: [ SvgPicture.asset(Assets.icons.location.path), const SizedBox(width: 4), - // ویجت Flexible باعث می‌شود که آدرس، فضای باقی‌مانده را پر کند - // و در صورت طولانی بودن، کوتاه شود. Flexible( child: Text( widget.offer.address, style: textTheme.bodySmall, maxLines: 1, - overflow: TextOverflow.ellipsis, // نمایش سه نقطه در انتهای متن طولانی + overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 4), - // این بخش همیشه به طور کامل نمایش داده می‌شود Text( '(${widget.offer.distanceInMeters.toString()}متر تا تخفیف)', style: textTheme.bodySmall, diff --git a/lib/presentation/pages/add_photo_screen.dart b/lib/presentation/pages/add_photo_screen.dart index 112d82c..4cd5a43 100644 --- a/lib/presentation/pages/add_photo_screen.dart +++ b/lib/presentation/pages/add_photo_screen.dart @@ -23,7 +23,6 @@ class AddPhotoScreen extends StatelessWidget { required this.offer, }); - // ✅ متد جدید برای نمایش منوی انتخاب void _showImageSourceActionSheet(BuildContext context) { showModalBottomSheet( context: context, @@ -147,7 +146,6 @@ class AddPhotoScreen extends StatelessWidget { context.read().isProductReserved(productId); if (isReserved) { - // ✅ به جای فراخوانی مستقیم، منوی انتخاب را نمایش می‌دهیم _showImageSourceActionSheet(context); } else { _showReservationPopup(context); @@ -177,7 +175,6 @@ class AddPhotoScreen extends StatelessWidget { } PreferredSizeWidget _buildCustomAppBar(BuildContext context) { - // ... این متد بدون تغییر باقی می‌ماند return PreferredSize( preferredSize: const Size.fromHeight(70.0), child: Container( @@ -222,7 +219,6 @@ class AddPhotoScreen extends StatelessWidget { } void _showReservationPopup(BuildContext context) { - // ... این متد بدون تغییر باقی می‌ماند showDialog( context: context, barrierDismissible: false, diff --git a/lib/presentation/pages/login_page.dart b/lib/presentation/pages/login_page.dart index ece61eb..abf1a80 100644 --- a/lib/presentation/pages/login_page.dart +++ b/lib/presentation/pages/login_page.dart @@ -1,11 +1,8 @@ -// lib/presentation/pages/login_page.dart - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:country_picker/country_picker.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; import 'package:proxibuy/presentation/pages/otp_page.dart'; -import '../../core/config/app_colors.dart'; import '../../core/gen/assets.gen.dart'; class LoginPage extends StatefulWidget { @@ -18,7 +15,16 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { final TextEditingController _phoneController = TextEditingController(); Country _selectedCountry = Country.parse('IR'); - bool _keepSignedIn = false; + + void _sendOtp() { + context.read().add( + SendOTPEvent( + phoneNumber: _phoneController.text, + countryCode: _selectedCountry.phoneCode, + ), + ); + } + // bool _keepSignedIn = false; @override void dispose() { @@ -92,17 +98,18 @@ class _LoginPageState extends State { ), ), const SizedBox(height: 16), - Row( - children: [ - Checkbox( - value: _keepSignedIn, - onChanged: (value) => - setState(() => _keepSignedIn = value ?? false), - activeColor: AppColors.primary, - ), - Text("مرا به خاطر بسپار", style: textTheme.bodyMedium), - ], - ), + // Row( + // children: [ + // Checkbox( + // value: _keepSignedIn, + // onChanged: + // (value) => + // setState(() => _keepSignedIn = value ?? false), + // activeColor: AppColors.primary, + // ), + // Text("مرا به خاطر بسپار", style: textTheme.bodyMedium), + // ], + // ), const SizedBox(height: 24), BlocConsumer( listener: (context, state) { @@ -115,23 +122,38 @@ class _LoginPageState extends State { ); } if (state is AuthCodeSentSuccess) { - final fullPhoneNumber = "0${_phoneController.text}"; Navigator.push( - context, - MaterialPageRoute( - builder: (_) => - OtpPage(phoneNumber: fullPhoneNumber))); + context, + MaterialPageRoute( + builder: + (_) => OtpPage( + phoneNumber: + "${state.countryCode}${state.phone}", + phone: state.phone, + countryCode: state.countryCode, + ), + ), + ); } }, builder: (context, state) { - if (state is AuthLoading) { - return const Center(child: CircularProgressIndicator()); - } + bool isLoading = state is AuthLoading; + return SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: _sendOtp, - child: const Text("کد یکبار مصرف"), + onPressed: isLoading ? null : _sendOtp, + child: + isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 3, + ), + ) + : const Text("کد یکبار مصرف"), ), ); }, @@ -192,10 +214,4 @@ class _LoginPageState extends State { onSelect: (Country country) => setState(() => _selectedCountry = country), ); } - - void _sendOtp() { - context - .read() - .add(SendOTPEvent(phoneNumber: _phoneController.text)); - } -} \ No newline at end of file +} diff --git a/lib/presentation/pages/notification_preferences_page.dart b/lib/presentation/pages/notification_preferences_page.dart index 978fc50..a9e1842 100644 --- a/lib/presentation/pages/notification_preferences_page.dart +++ b/lib/presentation/pages/notification_preferences_page.dart @@ -5,8 +5,6 @@ import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.dart'; import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_state.dart'; -import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; -import 'package:proxibuy/presentation/offer/bloc/offer_event.dart'; import 'package:proxibuy/presentation/pages/offers_page.dart'; import 'package:proxibuy/presentation/pages/reserved_list_page.dart'; import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; @@ -18,7 +16,10 @@ class NotificationPreferencesPage extends StatelessWidget { static Route route() { return MaterialPageRoute( - builder: (_) => const NotificationPreferencesPage(), + builder: (_) => BlocProvider( + create: (context) => NotificationPreferencesBloc(), + child: const NotificationPreferencesPage(), + ), ); } @@ -96,182 +97,160 @@ class NotificationPreferencesPage extends StatelessWidget { const SizedBox(width: 8), ], ), - body: 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, + body: BlocListener( + listener: (context, state) { + if (state.submissionSuccess) { + if (Navigator.canPop(context)) { + Navigator.of(context).pop(true); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const OffersPage(showDialogsOnLoad: true), + ), + ); + } + } + if (state.errorMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage!), + backgroundColor: Colors.red, ), - ), - const SizedBox(height: 4), - const Divider(), - RichText( - text: TextSpan( + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'دریافت اعلان', style: TextStyle( fontFamily: 'Dana', - fontSize: 14, - color: AppColors.hint, - height: 1.5, + fontSize: 20, + fontWeight: FontWeight.bold, ), - children: const [ - TextSpan( - text: - 'ترجیح می‌دی از کدام دسته‌بندی‌ها اعلان تخفیف دریافت کنی؟ ', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), - ), - TextSpan(text: '(حداقل یک مورد رو انتخاب کن).'), - ], ), - ), - const SizedBox(height: 24), - Expanded( - child: BlocBuilder< - NotificationPreferencesBloc, - NotificationPreferencesState - >( - builder: (context, state) { - if (state.categories.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } - - const double sidePadding = 24.0; - const double crossAxisSpacing = 16.0; - const int crossAxisCount = 3; - const double mainAxisSpacing = 24.0; - const double childAspectRatio = 0.9; - final screenWidth = MediaQuery.of(context).size.width; - - final totalHorizontalPadding = sidePadding * 2; - final totalSpacing = crossAxisSpacing * (crossAxisCount - 1); - final availableWidth = screenWidth - totalHorizontalPadding; - final itemWidth = - (availableWidth - totalSpacing) / crossAxisCount; - final itemHeight = itemWidth / childAspectRatio; - - return SingleChildScrollView( - child: Center( - child: Wrap( - alignment: WrapAlignment.center, - spacing: crossAxisSpacing, - runSpacing: mainAxisSpacing, - children: - state.categories.map((category) { - final isSelected = state.selectedCategoryIds - .contains(category.id); - return SizedBox( - width: 100, - height: itemHeight, - child: Center( - child: CategorySelectionCard( - name: category.name, - icon: category.icon, - isSelected: isSelected, - showSelectableIndicator: - state.selectedCategoryIds.isNotEmpty, - onTap: () { - context - .read() - .add( - ToggleCategorySelection( - category.id, - ), - ); - }, - ), - ), - ); - }).toList(), - ), + 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: 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(), + ), + ); + }, + ), + ), + 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(); + } }, ), - ), - BlocBuilder< - NotificationPreferencesBloc, - NotificationPreferencesState - >( - builder: (context, state) { - final isEnabled = state.selectedCategoryIds.isNotEmpty; - return SizedBox( - width: double.infinity, - child: - isEnabled - ? ElevatedButton( - onPressed: - isEnabled - ? () async { - final selectedCategoryNames = - state.categories - .where( - (cat) => state - .selectedCategoryIds - .contains(cat.id), - ) - .map((cat) => cat.name) - .toList(); - - final prefs = - await SharedPreferences.getInstance(); - await prefs.setStringList( - 'user_selected_categories', - selectedCategoryNames, - ); - - if (context.mounted) { - context.read().add( - OffersFetchRequested( - selectedCategories: - selectedCategoryNames, - ), - ); - } - - if (!context.mounted) return; - - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: - (context) => const OffersPage( - showDialogsOnLoad: true, - ), - ), - ); - } - : 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: const Text( - 'اعمال', - style: TextStyle( - fontFamily: 'Dana', - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ) - : const SizedBox(), - ); - }, - ), - const SizedBox(height: 20), - ], + const SizedBox(height: 20), + ], + ), ), ), ); } -} +} \ No newline at end of file diff --git a/lib/presentation/pages/offers_page.dart b/lib/presentation/pages/offers_page.dart index 203148c..51f7815 100644 --- a/lib/presentation/pages/offers_page.dart +++ b/lib/presentation/pages/offers_page.dart @@ -1,14 +1,12 @@ import 'package:collection/collection.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_svg/svg.dart'; import 'package:geolocator/geolocator.dart'; import 'package:proxibuy/core/config/app_colors.dart'; import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/data/models/offer_model.dart'; -import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; -import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_event.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_event.dart'; import 'package:proxibuy/presentation/offer/bloc/offer_state.dart'; @@ -81,17 +79,11 @@ class _OffersPageState extends State { onPressed: () async { final result = await Navigator.of(context).push( MaterialPageRoute( - builder: - (_) => BlocProvider.value( - value: - context.read() - ..add(LoadCategories()), - child: const NotificationPreferencesPage(), - ), + builder: (_) => const NotificationPreferencesPage(), ), ); - if (result == true) { + if (result == true && mounted) { _loadOffersAndPreferences(); } }, diff --git a/lib/presentation/pages/otp_page.dart b/lib/presentation/pages/otp_page.dart index b8d3340..3d3153f 100644 --- a/lib/presentation/pages/otp_page.dart +++ b/lib/presentation/pages/otp_page.dart @@ -1,15 +1,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:proxibuy/data/models/datasources/offer_data_source.dart'; +import 'package:proxibuy/data/repositories/offer_repository.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; +import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart'; +import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart'; import 'package:proxibuy/presentation/pages/user_info_page.dart'; +import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart'; import '../../core/config/app_colors.dart'; import '../../core/gen/assets.gen.dart'; import '../utils/otp_timer_helper.dart'; class OtpPage extends StatefulWidget { final String phoneNumber; - const OtpPage({super.key, required this.phoneNumber}); + final String phone; + final String countryCode; + + const OtpPage({ + super.key, + required this.phoneNumber, + required this.phone, + required this.countryCode, + }); @override State createState() => _OtpPageState(); @@ -77,19 +90,24 @@ class _OtpPageState extends State { height: 1.5, ), children: [ - const TextSpan(text: 'کد تایید به شماره ',style: TextStyle(fontSize: 15)), + const TextSpan( + text: 'کد تایید به شماره ', + style: TextStyle(fontSize: 15), + ), TextSpan( text: widget.phoneNumber, style: const TextStyle( - fontWeight: - FontWeight.bold, - fontSize: 15 + fontWeight: FontWeight.bold, + fontSize: 15, ), ), - const TextSpan(text: ' ارسال شد.',style: TextStyle(fontSize: 15)), + const TextSpan( + text: ' ارسال شد.', + style: TextStyle(fontSize: 15), + ), ], ), - textDirection: TextDirection.rtl, // جهت متن برای RichText + textDirection: TextDirection.rtl, ), SizedBox(height: 15), Row( @@ -133,22 +151,57 @@ class _OtpPageState extends State { _errorMessage = state.message; }); } - if (state is AuthVerified) { + if (state is AuthNeedsInfo) { + final offerRepository = OfferRepository( + offerDataSource: MockOfferDataSource(), + ); Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => const UserInfoPage()), + MaterialPageRoute( + builder: + (_) => MultiBlocProvider( + providers: [ + BlocProvider.value( + value: context.read(), + ), + BlocProvider( + create: + (_) => OffersBloc( + offerRepository: offerRepository, + ), + ), + BlocProvider( + create: (_) => ReservationCubit(), + ), + BlocProvider( + create: + (_) => NotificationPreferencesBloc(), + ), + ], + child: const UserInfoPage(), + ), + ), (route) => false, ); } }, builder: (context, state) { + bool isLoading = state is AuthLoading; if (state is AuthLoading) { return const Center(child: CircularProgressIndicator()); } return SizedBox( width: double.infinity, + height: 60, child: ElevatedButton( - onPressed: _isOtpComplete ? _verifyOtp : null, - child: const Text("ورود"), + onPressed: + (_isOtpComplete && !isLoading) ? _verifyOtp : null, + child: + isLoading + ? const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 3, + ) + : const Text("ورود"), ), ); }, @@ -268,20 +321,26 @@ class _OtpPageState extends State { void _verifyOtp() { final otpCode = _controllers.map((c) => c.text).join(); if (otpCode.length == 5) { - context.read().add(VerifyOTPEvent(otp: otpCode)); + context.read().add( + VerifyOTPEvent( + otp: otpCode, + phoneNumber: widget.phone, + countryCode: widget.countryCode, + ), + ); } } void _resendOtp() { - setState(() { - _hasError = false; - _errorMessage = null; - for (var controller in _controllers) { - controller.clear(); - } - _isOtpComplete = false; - }); - context.read().add(SendOTPEvent(phoneNumber: widget.phoneNumber)); + for (var controller in _controllers) { + controller.clear(); + } + FocusScope.of(context).requestFocus(_focusNodes[0]); + setState(() => _isOtpComplete = false); + + context.read().add( + SendOTPEvent(phoneNumber: widget.phone, countryCode: widget.countryCode), + ); _otpTimer.resetTimer(); } } diff --git a/lib/presentation/pages/splash_screen.dart b/lib/presentation/pages/splash_screen.dart new file mode 100644 index 0000000..bdfc24f --- /dev/null +++ b/lib/presentation/pages/splash_screen.dart @@ -0,0 +1,53 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; +import 'package:proxibuy/presentation/pages/onboarding_page.dart'; +import 'package:proxibuy/presentation/pages/offers_page.dart'; +import 'package:proxibuy/core/gen/assets.gen.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + late final StreamSubscription _authSubscription; + + @override + void initState() { + super.initState(); + final authBloc = context.read(); + + _authSubscription = authBloc.stream.listen((state) { + _authSubscription.cancel(); + if (state is AuthSuccess) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const OffersPage()), + ); + } else { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const OnboardingPage()), + ); + } + }); + } + + @override + void dispose() { + _authSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: Center( + child: Assets.icons.logo.svg(height: 160), + ), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/pages/user_info_page.dart b/lib/presentation/pages/user_info_page.dart index d239d82..601063b 100644 --- a/lib/presentation/pages/user_info_page.dart +++ b/lib/presentation/pages/user_info_page.dart @@ -1,4 +1,3 @@ -// lib/presentation/pages/user_info_page.dart import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart'; @@ -15,7 +14,7 @@ class UserInfoPage extends StatefulWidget { class _UserInfoPageState extends State { final _nameController = TextEditingController(); - String _selectedGender = 'مرد'; + String _selectedGender = 'male'; @override void dispose() { @@ -32,8 +31,9 @@ class _UserInfoPageState extends State { Radio( value: value, groupValue: _selectedGender, - onChanged: (newValue) => setState(() => _selectedGender = newValue!), - activeColor: AppColors.primary, + onChanged: + (newValue) => setState(() => _selectedGender = newValue!), + activeColor: AppColors.primary, ), Text(title, style: const TextStyle(color: Colors.grey)), ], @@ -41,6 +41,24 @@ class _UserInfoPageState extends State { ); } + void _submitUserInfo() { + if (_nameController.text.isNotEmpty) { + context.read().add( + UpdateUserInfoEvent( + name: _nameController.text, + gender: _selectedGender, + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('لطفا نام خود را وارد کنید.'), + backgroundColor: Colors.red, + ), + ); + } + } + @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; @@ -49,10 +67,7 @@ class _UserInfoPageState extends State { body: Stack( children: [ Positioned.fill( - child: Image.asset( - Assets.images.userinfo.path, - fit: BoxFit.cover, - ), + child: Image.asset(Assets.images.userinfo.path, fit: BoxFit.cover), ), DraggableScrollableSheet( @@ -77,7 +92,7 @@ class _UserInfoPageState extends State { height: 5, decoration: BoxDecoration( // ignore: deprecated_member_use - color:Colors.grey.withOpacity(0.5), + color: Colors.grey.withOpacity(0.5), borderRadius: BorderRadius.circular(12), ), ), @@ -93,7 +108,10 @@ class _UserInfoPageState extends State { fontSize: 20, ), hintText: "مثلا نام کوچک شما", - hintStyle: TextStyle(fontSize: 15, color: Colors.grey), + hintStyle: TextStyle( + fontSize: 15, + color: Colors.grey, + ), ), ), const SizedBox(height: 24), @@ -109,44 +127,48 @@ class _UserInfoPageState extends State { spacing: 10.0, runSpacing: 8.0, children: [ - _buildGenderRadio('مرد', 'مرد'), - _buildGenderRadio('زن', 'زن'), - _buildGenderRadio('تمایلی به پاسخ ندارم', 'نامشخص'), + _buildGenderRadio('مرد', 'male'), + _buildGenderRadio('زن', 'female'), + _buildGenderRadio('تمایلی به پاسخ ندارم', 'none'), ], ), const SizedBox(height: 55), BlocConsumer( listener: (context, state) { - if (state is UserInfoSaved) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const NotificationPreferencesPage()), - ); - } else if (state is AuthFailure) { + if (state is AuthFailure) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.message), backgroundColor: Colors.red), + SnackBar( + content: Text(state.message), + backgroundColor: Colors.red, + ), ); } }, builder: (context, state) { - if (state is AuthLoading) { - return const Center(child: CircularProgressIndicator()); - } - return SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.confirm, - foregroundColor: Colors.white, + final isLoading = state is AuthLoading; + + return Column( + children: [ + SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.confirm, + foregroundColor: Colors.white, + ), + onPressed: isLoading ? null : _submitUserInfo, + child: + isLoading + ? const CircularProgressIndicator( + color: Colors.white, + ) + : const Text("اعمال"), + ), ), - onPressed: () { - context.read().add(SaveUserInfoEvent( - name: _nameController.text, - gender: _selectedGender, - )); - }, - child: const Text("اعمال"), - ), + const SizedBox(height: 9), + ], ); }, ), @@ -155,7 +177,9 @@ class _UserInfoPageState extends State { Center( child: TextButton( onPressed: () { - Navigator.of(context).pushReplacement(NotificationPreferencesPage.route()); + Navigator.of(context).pushReplacement( + NotificationPreferencesPage.route(), + ); }, child: const Text( "رد شدن", @@ -173,4 +197,4 @@ class _UserInfoPageState extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/product_detail/bloc/product_detail_event.dart b/lib/presentation/product_detail/bloc/product_detail_event.dart index 9071dcf..60be6d9 100644 --- a/lib/presentation/product_detail/bloc/product_detail_event.dart +++ b/lib/presentation/product_detail/bloc/product_detail_event.dart @@ -8,7 +8,6 @@ abstract class ProductDetailEvent extends Equatable { List get props => []; } -// این ایونت زمانی فراخوانی می‌شود که بخواهیم جزئیات یک محصول را دریافت کنیم class ProductDetailFetchRequested extends ProductDetailEvent { final String offerId; diff --git a/lib/presentation/utils/otp_timer_helper.dart b/lib/presentation/utils/otp_timer_helper.dart index 2174acd..0d5d28f 100644 --- a/lib/presentation/utils/otp_timer_helper.dart +++ b/lib/presentation/utils/otp_timer_helper.dart @@ -1,4 +1,3 @@ -// lib/presentation/utils/otp_timer_helper.dart import 'dart:async'; import 'package:flutter/foundation.dart'; diff --git a/lib/presentation/widgets/category_selection_card.dart b/lib/presentation/widgets/category_selection_card.dart index 4511b1b..9a00c4d 100644 --- a/lib/presentation/widgets/category_selection_card.dart +++ b/lib/presentation/widgets/category_selection_card.dart @@ -21,7 +21,6 @@ class CategorySelectionCard extends StatelessWidget { @override Widget build(BuildContext context) { - // رنگ بردر و متن بر اساس انتخاب شدن تغییر می‌کند final textAndBorderColor = isSelected ? AppColors.confirm : AppColors.unselectedBorder; return GestureDetector( @@ -32,7 +31,6 @@ class CategorySelectionCard extends StatelessWidget { child: Stack( alignment: Alignment.center, children: [ - // این ویجت تغییرات در دکوریشن را به صورت انیمیشنی نمایش می‌دهد AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, @@ -58,14 +56,12 @@ class CategorySelectionCard extends StatelessWidget { ), ), - // این ویجت انیمیشن بین نمایش تیک، دایره یا هیچ‌کدام را مدیریت می‌کند Positioned( top: 5, right: 5, child: AnimatedSwitcher( duration: const Duration(milliseconds: 250), transitionBuilder: (child, animation) { - // انیمیشن محو شدن و بزرگ شدن همزمان return FadeTransition( opacity: animation, child: ScaleTransition( @@ -81,7 +77,6 @@ class CategorySelectionCard extends StatelessWidget { ), ), const SizedBox(height: 8), - // رنگ متن هم به صورت انیمیشنی (توسط بازسازی ویجت) تغییر می‌کند Text( name, style: TextStyle( @@ -98,20 +93,17 @@ class CategorySelectionCard extends StatelessWidget { ); } - /// این متد کمکی مشخص می‌کند کدام نشانگر (تیک، دایره یا هیچکدام) نمایش داده شود Widget _buildIndicator() { if (isSelected) { - // نمایش تیک در صورت انتخاب return SvgPicture.asset( Assets.icons.tickCircle.path, - key: const ValueKey('tick'), // کلید برای کارکرد صحیح AnimatedSwitcher + key: const ValueKey('tick'), ); } if (showSelectableIndicator) { - // نمایش دایره در صورتی که آیتم‌های دیگر انتخاب شده‌اند return Container( - key: const ValueKey('circle'), // کلید برای کارکرد صحیح AnimatedSwitcher + key: const ValueKey('circle'), width: 12, height: 12, decoration: BoxDecoration( @@ -122,7 +114,6 @@ class CategorySelectionCard extends StatelessWidget { ); } - // در غیر این صورت، چیزی نمایش نده return const SizedBox.shrink(key: ValueKey('empty')); } } \ No newline at end of file diff --git a/lib/presentation/widgets/flutter_staggered_grid_view.dart b/lib/presentation/widgets/flutter_staggered_grid_view.dart index 78adabd..87bf896 100644 --- a/lib/presentation/widgets/flutter_staggered_grid_view.dart +++ b/lib/presentation/widgets/flutter_staggered_grid_view.dart @@ -12,7 +12,6 @@ class PhotoGalleryView extends StatelessWidget { required this.remainingPhotos, }); - // این ویجت کمکی تشخیص می‌دهد که عکس از فایل محلی است یا از اینترنت Widget _buildSmartImage(String imageUrl) { bool isFile = !imageUrl.startsWith('http'); return ClipRRect( @@ -61,10 +60,9 @@ class PhotoGalleryView extends StatelessWidget { return const Center(child: Text("هنوز عکسی وجود ندارد.")); } - // این بخش برای جلوگیری از خطا در صورتی که تعداد عکس‌ها کمتر از نیاز گرید باشد، اضافه شده است. final displayUrls = List.from(imageUrls); while (displayUrls.length < 6) { - displayUrls.add('https://via.placeholder.com/200'); // یک عکس جایگزین + displayUrls.add('https://via.placeholder.com/200'); } diff --git a/lib/presentation/widgets/gps_dialog.dart b/lib/presentation/widgets/gps_dialog.dart index 117232e..33ce6fe 100644 --- a/lib/presentation/widgets/gps_dialog.dart +++ b/lib/presentation/widgets/gps_dialog.dart @@ -55,27 +55,27 @@ Future showGPSDialog(BuildContext context) async { onTap: () => Navigator.of(context).pop(), child: Text("الان نه",style: TextStyle(color: AppColors.primary,fontWeight: FontWeight.bold),), ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primary, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide(color: AppColors.border), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide(color: AppColors.border), + ), + padding: const EdgeInsets.symmetric( + horizontal: 45, vertical: 7), + ), + onPressed: () async { + await Geolocator.openLocationSettings(); + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + }, + child: const Text( + "فعالسازی", + style: TextStyle(color: Colors.white), ), - padding: const EdgeInsets.symmetric( - horizontal: 45, vertical: 7), ), - onPressed: () async { - await Geolocator.openLocationSettings(); - // ignore: use_build_context_synchronously - Navigator.of(context).pop(); - }, - child: const Text( - "فعالسازی", - style: TextStyle(color: Colors.white), - ), - ), ], ), ], diff --git a/lib/presentation/widgets/onboarding_indicator_painter.dart b/lib/presentation/widgets/onboarding_indicator_painter.dart index c67ed35..50c001d 100644 --- a/lib/presentation/widgets/onboarding_indicator_painter.dart +++ b/lib/presentation/widgets/onboarding_indicator_painter.dart @@ -24,19 +24,11 @@ class OnboardingIndicatorPainter extends CustomPainter { final center = Offset(size.width / 2, size.height / 2); final radius = min(size.width, size.height) / 2; - // ================== شروع تغییرات ================== - - // 1. تعریف اندازه فاصله بین قوس‌ها (بر حسب درجه) - // این عدد را می‌توانید برای کم و زیاد کردن فاصله تغییر دهید const double gapInDegrees = 30.0; const double gapInRadians = gapInDegrees * (pi / 180); - // 2. محاسبه طول جدید هر قوس (۹۰ درجه منهای اندازه فاصله) const double arcAngle = (pi / 2) - gapInRadians; - // =================== پایان تغییرات =================== - - final startAngles = [-pi / 2, 0.0, pi / 2, pi]; for (int i = 0; i < pageCount; i++) { @@ -44,15 +36,14 @@ class OnboardingIndicatorPainter extends CustomPainter { ..color = (i == currentPage) ? activeColor : inactiveColor ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth - ..strokeCap = StrokeCap.round; // StrokeCap.round لبه‌های خط را گرد می‌کند که زیباتر است + ..strokeCap = StrokeCap.round; - // 3. محاسبه نقطه شروع جدید (با افزودن نصف فاصله برای وسط‌چین شدن) final correctedStartAngle = startAngles[i] + (gapInRadians / 2); canvas.drawArc( Rect.fromCircle(center: center, radius: radius), - correctedStartAngle, // استفاده از نقطه شروع اصلاح شده - arcAngle, // استفاده از طول قوس جدید + correctedStartAngle, + arcAngle, false, paint, ); diff --git a/lib/presentation/widgets/reserved_list_item_card.dart b/lib/presentation/widgets/reserved_list_item_card.dart index 3a471b2..c65144b 100644 --- a/lib/presentation/widgets/reserved_list_item_card.dart +++ b/lib/presentation/widgets/reserved_list_item_card.dart @@ -66,10 +66,9 @@ class _ReservedListItemCardState extends State { @override Widget build(BuildContext context) { - // ویجت اصلی به Column تغییر کرده است + return Column( children: [ - // بخش اول: کارت اصلی که فقط شامل اطلاعات محصول است Card( color: Colors.white, shape: RoundedRectangleBorder( @@ -79,16 +78,14 @@ class _ReservedListItemCardState extends State { elevation: 0, clipBehavior: Clip.antiAlias, margin: EdgeInsets.zero, - child: _buildOfferPrimaryDetails(), // فقط اطلاعات اصلی داخل کارت + child: _buildOfferPrimaryDetails(), ), _buildActionsRow(), - // بخش سوم: پنل باز شونده QR کد _buildExpansionPanel(), ], ); } - // این متد فقط اطلاعات اصلی داخل کارت را می‌سازد Widget _buildOfferPrimaryDetails() { return Padding( padding: const EdgeInsets.fromLTRB(15, 25, 15, 25), @@ -138,7 +135,6 @@ class _ReservedListItemCardState extends State { children: [ SvgPicture.asset(Assets.icons.location.path), SizedBox(width: 8), - // برای جلوگیری از سرریز شدن متن، از Flexible استفاده می‌کنیم Flexible( child: Text( "${widget.offer.address} (${widget.offer.distanceInMeters} متر تا تخفیف)", diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart new file mode 100644 index 0000000..4a092c1 --- /dev/null +++ b/lib/services/mqtt_service.dart @@ -0,0 +1,60 @@ +// import 'dart:async'; +// import 'dart:math'; +// import 'package:mqtt_client/mqtt_client.dart'; +// import 'package:mqtt_client/mqtt_server_client.dart'; + +// class MqttService { +// late MqttServerClient client; +// final String server = '5.75.200.241'; +// final int port = 1883; + +// Future connect(String token) async { +// // 1. معادل‌سازی پارامترها +// final String clientId = 'nest-' + Random().nextInt(0xFFFFFF).toRadixString(16).padLeft(6, '0'); +// final String username = 'ignored'; +// final String password = token; // ✨ توکن شما مستقیماً به عنوان پسورد در نظر گرفته می‌شود + +// // 2. ساخت کلاینت +// client = MqttServerClient.withPort(server, clientId, port); +// client.logging(on: true); +// client.keepAlivePeriod = 60; +// client.autoReconnect = false; // معادل reconnectPeriod: 0 +// client.setProtocolV311(); + +// // 3. ساخت پیام اتصال با پارامترهای تعریف شده +// final connMessage = MqttConnectMessage() +// .withClientIdentifier(clientId) +// .startClean() +// .authenticateAs(username, password); // ارسال نام کاربری و رمز عبور (توکن) + +// client.connectionMessage = connMessage; + +// // 4. تعریف Callbackها و اتصال +// client.onConnected = () { +// print('✅ MQTT Connected'); +// client.updates!.listen((List> c) { +// final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage; +// final String payload = +// MqttPublishPayload.bytesToStringAsString(recMess.payload.message); +// print('Received message: "$payload" from topic: ${c[0].topic}'); +// }); + +// client.subscribe('test/topic', MqttQos.atLeastOnce); +// }; + +// client.onDisconnected = () { +// print('❌ MQTT Disconnected'); +// }; + +// client.onSubscribed = (String topic) { +// print('✅ Subscribed to $topic'); +// }; + +// try { +// await client.connect(); +// } catch (e) { +// print('Exception: $e'); +// client.disconnect(); +// } +// } +// } \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 020f725..2c04cf6 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_localization_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin"); flutter_localization_plugin_register_with_registrar(flutter_localization_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); g_autoptr(FlPluginRegistrar) maps_launcher_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MapsLauncherPlugin"); maps_launcher_plugin_register_with_registrar(maps_launcher_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index a021899..1462f6f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_localization + flutter_secure_storage_linux maps_launcher url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 5f18724..053b1a1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,13 @@ import FlutterMacOS import Foundation +import cloud_firestore import file_selector_macos +import firebase_auth +import firebase_core +import firebase_storage import flutter_localization +import flutter_secure_storage_macos import geolocator_apple import maps_launcher import path_provider_foundation @@ -15,8 +20,13 @@ import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) MapsLauncherPlugin.register(with: registry.registrar(forPlugin: "MapsLauncherPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 7adbb89..8b4111d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "82.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 + url: "https://pub.dev" + source: hosted + version: "1.3.59" analyzer: dependency: transitive description: @@ -169,6 +177,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "2d33da4465bdb81b6685c41b535895065adcb16261beb398f5f3bbc623979e9c" + url: "https://pub.dev" + source: hosted + version: "5.6.12" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "413c4e01895cf9cb3de36fa5c219479e06cd4722876274ace5dfc9f13ab2e39b" + url: "https://pub.dev" + source: hosted + version: "6.6.12" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: c1e30fc4a0fcedb08723fb4b1f12ee4e56d937cbf9deae1bda43cbb6367bb4cf + url: "https://pub.dev" + source: hosted + version: "4.4.12" code_builder: dependency: transitive description: @@ -273,6 +305,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.7" + event_bus: + dependency: transitive + description: + name: event_bus + sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" + url: "https://pub.dev" + source: hosted + version: "2.0.1" fake_async: dependency: transitive description: @@ -329,6 +369,78 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+4" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386" + url: "https://pub.dev" + source: hosted + version: "7.7.3" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e + url: "https://pub.dev" + source: hosted + version: "5.15.3" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" + url: "https://pub.dev" + source: hosted + version: "3.15.2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" + url: "https://pub.dev" + source: hosted + version: "2.24.1" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + sha256: "958fc88a7ef0b103e694d30beed515c8f9472dde7e8459b029d0e32b8ff03463" + url: "https://pub.dev" + source: hosted + version: "12.4.10" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + sha256: d2661c05293c2a940c8ea4bc0444e1b5566c79dd3202c2271140c082c8cd8dd4 + url: "https://pub.dev" + source: hosted + version: "5.2.10" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + sha256: "629a557c5e1ddb97a3666cbf225e97daa0a66335dbbfdfdce113ef9f881e833f" + url: "https://pub.dev" + source: hosted + version: "3.10.17" fixnum: dependency: transitive description: @@ -419,6 +531,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.28" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_shaders: dependency: transitive description: @@ -649,10 +809,10 @@ packages: dependency: transitive description: name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -741,6 +901,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mqtt_client: + dependency: "direct main" + description: + name: mqtt_client + sha256: "85fa7e9aad03fbd6a54fcaf46127579f3e049b4834557a06e49aea7869b04c94" + url: "https://pub.dev" + source: hosted + version: "10.8.0" nested: dependency: transitive description: @@ -1346,6 +1514,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://pub.dev" + source: hosted + version: "5.13.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cefb72b..51a4a9a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,12 @@ dependencies: qr_flutter: ^4.1.0 flutter_staggered_grid_view: ^0.7.0 image_picker: ^1.1.2 + mqtt_client: ^10.8.0 + firebase_core: ^3.15.2 + firebase_auth: ^5.7.0 + cloud_firestore: ^5.6.12 + firebase_storage: ^12.4.10 + flutter_secure_storage: ^9.2.4 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ccecd30..c5be68e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,18 +6,33 @@ #include "generated_plugin_registrant.h" +#include #include +#include +#include +#include #include +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FirebaseStoragePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi")); FlutterLocalizationPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); MapsLauncherPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f5f5fec..489377e 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,13 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore file_selector_windows + firebase_auth + firebase_core + firebase_storage flutter_localization + flutter_secure_storage_windows geolocator_windows maps_launcher permission_handler_windows