diff --git a/lib/app.dart b/lib/app.dart index 244ea36..e6cb315 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -37,4 +37,3 @@ class MyApp extends StatelessWidget { ); } } -// sadadda diff --git a/lib/app_view.dart b/lib/app_view.dart index f8b4763..ca9ab09 100644 --- a/lib/app_view.dart +++ b/lib/app_view.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:klimatologiot/blocs/authentication_bloc/authentication_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:klimatologiot/screens/auth/views/welcome_screen.dart'; -import 'package:klimatologiot/screens/home/views/home_screen.dart'; +import 'screens/splash/views/splash_screen.dart'; class MyAppView extends StatelessWidget { const MyAppView({super.key}); @@ -11,32 +8,25 @@ class MyAppView extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Klimatologi', - debugShowCheckedModeBanner: false, - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('id', 'ID'), - Locale('en'), - ], - theme: ThemeData( - colorScheme: ColorScheme.light( - surface: Colors.grey.shade100, - onSurface: Colors.black, - primary: Colors.blue, - onPrimary: Colors.white), - ), - home: BlocBuilder( - builder: ((context, state) { - if (state.status == AuthenticationStatus.authenticated) { - return HomeScreen(); - } else { - return WelcomeScreen(); - } - }), - )); + title: 'Klimatologi', + debugShowCheckedModeBanner: false, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('id', 'ID'), + Locale('en'), + ], + theme: ThemeData( + colorScheme: ColorScheme.light( + surface: Colors.grey.shade100, + onSurface: Colors.black, + primary: Colors.blue, + onPrimary: Colors.white), + ), + home: const SplashScreen(), + ); } } diff --git a/lib/main.dart b/lib/main.dart index da80b35..bd3e7d5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,6 @@ void main() async { options: DefaultFirebaseOptions.currentPlatform, ); await initializeDateFormatting('id_ID', null); - await Future.delayed(const Duration(seconds: 2)); runApp(MyApp( FirebaseUserRepo(), FirebaseMonitoringRepo(), diff --git a/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart b/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart index aa0fbfb..4125431 100644 --- a/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart +++ b/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart @@ -28,7 +28,5 @@ class SignInBloc extends Bloc { emit(SignInFailure()); } }); - - on((event, emit) async => await _userRepository.logOut()); } } diff --git a/lib/screens/auth/blocs/sign_up_bloc/sign_up_bloc.dart b/lib/screens/auth/blocs/sign_up_bloc/sign_up_bloc.dart index 647c241..f142840 100644 --- a/lib/screens/auth/blocs/sign_up_bloc/sign_up_bloc.dart +++ b/lib/screens/auth/blocs/sign_up_bloc/sign_up_bloc.dart @@ -1,6 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:user_repository/user_repository.dart'; +import 'package:firebase_auth/firebase_auth.dart'; part 'sign_up_event.dart'; part 'sign_up_state.dart'; @@ -12,13 +13,27 @@ class SignUpBloc extends Bloc { on((event, emit) async { emit(SignUpProcess()); try { - MyUser myUser = - await _userRepository.signUp(event.user, event.password); - await _userRepository.setUserData(myUser); + await _userRepository.signUp(event.user, event.password); emit(SignUpSuccess()); } catch (e) { - emit(SignUpFailure(e.toString())); + String message = 'Terjadi kesalahan. Coba lagi.'; + if (e is FirebaseAuthException) { + switch (e.code) { + case 'email-already-in-use': + message = 'Email sudah terdaftar.'; + break; + case 'weak-password': + message = 'Password terlalu lemah.'; + break; + case 'invalid-email': + message = 'Format email tidak valid.'; + break; + } + } + emit(SignUpFailure(message)); } }); } } + +// 2 / 5 diff --git a/lib/screens/auth/views/sign_in_screen.dart b/lib/screens/auth/views/sign_in_screen.dart index b4f00f1..ac4a0cd 100644 --- a/lib/screens/auth/views/sign_in_screen.dart +++ b/lib/screens/auth/views/sign_in_screen.dart @@ -39,12 +39,6 @@ class _SignInScreenState extends State { if (state is SignInFailure) { setState(() => _errorMsg = 'Email atau password salah. Coba lagi.'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(_errorMsg!), - backgroundColor: Colors.red, - ), - ); } else if (state is SignInSuccess) { setState(() => _errorMsg = null); } @@ -159,7 +153,7 @@ class _SignInScreenState extends State { ), const SizedBox(height: 16), - // Tombol Google — hanya muncul ketika tidak loading + // // Tombol Google — hanya muncul ketika tidak loading SizedBox( width: MediaQuery.of(context).size.width * 0.5, child: TextButton.icon( diff --git a/lib/screens/auth/views/welcome_screen.dart b/lib/screens/auth/views/welcome_screen.dart index 5c11126..d090d1c 100644 --- a/lib/screens/auth/views/welcome_screen.dart +++ b/lib/screens/auth/views/welcome_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:user_repository/user_repository.dart'; +import 'package:klimatologiot/blocs/authentication_bloc/authentication_bloc.dart'; // ← tambah +import 'package:klimatologiot/screens/home/views/home_screen.dart'; import '../blocs/sign_in_bloc/sign_in_bloc.dart'; import '../blocs/sign_up_bloc/sign_up_bloc.dart'; @@ -32,54 +34,65 @@ class _WelcomeScreenState extends State @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: Column( - children: [ - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 50.0), - child: TabBar( - controller: tabController, - unselectedLabelColor: - Theme.of(context).colorScheme.onSurface.withOpacity(0.5), - labelColor: Theme.of(context).colorScheme.onSurface, - tabs: const [ - Padding( - padding: EdgeInsets.all(12.0), - child: Text('Sign In', style: TextStyle(fontSize: 18)), - ), - Padding( - padding: EdgeInsets.all(12.0), - child: Text('Sign Up', style: TextStyle(fontSize: 18)), - ), - ], - ), - ), - // FIX: Expanded supaya TabBarView mengisi sisa tinggi layar - // dengan ini scroll di dalam tab bisa berjalan normal - Expanded( - child: TabBarView( - controller: tabController, - children: [ - BlocProvider( - create: (context) => - // FIX: akses UserRepository langsung, bukan via AuthBloc - SignInBloc(context.read()), - child: const SignInScreen(), - ), - BlocProvider( - create: (context) => - SignUpBloc(context.read()), - child: SignUpScreen( - onSignInTap: () => tabController.animateTo(0), + return BlocListener( + listener: (context, state) { + if (state.status == AuthenticationStatus.authenticated) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => HomeScreen()), + (route) => false, // hapus semua route lama (termasuk SplashScreen) + ); + } + }, + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 50.0), + child: TabBar( + controller: tabController, + unselectedLabelColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.5), + labelColor: Theme.of(context).colorScheme.onSurface, + tabs: const [ + Padding( + padding: EdgeInsets.all(12.0), + child: Text('Sign In', style: TextStyle(fontSize: 18)), ), - ), - ], + Padding( + padding: EdgeInsets.all(12.0), + child: Text('Sign Up', style: TextStyle(fontSize: 18)), + ), + ], + ), ), - ), - ], + // FIX: Expanded supaya TabBarView mengisi sisa tinggi layar + // dengan ini scroll di dalam tab bisa berjalan normal + Expanded( + child: TabBarView( + controller: tabController, + children: [ + BlocProvider( + create: (context) => + // FIX: akses UserRepository langsung, bukan via AuthBloc + SignInBloc(context.read()), + child: const SignInScreen(), + ), + BlocProvider( + create: (context) => + SignUpBloc(context.read()), + child: SignUpScreen( + onSignInTap: () => tabController.animateTo(0), + ), + ), + ], + ), + ), + ], + ), ), ), ); diff --git a/lib/screens/splash/views/splash_screen.dart b/lib/screens/splash/views/splash_screen.dart index 8f514fe..fa3b8a0 100644 --- a/lib/screens/splash/views/splash_screen.dart +++ b/lib/screens/splash/views/splash_screen.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; - -import '../../auth/views/welcome_screen.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:klimatologiot/blocs/authentication_bloc/authentication_bloc.dart'; +import 'package:klimatologiot/screens/auth/views/welcome_screen.dart'; +import 'package:klimatologiot/screens/home/views/home_screen.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -15,15 +17,36 @@ class _SplashScreenState extends State { @override void initState() { super.initState(); + _navigateAfterSplash(); + } - Timer(const Duration(seconds: 3), () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (_) => const WelcomeScreen(), - ), - ); - }); + Future _navigateAfterSplash() async { + // Tunggu minimal 3 detik (efek splash) + await Future.delayed(const Duration(seconds: 3)); + if (!mounted) return; + + final bloc = context.read(); + + // Kalau auth state masih unknown (Firebase belum resolve), tunggu dulu + AuthenticationState state = bloc.state; + if (state.status == AuthenticationStatus.unknown) { + state = await bloc.stream + .firstWhere((s) => s.status != AuthenticationStatus.unknown) + .timeout( + const Duration(seconds: 5), + onTimeout: () => const AuthenticationState.unauthenticated(), + ); + } + if (!mounted) return; + + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => state.status == AuthenticationStatus.authenticated + ? HomeScreen() + : WelcomeScreen(), + ), + ); } @override @@ -35,7 +58,7 @@ class _SplashScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( - 'assets/images/logo.png', + 'images/logo_klimatologi.png', width: 140, ), const SizedBox(height: 24), diff --git a/packages/user_repository/lib/src/entities/user_entity.dart b/packages/user_repository/lib/src/entities/user_entity.dart index a66e14a..1599680 100644 --- a/packages/user_repository/lib/src/entities/user_entity.dart +++ b/packages/user_repository/lib/src/entities/user_entity.dart @@ -1,16 +1,14 @@ - class MyUserEntity { - String userId; - String email; - String name; - bool hasActiveCart; + final String userId; + final String email; + final String name; + final bool hasActiveCart; - MyUserEntity ({ + MyUserEntity({ required this.userId, required this.email, required this.name, required this.hasActiveCart, - }); Map toDocument() { @@ -19,17 +17,17 @@ class MyUserEntity { 'email': email, 'name': name, 'hasActiveCart': hasActiveCart, - }; } static MyUserEntity fromDocument(Map doc) { return MyUserEntity( - userId: doc['userId'] as String, - email: doc['email'] as String, - name: doc['name'] as String, - hasActiveCart: doc['hasActiveCart'] as bool, + userId: doc['userId'] as String? ?? '', + email: doc['email'] as String? ?? '', + name: doc['name'] as String? ?? '', + hasActiveCart: doc['hasActiveCart'] as bool? ?? false, ); } +} -} \ No newline at end of file +// 3 diff --git a/packages/user_repository/lib/src/firebase_user_repo.dart b/packages/user_repository/lib/src/firebase_user_repo.dart index 004c865..c5a14ab 100644 --- a/packages/user_repository/lib/src/firebase_user_repo.dart +++ b/packages/user_repository/lib/src/firebase_user_repo.dart @@ -3,14 +3,11 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:rxdart/rxdart.dart'; import 'package:user_repository/user_repository.dart'; -// Google Sign-In import di-disable sementara karena error analisis: -// "Target of URI doesn't exist: package:google_sign_in/google_sign_in.dart". -// Implementasi Google Sign-In (kecuali untuk web auth) akan dilewatkan. -import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:google_sign_in/google_sign_in.dart'; class FirebaseUserRepo implements UserRepository { - final FirebaseAuth _firebaseAuth; + final _googleSignIn = GoogleSignIn(); final userCollection = FirebaseFirestore.instance.collection('users'); FirebaseUserRepo({FirebaseAuth? firebaseAuth}) @@ -29,7 +26,12 @@ class FirebaseUserRepo implements UserRepository { MyUserEntity.fromDocument(userData.data()!)); } else { // Data Firestore belum ada (seharusnya tidak terjadi dengan flow yang sudah diperbaiki) - yield MyUser.empty; + yield MyUser( + userId: firebaseUser.uid, + email: firebaseUser.email ?? '', + name: firebaseUser.displayName ?? '', + hasActiveCart: false, + ); } } catch (e) { log('Error fetching user data: $e'); @@ -53,27 +55,29 @@ class FirebaseUserRepo implements UserRepository { @override Future signUp(MyUser myUser, String password) async { try { - UserCredential user = await _firebaseAuth.createUserWithEmailAndPassword( - email: myUser.email, password: password); - myUser.userId = user.user!.uid; - return myUser; + UserCredential credential = + await _firebaseAuth.createUserWithEmailAndPassword( + email: myUser.email, password: password); + final newUser = MyUser( + userId: credential.user!.uid, + email: myUser.email, + name: myUser.name, + hasActiveCart: false, + ); + await setUserData(newUser); + return newUser; } catch (e) { log(e.toString()); rethrow; } } - // NOTE: Google Sign-In disabled because dependency import fails on this workspace. - // After google_sign_in issue resolved, restore the implementation. - @override - Future logOut() async { - // GoogleSignIn sementara di-skip karena error analisis import. + await _googleSignIn.signOut(); await _firebaseAuth.signOut(); } - @override Future setUserData(MyUser myUser) async { try { @@ -88,9 +92,34 @@ class FirebaseUserRepo implements UserRepository { @override Future signInWithGoogle() async { - // Google Sign-In sementara dimatikan agar build tidak gagal. - // Setelah dependency google_sign_in benar-benar resolvable, bagian ini bisa dikembalikan. - throw UnimplementedError('Google Sign-In disabled (analysis fix)'); - } + try { + final googleUser = await _googleSignIn.signIn(); + if (googleUser == null) return; // user cancel + final googleAuth = await googleUser.authentication; + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + + final userCredential = + await _firebaseAuth.signInWithCredential(credential); + final firebaseUser = userCredential.user!; + + final doc = await userCollection.doc(firebaseUser.uid).get(); + if (!doc.exists) { + // User baru via Google — buat doc Firestore + final myUser = MyUser( + userId: firebaseUser.uid, + email: firebaseUser.email ?? '', + name: firebaseUser.displayName ?? '', + hasActiveCart: false, + ); + await setUserData(myUser); + } + } catch (e) { + log(e.toString()); + rethrow; + } + } } diff --git a/packages/user_repository/lib/src/models/user.dart b/packages/user_repository/lib/src/models/user.dart index e867c7a..ba57c8b 100644 --- a/packages/user_repository/lib/src/models/user.dart +++ b/packages/user_repository/lib/src/models/user.dart @@ -1,25 +1,43 @@ import '../entities/entities.dart'; class MyUser { - String userId; - String email; - String name; - bool hasActiveCart; + final String userId; // ← final semua + final String email; + final String name; + final bool hasActiveCart; - MyUser({ + const MyUser({ required this.userId, required this.email, required this.name, required this.hasActiveCart, }); - static final empty = MyUser( + static const empty = MyUser( userId: '', email: '', name: '', hasActiveCart: false, ); + bool get isEmpty => this == empty; + bool get isNotEmpty => this != empty; + + // ← tambah copyWith agar bisa "ubah" tanpa mutasi + MyUser copyWith({ + String? userId, + String? email, + String? name, + bool? hasActiveCart, + }) { + return MyUser( + userId: userId ?? this.userId, + email: email ?? this.email, + name: name ?? this.name, + hasActiveCart: hasActiveCart ?? this.hasActiveCart, + ); + } + MyUserEntity toEntity() { return MyUserEntity( userId: userId,