google sign-in/out

This commit is contained in:
mohamadmahdi jebeli 2025-09-08 11:04:57 +03:30
parent 049e037933
commit 2354ee0a14
19 changed files with 493 additions and 291 deletions

View File

0
SETUP_INSTRUCTIONS.md Normal file
View File

0
TROUBLESHOOTING_GUIDE.md Normal file
View File

42
lib/auth_gate.dart Normal file
View File

@ -0,0 +1,42 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lba/screens/auth/login_page.dart';
import 'package:lba/screens/mains/navigation/navigation.dart';
import 'package:lba/screens/auth/bloc/auth_bloc.dart';
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthInitial) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginPage()),
(route) => false,
);
}
},
child: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
if (!snapshot.hasData || snapshot.data == null) {
return const LoginPage();
}
return const MainScreen();
},
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:lba/screens/auth/onboarding_page.dart';
import 'package:lba/screens/mains/navigation/navigation.dart';
class AuthNavigationHandler extends StatelessWidget {
const AuthNavigationHandler({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
print('🔍 Auth State: ${snapshot.connectionState}, hasData: ${snapshot.hasData}');
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
if (snapshot.hasData && snapshot.data != null) {
print('✅ User is logged in: ${snapshot.data!.uid}');
return const MainScreen();
}
print('❌ User is not logged in');
return const OnboardingPage();
},
);
}
}

53
lib/auth_wrapper.dart Normal file
View File

@ -0,0 +1,53 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lba/screens/auth/onboarding_page.dart';
import 'package:lba/screens/mains/navigation/navigation.dart';
import 'package:lba/screens/auth/bloc/auth_bloc.dart';
class AuthWrapper extends StatefulWidget {
const AuthWrapper({super.key});
@override
State<AuthWrapper> createState() => _AuthWrapperState();
}
class _AuthWrapperState extends State<AuthWrapper> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthInitial) {
if (FirebaseAuth.instance.currentUser == null) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const OnboardingPage()),
(route) => false,
);
}
}
},
child: StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
final bool isLoggedIn = snapshot.hasData && snapshot.data != null;
if (isLoggedIn) {
return const MainScreen();
} else {
return const OnboardingPage();
}
},
),
);
}
}

View File

View File

@ -3,13 +3,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'screens/auth/onboarding_page.dart';
import 'screens/auth/bloc/auth_bloc.dart'; import 'screens/auth/bloc/auth_bloc.dart';
import 'screens/auth/usecases/send_otp.dart';
import 'screens/auth/usecases/verify_otp.dart';
import 'widgets/animated_splash_screen.dart'; import 'widgets/animated_splash_screen.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'utils/theme_manager.dart'; import 'utils/theme_manager.dart';
import 'simple_auth_gate.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -26,10 +24,7 @@ class MyApp extends StatelessWidget {
providers: [ providers: [
ChangeNotifierProvider(create: (context) => ThemeManager()), ChangeNotifierProvider(create: (context) => ThemeManager()),
BlocProvider( BlocProvider(
create: (context) => AuthBloc( create: (context) => AuthBloc(),
sendOTPUseCase: SendOTP(),
verifyOTPUseCase: VerifyOTP(),
),
), ),
], ],
child: Consumer<ThemeManager>( child: Consumer<ThemeManager>(
@ -38,18 +33,17 @@ class MyApp extends StatelessWidget {
title: 'LBA', title: 'LBA',
theme: themeManager.lightTheme, theme: themeManager.lightTheme,
darkTheme: themeManager.darkTheme, darkTheme: themeManager.darkTheme,
themeMode: themeManager.isDarkMode ? ThemeMode.dark : ThemeMode.light, themeMode:
themeManager.isDarkMode ? ThemeMode.dark : ThemeMode.light,
themeAnimationDuration: const Duration(milliseconds: 300), themeAnimationDuration: const Duration(milliseconds: 300),
themeAnimationCurve: Curves.easeInOutCubic, themeAnimationCurve: Curves.easeInOutCubic,
home: CoolSplashScreen( home: CoolSplashScreen(
nextScreen: const OnboardingPage(), nextScreen: const SimpleAuthGate(),
duration: const Duration(seconds: 6), duration: const Duration(seconds: 3),
), ),
); );
}, },
), ),
); );
} }
} }

View File

@ -1,58 +1,90 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:lba/screens/auth/usecases/send_otp.dart';
import 'package:lba/screens/auth/usecases/verify_otp.dart';
part 'auth_event.dart'; part 'auth_event.dart';
part 'auth_state.dart'; part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> { class AuthBloc extends Bloc<AuthEvent, AuthState> {
final SendOTP sendOTPUseCase;
final VerifyOTP verifyOTPUseCase;
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
String? _verificationId;
String _timeStamp = ""; AuthBloc() : super(AuthInitial()) {
String _timeDue = "";
String get timeStamp => _timeStamp;
String get timeDue => _timeDue;
AuthBloc({
required this.sendOTPUseCase,
required this.verifyOTPUseCase,
}) : super(AuthInitial()) {
on<SendOTPEvent>(_onSendOTP); on<SendOTPEvent>(_onSendOTP);
on<VerifyOTPEvent>(_onVerifyOTP); on<VerifyOTPEvent>(_onVerifyOTP);
on<SignInWithGoogleEvent>(_onSignInWithGoogle); on<SignInWithGoogleEvent>(_onSignInWithGoogle);
on<SignOutEvent>(_onSignOut);
} }
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async { Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading()); emit(AuthLoading());
final result = await sendOTPUseCase(SendOTPParams(phoneNumber: event.phoneNumber)); final completer = Completer<void>();
if (result.isSuccess && result.data != null) { try {
_timeStamp = result.data!.timeStamp; await _firebaseAuth.verifyPhoneNumber(
_timeDue = result.data!.timeDue;
emit(AuthSuccess(
phoneNumber: event.phoneNumber, phoneNumber: event.phoneNumber,
timeStamp: _timeStamp, verificationCompleted: (PhoneAuthCredential credential) async {
timeDue: _timeDue, debugPrint("✅ Verification Completed (Auto-retrieval)");
)); await _firebaseAuth.signInWithCredential(credential);
} else { if (!completer.isCompleted) {
emit(AuthError('Error sending code. Please try again.')); emit(OTPVerified());
completer.complete();
}
},
verificationFailed: (FirebaseAuthException e) {
debugPrint("❌ Verification Failed: ${e.message}");
if (!completer.isCompleted) {
emit(AuthError(e.message ?? 'Verification Failed'));
completer.complete();
}
},
codeSent: (String verificationId, int? resendToken) {
debugPrint("📬 Code Sent! Verification ID: $verificationId");
_verificationId = verificationId;
if (!completer.isCompleted) {
emit(AuthCodeSent(
phoneNumber: event.phoneNumber,
));
completer.complete();
}
},
codeAutoRetrievalTimeout: (String verificationId) {
debugPrint("⌛️ Code Auto-retrieval Timeout.");
},
);
await completer.future;
} catch (e) {
debugPrint("⛔️ General Error in _onSendOTP: ${e.toString()}");
if (!completer.isCompleted) {
emit(AuthError(e.toString()));
}
} }
} }
// ... بقیه کد بدون تغییر ...
Future<void> _onVerifyOTP(VerifyOTPEvent event, Emitter<AuthState> emit) async { Future<void> _onVerifyOTP(VerifyOTPEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading()); emit(AuthLoading());
final result = await verifyOTPUseCase(VerifyOTPParams( try {
otpCode: event.otpCode, if (_verificationId != null) {
phoneNumber: event.phoneNumber, final credential = PhoneAuthProvider.credential(
)); verificationId: _verificationId!,
if (result.isSuccess && result.data == true) { smsCode: event.otpCode,
emit(OTPVerified()); );
} else {
emit(AuthError('The code entered is incorrect. Please try again.')); await _firebaseAuth.signInWithCredential(credential);
emit(OTPVerified());
} else {
emit(AuthError("Verification ID not found. Please try again."));
}
} on FirebaseAuthException catch (e) {
if (e.code == 'invalid-verification-code') {
emit(AuthError('The code entered is incorrect. Please try again.'));
} else {
emit(AuthError(e.message ?? 'An error occurred'));
}
} catch (e) {
emit(AuthError(e.toString()));
} }
} }
@ -60,7 +92,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
SignInWithGoogleEvent event, SignInWithGoogleEvent event,
Emitter<AuthState> emit, Emitter<AuthState> emit,
) async { ) async {
emit(AuthLoading()); emit(AuthLoading());
try { try {
final GoogleSignIn googleSignIn = GoogleSignIn( final GoogleSignIn googleSignIn = GoogleSignIn(
scopes: ['email'], scopes: ['email'],
@ -94,4 +126,24 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
} }
} }
} Future<void> _onSignOut(SignOutEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
debugPrint('🚪 Starting logout process...');
await GoogleSignIn().signOut();
debugPrint('✅ Google SignOut completed');
await _firebaseAuth.signOut();
debugPrint('✅ Firebase SignOut completed');
emit(AuthInitial());
debugPrint('✅ AuthInitial state emitted');
} catch (e) {
debugPrint('❌ Error in logout: $e');
emit(AuthError("Error signing out: ${e.toString()}"));
}
}
}

View File

@ -18,4 +18,6 @@ class VerifyOTPEvent extends AuthEvent {
}); });
} }
class SignInWithGoogleEvent extends AuthEvent {} class SignInWithGoogleEvent extends AuthEvent {}
class SignOutEvent extends AuthEvent {}

View File

@ -6,21 +6,13 @@ class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {} class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState { class AuthCodeSent extends AuthState {
final String phoneNumber; final String phoneNumber;
final String timeStamp; AuthCodeSent({required this.phoneNumber});
final String timeDue;
AuthSuccess({
required this.phoneNumber,
required this.timeStamp,
required this.timeDue,
});
} }
class AuthError extends AuthState { class AuthError extends AuthState {
final String message; final String message;
AuthError(this.message); AuthError(this.message);
} }

View File

@ -91,9 +91,7 @@ class _LoginPageState extends State<LoginPage> {
), ),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
borderSide: BorderSide( borderSide: BorderSide(color: AppColors.borderPrimary),
color: AppColors.borderPrimary,
),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
@ -142,37 +140,40 @@ class _LoginPageState extends State<LoginPage> {
// keepSignedIn = !keepSignedIn; // keepSignedIn = !keepSignedIn;
// }); // });
// }, // },
// child: Row( // child: Row(
// mainAxisAlignment: MainAxisAlignment.start, // mainAxisAlignment: MainAxisAlignment.start,
// children: [ // children: [
// Checkbox( // Checkbox(
// value: keepSignedIn, // value: keepSignedIn,
// activeColor: AppColors.borderPrimary, // activeColor: AppColors.borderPrimary,
// onChanged: (bool? value) { // onChanged: (bool? value) {
// setState(() { // setState(() {
// keepSignedIn = value ?? false; // keepSignedIn = value ?? false;
// }); // });
// }, // },
// ), // ),
// const Text( // const Text(
// "Keep me signed in", // "Keep me signed in",
// style: TextStyle(fontSize: 15), // style: TextStyle(fontSize: 15),
// ), // ),
// ], // ],
// ), // ),
// ), // ),
SizedBox(height: height / 60), SizedBox(height: height / 60),
BlocConsumer<AuthBloc, AuthState>( BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) { listener: (context, state) {
if (state is AuthSuccess) { // اینجا رو تغییر بده
if (state is AuthCodeSent) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: builder:
(context) => OTPVerificationPage( (context) => OTPVerificationPage(
phoneNumber: phoneNumber: state.phoneNumber,
"+${_selectedCountry.phoneCode}${phoneController.text}", timeDue:
timeDue: state.timeDue, DateTime.now()
.add(const Duration(minutes: 2))
.toIso8601String(),
), ),
), ),
); );
@ -219,7 +220,10 @@ class _LoginPageState extends State<LoginPage> {
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Divider(thickness: 1, color: AppColors.greyBorder), child: Divider(
thickness: 1,
color: AppColors.greyBorder,
),
), ),
Padding( Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
@ -234,7 +238,10 @@ class _LoginPageState extends State<LoginPage> {
), ),
), ),
Expanded( Expanded(
child: Divider(thickness: 1, color: AppColors.greyBorder), child: Divider(
thickness: 1,
color: AppColors.greyBorder,
),
), ),
], ],
), ),
@ -259,7 +266,9 @@ class _LoginPageState extends State<LoginPage> {
), ),
), ),
onPressed: () { onPressed: () {
context.read<AuthBloc>().add(SignInWithGoogleEvent()); context.read<AuthBloc>().add(
SignInWithGoogleEvent(),
);
}, },
), ),
), ),
@ -277,14 +286,15 @@ class _LoginPageState extends State<LoginPage> {
void _openCountryPicker(BuildContext context) { void _openCountryPicker(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => CustomCountryPicker( builder:
onCountrySelected: (Country country) { (context) => CustomCountryPicker(
setState(() { onCountrySelected: (Country country) {
_selectedCountry = country; setState(() {
countryController.text = country.name; _selectedCountry = country;
}); countryController.text = country.name;
}, });
), },
),
); );
} }
} }

View File

@ -6,7 +6,6 @@ import 'package:lba/screens/auth/bloc/auth_bloc.dart';
import 'package:lba/widgets/app_snackbar.dart'; import 'package:lba/widgets/app_snackbar.dart';
import '../../gen/assets.gen.dart'; import '../../gen/assets.gen.dart';
import '../../widgets/button.dart'; import '../../widgets/button.dart';
import '../../widgets/remainingTime.dart';
import 'user_info_page.dart'; import 'user_info_page.dart';
class OTPVerificationPage extends StatefulWidget { class OTPVerificationPage extends StatefulWidget {
@ -24,29 +23,8 @@ class OTPVerificationPage extends StatefulWidget {
} }
class _OTPVerificationPageState extends State<OTPVerificationPage> { class _OTPVerificationPageState extends State<OTPVerificationPage> {
final List<FocusNode> _focusNodes = List.generate(5, (_) => FocusNode()); final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
final List<TextEditingController> _controllers = final List<TextEditingController> _controllers = List.generate(6, (_) => TextEditingController());
List.generate(5, (_) => TextEditingController());
late RemainingTime _otpTimer;
@override
void initState() {
super.initState();
_otpTimer = RemainingTime();
_initializeTimer();
}
void _initializeTimer() {
_otpTimer.initializeFromExpiry(expiryTime: widget.timeDue);
}
void _resendOTP() {
if (_otpTimer.canResend.value) {
context
.read<AuthBloc>()
.add(SendOTPEvent(phoneNumber: widget.phoneNumber));
}
}
@override @override
void dispose() { void dispose() {
@ -56,16 +34,15 @@ class _OTPVerificationPageState extends State<OTPVerificationPage> {
for (final controller in _controllers) { for (final controller in _controllers) {
controller.dispose(); controller.dispose();
} }
_otpTimer.dispose();
super.dispose(); super.dispose();
} }
Widget _buildOTPFields() { Widget _buildOTPFields() {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(5, (index) { children: List.generate(6, (index) {
return SizedBox( return SizedBox(
width: 60, width: 50,
child: TextField( child: TextField(
controller: _controllers[index], controller: _controllers[index],
focusNode: _focusNodes[index], focusNode: _focusNodes[index],
@ -81,14 +58,16 @@ class _OTPVerificationPageState extends State<OTPVerificationPage> {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide( borderSide: BorderSide(color: AppColors.borderPrimary),
color: AppColors.borderPrimary),
), ),
), ),
onChanged: (value) { onChanged: (value) {
if (value.length == 1 && index < 4) { if (value.length == 1 && index < 5) {
FocusScope.of(context).requestFocus(_focusNodes[index + 1]); FocusScope.of(context).requestFocus(_focusNodes[index + 1]);
} }
if (value.isEmpty && index > 0) {
FocusScope.of(context).requestFocus(_focusNodes[index - 1]);
}
}, },
), ),
); );
@ -100,157 +79,107 @@ class _OTPVerificationPageState extends State<OTPVerificationPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.height; final height = MediaQuery.of(context).size.height;
return BlocListener<AuthBloc, AuthState>( return Scaffold(
listener: (context, state) { body: SafeArea(
if (state is AuthSuccess) { child: SingleChildScrollView(
_otpTimer.initializeFromExpiry(expiryTime: state.timeDue); child: Column(
} crossAxisAlignment: CrossAxisAlignment.start,
}, children: [
child: Scaffold( Row(
body: SafeArea( children: [
child: SingleChildScrollView( IconButton(
child: Column( icon: SvgPicture.asset(Assets.icons.back.path),
crossAxisAlignment: CrossAxisAlignment.start, onPressed: () => Navigator.pop(context),
children: [ ),
Row( ],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
IconButton( Center(
icon: SvgPicture.asset(Assets.icons.back.path), child: Padding(
onPressed: () => Navigator.pop(context), padding: const EdgeInsets.only(top: 0),
child: SvgPicture.asset(Assets.images.logo.path, height: height / 5.2),
),
),
SizedBox(height: height / 20),
const Text("OTP Verification", style: TextStyle(fontSize: 33, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(
"Enter the 6-digit verification code sent to your device.",
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
),
const SizedBox(height: 25),
_buildOTPFields(),
SizedBox(height: height / 7),
BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is OTPVerified) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const UserInfoPage()),
(route) => false,
);
}
if (state is AuthError) {
AppSnackBar.showError(
context: context,
message: state.message,
duration: const Duration(seconds: 3),
);
}
},
builder: (context, state) {
if (state is AuthLoading) {
return const Center(child: CircularProgressIndicator());
}
return SizedBox(
width: double.infinity,
height: 48,
child: Button(
text: "Verify",
onPressed: () {
final otpCode = _controllers.map((c) => c.text).join();
if (otpCode.length == 6) {
context.read<AuthBloc>().add(VerifyOTPEvent(
otpCode: otpCode,
phoneNumber: widget.phoneNumber,
));
} else {
AppSnackBar.showWarning(
context: context,
message: 'Please enter the complete 6-digit OTP code',
);
}
},
color: AppColors.buttonPrimary,
),
);
},
),
SizedBox(height: height / 25),
Center(
child: InkWell(
onTap: () {
context.read<AuthBloc>().add(SendOTPEvent(phoneNumber: widget.phoneNumber));
AppSnackBar.showInfo(context: context, message: "A new code has been sent.");
},
child: Text(
"Resend Code",
style: TextStyle(
fontSize: 16,
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
),
), ),
], ],
), ),
Padding( ),
padding: ],
const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Padding(
padding: const EdgeInsets.only(top: 0),
child: SvgPicture.asset(Assets.images.logo.path,
height: height / 5.2),
),
),
SizedBox(height: height / 20),
const Text("OTP Verification",
style: TextStyle(
fontSize: 33, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text(
"Enter the verification code we just sent to your device.",
style: TextStyle(
fontSize: 17, fontWeight: FontWeight.w500),
),
const SizedBox(height: 25),
_buildOTPFields(),
SizedBox(height: height / 7),
BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is OTPVerified) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const UserInfoPage()),
);
}
if (state is AuthError) {
AppSnackBar.showError(
context: context,
message: state.message,
duration: const Duration(seconds: 2),
);
}
},
builder: (context, state) {
if (state is AuthLoading) {
return const Center(
child: CircularProgressIndicator());
}
return SizedBox(
width: double.infinity,
height: 48,
child: Button(
text: "Verify",
onPressed: () {
final otpCode =
_controllers.map((c) => c.text).join();
if (otpCode.length >= 4) {
context.read<AuthBloc>().add(VerifyOTPEvent(
otpCode: otpCode,
phoneNumber: widget.phoneNumber,
));
} else {
AppSnackBar.showWarning(
context: context,
message: 'Please enter the complete OTP code',
);
}
},
color: AppColors.buttonPrimary,
),
);
},
),
SizedBox(height: height / 25),
Center(
child: Column(
children: [
Row(
children: [
Expanded(
child: Divider(
thickness: 1, color: AppColors.greyBorder),
),
ValueListenableBuilder<int>(
valueListenable: _otpTimer.remainingSeconds,
builder: (context, seconds, _) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0),
child: Text(
"Resend OTP in ${_otpTimer.formatTime()}",
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 18),
),
);
},
),
Expanded(
child: Divider(
thickness: 1, color: AppColors.greyBorder),
),
],
),
const SizedBox(height: 8),
ValueListenableBuilder<bool>(
valueListenable: _otpTimer.canResend,
builder: (context, canResend, _) {
return GestureDetector(
onTap: canResend ? _resendOTP : null,
child: Text(
"Resend OTP",
style: TextStyle(
fontSize: 16,
color: canResend
? const Color.fromARGB(
255, 0, 0, 0)
: AppColors.greyBorder,
),
),
);
},
),
],
),
),
],
),
),
],
),
), ),
), ),
), ),

View File

@ -1224,7 +1224,7 @@ class FirstPurchaseCard extends StatelessWidget {
Row( Row(
children: [ children: [
SvgPicture.asset(Assets.icons.star.path, SvgPicture.asset(Assets.icons.star.path,
width: 16, color: AppColors.textSecondary), width: 16,),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
rating.toString(), rating.toString(),

53
lib/simple_auth_gate.dart Normal file
View File

@ -0,0 +1,53 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:lba/screens/auth/login_page.dart';
import 'package:lba/screens/auth/onboarding_page.dart';
import 'package:lba/screens/mains/navigation/navigation.dart';
class SimpleAuthGate extends StatelessWidget {
const SimpleAuthGate({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
if (snapshot.hasError) {
return const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text('An error occurred. Please try again later.'),
],
),
),
);
}
final user = snapshot.data;
final isLoggedIn = user != null;
if (isLoggedIn) {
return const MainScreen();
}
return _shouldShowOnboarding() ? const OnboardingPage() : const LoginPage();
},
);
}
bool _shouldShowOnboarding() {
return false;
}
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class GlobalNavigator {
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
static NavigatorState? get navigator => navigatorKey.currentState;
static void navigateToLogin() {
print('🚀 GlobalNavigator: Navigating to login...');
if (navigator != null) {
navigator!.pushNamedAndRemoveUntil(
'/login',
(Route<dynamic> route) => false,
);
}
}
static void navigateToMain() {
print('🚀 GlobalNavigator: Navigating to main...');
if (navigator != null) {
navigator!.pushNamedAndRemoveUntil(
'/main',
(Route<dynamic> route) => false,
);
}
}
}

View File

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/gen/assets.gen.dart'; import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart'; import 'package:lba/res/colors.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
Future<void> showLogoutDialog(BuildContext context) async { Future<void> showLogoutDialog(BuildContext context) async {
showDialog( showDialog(
@ -39,10 +41,7 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
curve: Curves.elasticOut, curve: Curves.elasticOut,
); );
_fadeAnimation = CurvedAnimation( _fadeAnimation = CurvedAnimation(parent: _controller, curve: Curves.easeIn);
parent: _controller,
curve: Curves.easeIn,
);
_controller.forward(); _controller.forward();
} }
@ -60,8 +59,9 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
child: ScaleTransition( child: ScaleTransition(
scale: _scaleAnimation, scale: _scaleAnimation,
child: Dialog( child: Dialog(
shape: shape: RoundedRectangleBorder(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), borderRadius: BorderRadius.circular(15),
),
elevation: 10, elevation: 10,
backgroundColor: AppColors.surface, backgroundColor: AppColors.surface,
child: Stack( child: Stack(
@ -69,8 +69,12 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only( padding: EdgeInsets.only(
top: 50, left: 20, right: 20, bottom: 20), top: 50,
left: 20,
right: 20,
bottom: 20,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -109,12 +113,20 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
side: BorderSide(color: AppColors.offerTimer), side: BorderSide(color: AppColors.offerTimer),
), ),
padding: padding: const EdgeInsets.symmetric(vertical: 12),
const EdgeInsets.symmetric(vertical: 12),
), ),
onPressed: () { onPressed: () async {
// TODO: Add actual logout logic here Navigator.of(context).pop();
Navigator.of(context).pop(); try {
await GoogleSignIn().signOut();
await FirebaseAuth.instance.signOut();
print('✅ Logout successful');
} catch (e) {
print('❌ Logout error: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('خطا در خروج: $e')),
);
}
}, },
child: Text( child: Text(
"Log Out", "Log Out",
@ -132,8 +144,7 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
side: BorderSide(color: AppColors.greyBorder), side: BorderSide(color: AppColors.greyBorder),
padding: padding: const EdgeInsets.symmetric(vertical: 12),
const EdgeInsets.symmetric(vertical: 12),
), ),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: const Text("Cancel"), child: const Text("Cancel"),
@ -163,7 +174,9 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
radius: 40, radius: 40,
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: SvgPicture.asset(Assets.icons.solarLogout3BoldDuotone.path), child: SvgPicture.asset(
Assets.icons.solarLogout3BoldDuotone.path,
),
), ),
), ),
), ),
@ -174,4 +187,4 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
), ),
); );
} }
} }

View File

@ -49,7 +49,6 @@ class IsOpenChecker {
final nowTime = Duration(hours: now.hour, minutes: now.minute); final nowTime = Duration(hours: now.hour, minutes: now.minute);
if (closeTime <= openTime) { if (closeTime <= openTime) {
// شیفت شبانه (مثلاً 22:00 تا 02:00)
return nowTime >= openTime || nowTime <= closeTime; return nowTime >= openTime || nowTime <= closeTime;
} }