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_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'screens/auth/onboarding_page.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 'package:firebase_core/firebase_core.dart';
import 'utils/theme_manager.dart';
import 'simple_auth_gate.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -26,10 +24,7 @@ class MyApp extends StatelessWidget {
providers: [
ChangeNotifierProvider(create: (context) => ThemeManager()),
BlocProvider(
create: (context) => AuthBloc(
sendOTPUseCase: SendOTP(),
verifyOTPUseCase: VerifyOTP(),
),
create: (context) => AuthBloc(),
),
],
child: Consumer<ThemeManager>(
@ -38,18 +33,17 @@ class MyApp extends StatelessWidget {
title: 'LBA',
theme: themeManager.lightTheme,
darkTheme: themeManager.darkTheme,
themeMode: themeManager.isDarkMode ? ThemeMode.dark : ThemeMode.light,
themeMode:
themeManager.isDarkMode ? ThemeMode.dark : ThemeMode.light,
themeAnimationDuration: const Duration(milliseconds: 300),
themeAnimationCurve: Curves.easeInOutCubic,
home: CoolSplashScreen(
nextScreen: const OnboardingPage(),
duration: const Duration(seconds: 6),
nextScreen: const SimpleAuthGate(),
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:google_sign_in/google_sign_in.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_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final SendOTP sendOTPUseCase;
final VerifyOTP verifyOTPUseCase;
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
String? _verificationId;
String _timeStamp = "";
String _timeDue = "";
String get timeStamp => _timeStamp;
String get timeDue => _timeDue;
AuthBloc({
required this.sendOTPUseCase,
required this.verifyOTPUseCase,
}) : super(AuthInitial()) {
AuthBloc() : super(AuthInitial()) {
on<SendOTPEvent>(_onSendOTP);
on<VerifyOTPEvent>(_onVerifyOTP);
on<SignInWithGoogleEvent>(_onSignInWithGoogle);
on<SignOutEvent>(_onSignOut);
}
Future<void> _onSendOTP(SendOTPEvent event, Emitter<AuthState> emit) async {
emit(AuthLoading());
final result = await sendOTPUseCase(SendOTPParams(phoneNumber: event.phoneNumber));
if (result.isSuccess && result.data != null) {
_timeStamp = result.data!.timeStamp;
_timeDue = result.data!.timeDue;
emit(AuthSuccess(
final completer = Completer<void>();
try {
await _firebaseAuth.verifyPhoneNumber(
phoneNumber: event.phoneNumber,
timeStamp: _timeStamp,
timeDue: _timeDue,
));
} else {
emit(AuthError('Error sending code. Please try again.'));
verificationCompleted: (PhoneAuthCredential credential) async {
debugPrint("✅ Verification Completed (Auto-retrieval)");
await _firebaseAuth.signInWithCredential(credential);
if (!completer.isCompleted) {
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 {
emit(AuthLoading());
final result = await verifyOTPUseCase(VerifyOTPParams(
otpCode: event.otpCode,
phoneNumber: event.phoneNumber,
));
if (result.isSuccess && result.data == true) {
emit(OTPVerified());
} else {
emit(AuthError('The code entered is incorrect. Please try again.'));
try {
if (_verificationId != null) {
final credential = PhoneAuthProvider.credential(
verificationId: _verificationId!,
smsCode: event.otpCode,
);
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,
Emitter<AuthState> emit,
) async {
emit(AuthLoading());
emit(AuthLoading());
try {
final GoogleSignIn googleSignIn = GoogleSignIn(
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 AuthSuccess extends AuthState {
class AuthCodeSent extends AuthState {
final String phoneNumber;
final String timeStamp;
final String timeDue;
AuthSuccess({
required this.phoneNumber,
required this.timeStamp,
required this.timeDue,
});
AuthCodeSent({required this.phoneNumber});
}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}

View File

@ -91,9 +91,7 @@ class _LoginPageState extends State<LoginPage> {
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(
color: AppColors.borderPrimary,
),
borderSide: BorderSide(color: AppColors.borderPrimary),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
@ -142,37 +140,40 @@ class _LoginPageState extends State<LoginPage> {
// keepSignedIn = !keepSignedIn;
// });
// },
// child: Row(
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
// Checkbox(
// value: keepSignedIn,
// activeColor: AppColors.borderPrimary,
// onChanged: (bool? value) {
// setState(() {
// keepSignedIn = value ?? false;
// });
// },
// ),
// const Text(
// "Keep me signed in",
// style: TextStyle(fontSize: 15),
// ),
// ],
// ),
// child: Row(
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
// Checkbox(
// value: keepSignedIn,
// activeColor: AppColors.borderPrimary,
// onChanged: (bool? value) {
// setState(() {
// keepSignedIn = value ?? false;
// });
// },
// ),
// const Text(
// "Keep me signed in",
// style: TextStyle(fontSize: 15),
// ),
// ],
// ),
// ),
SizedBox(height: height / 60),
BlocConsumer<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthSuccess) {
// اینجا رو تغییر بده
if (state is AuthCodeSent) {
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => OTPVerificationPage(
phoneNumber:
"+${_selectedCountry.phoneCode}${phoneController.text}",
timeDue: state.timeDue,
phoneNumber: state.phoneNumber,
timeDue:
DateTime.now()
.add(const Duration(minutes: 2))
.toIso8601String(),
),
),
);
@ -219,7 +220,10 @@ class _LoginPageState extends State<LoginPage> {
Row(
children: [
Expanded(
child: Divider(thickness: 1, color: AppColors.greyBorder),
child: Divider(
thickness: 1,
color: AppColors.greyBorder,
),
),
Padding(
padding: EdgeInsets.symmetric(
@ -234,7 +238,10 @@ class _LoginPageState extends State<LoginPage> {
),
),
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: () {
context.read<AuthBloc>().add(SignInWithGoogleEvent());
context.read<AuthBloc>().add(
SignInWithGoogleEvent(),
);
},
),
),
@ -277,14 +286,15 @@ class _LoginPageState extends State<LoginPage> {
void _openCountryPicker(BuildContext context) {
showDialog(
context: context,
builder: (context) => CustomCountryPicker(
onCountrySelected: (Country country) {
setState(() {
_selectedCountry = country;
countryController.text = country.name;
});
},
),
builder:
(context) => CustomCountryPicker(
onCountrySelected: (Country country) {
setState(() {
_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 '../../gen/assets.gen.dart';
import '../../widgets/button.dart';
import '../../widgets/remainingTime.dart';
import 'user_info_page.dart';
class OTPVerificationPage extends StatefulWidget {
@ -24,29 +23,8 @@ class OTPVerificationPage extends StatefulWidget {
}
class _OTPVerificationPageState extends State<OTPVerificationPage> {
final List<FocusNode> _focusNodes = List.generate(5, (_) => FocusNode());
final List<TextEditingController> _controllers =
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));
}
}
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
final List<TextEditingController> _controllers = List.generate(6, (_) => TextEditingController());
@override
void dispose() {
@ -56,16 +34,15 @@ class _OTPVerificationPageState extends State<OTPVerificationPage> {
for (final controller in _controllers) {
controller.dispose();
}
_otpTimer.dispose();
super.dispose();
}
Widget _buildOTPFields() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(5, (index) {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(6, (index) {
return SizedBox(
width: 60,
width: 50,
child: TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
@ -81,14 +58,16 @@ class _OTPVerificationPageState extends State<OTPVerificationPage> {
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: AppColors.borderPrimary),
borderSide: BorderSide(color: AppColors.borderPrimary),
),
),
onChanged: (value) {
if (value.length == 1 && index < 4) {
if (value.length == 1 && index < 5) {
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) {
final height = MediaQuery.of(context).size.height;
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthSuccess) {
_otpTimer.initializeFromExpiry(expiryTime: state.timeDue);
}
},
child: Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
IconButton(
icon: SvgPicture.asset(Assets.icons.back.path),
onPressed: () => Navigator.pop(context),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
icon: SvgPicture.asset(Assets.icons.back.path),
onPressed: () => Navigator.pop(context),
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),
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(
children: [
SvgPicture.asset(Assets.icons.star.path,
width: 16, color: AppColors.textSecondary),
width: 16,),
const SizedBox(width: 4),
Text(
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:lba/gen/assets.gen.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 {
showDialog(
@ -39,10 +41,7 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
curve: Curves.elasticOut,
);
_fadeAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
);
_fadeAnimation = CurvedAnimation(parent: _controller, curve: Curves.easeIn);
_controller.forward();
}
@ -60,8 +59,9 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
child: ScaleTransition(
scale: _scaleAnimation,
child: Dialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
elevation: 10,
backgroundColor: AppColors.surface,
child: Stack(
@ -69,8 +69,12 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
alignment: Alignment.topCenter,
children: [
Padding(
padding: const EdgeInsets.only(
top: 50, left: 20, right: 20, bottom: 20),
padding: EdgeInsets.only(
top: 50,
left: 20,
right: 20,
bottom: 20,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@ -109,12 +113,20 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: AppColors.offerTimer),
),
padding:
const EdgeInsets.symmetric(vertical: 12),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () {
// TODO: Add actual logout logic here
Navigator.of(context).pop();
onPressed: () async {
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(
"Log Out",
@ -132,8 +144,7 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
borderRadius: BorderRadius.circular(8),
),
side: BorderSide(color: AppColors.greyBorder),
padding:
const EdgeInsets.symmetric(vertical: 12),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: () => Navigator.of(context).pop(),
child: const Text("Cancel"),
@ -163,7 +174,9 @@ class _AnimatedLogoutDialogState extends State<_AnimatedLogoutDialog>
radius: 40,
child: Padding(
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);
if (closeTime <= openTime) {
// شیفت شبانه (مثلاً 22:00 تا 02:00)
return nowTime >= openTime || nowTime <= closeTime;
}