Merge branch 'Nadif_version' into branch_percobaan
This commit is contained in:
commit
7afab0e67b
|
|
@ -15,21 +15,19 @@ class MyAppView extends StatelessWidget {
|
|||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.light(
|
||||
background: Colors.grey.shade100,
|
||||
onBackground: Colors.black,
|
||||
surface: Colors.grey.shade100,
|
||||
onSurface: Colors.black,
|
||||
primary: Colors.blue,
|
||||
onPrimary: Colors.white
|
||||
),
|
||||
onPrimary: Colors.white),
|
||||
),
|
||||
home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
|
||||
builder: ((context, state){
|
||||
if(state.status == AuthenticationStatus.authenticated){
|
||||
builder: ((context, state) {
|
||||
if (state.status == AuthenticationStatus.authenticated) {
|
||||
return HomeScreen();
|
||||
} else {
|
||||
return WelcomeScreen();
|
||||
}
|
||||
}),
|
||||
)
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -7,20 +7,19 @@ import 'package:user_repository/user_repository.dart';
|
|||
part 'authentication_event.dart';
|
||||
part 'authentication_state.dart';
|
||||
|
||||
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> {
|
||||
class AuthenticationBloc
|
||||
extends Bloc<AuthenticationEvent, AuthenticationState> {
|
||||
final UserRepository userRepository;
|
||||
late final StreamSubscription<MyUser?> _userSubscription;
|
||||
|
||||
AuthenticationBloc({
|
||||
required this.userRepository
|
||||
}) : super(const AuthenticationState.unknown()) {
|
||||
AuthenticationBloc({required this.userRepository})
|
||||
: super(const AuthenticationState.unknown()) {
|
||||
_userSubscription = userRepository.user.listen((user) {
|
||||
add(AuthenticationUserChanged(user));
|
||||
});
|
||||
|
||||
|
||||
on<AuthenticationUserChanged>((event, emit) {
|
||||
if(event.user != MyUser.empty) {
|
||||
if (event.user != null && event.user != MyUser.empty) {
|
||||
emit(AuthenticationState.authenticated(event.user!));
|
||||
} else {
|
||||
emit(AuthenticationState.unauthenticated());
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
|||
emit(SignInProcess());
|
||||
try {
|
||||
await _userRepository.signIn(event.email, event.password);
|
||||
emit(SignInSuccess());
|
||||
} catch (e) {
|
||||
emit(SignInFailure());
|
||||
}
|
||||
|
|
@ -25,7 +26,6 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
|
|||
emit(SignInSuccess());
|
||||
} catch (e) {
|
||||
emit(SignInFailure());
|
||||
emit(SignInInitial());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,31 +21,44 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
bool obscurePassword = true;
|
||||
String? _errorMsg;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<SignInBloc, SignInState>(
|
||||
listener: (context, state) {
|
||||
if (state is SignInProcess) {
|
||||
setState(() {
|
||||
signInRequired = true;
|
||||
});
|
||||
setState(() => signInRequired = true);
|
||||
} else {
|
||||
setState(() {
|
||||
signInRequired = false;
|
||||
});
|
||||
setState(() => signInRequired = false);
|
||||
|
||||
if (state is SignInFailure) {
|
||||
setState(() {
|
||||
// PERUBAHAN: Pesan error lebih umum karena Google gagal belum tentu soal password
|
||||
_errorMsg = 'Sign in failed. Please try again.';
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Email field
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
|
|
@ -54,14 +67,22 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
obscureText: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
prefixIcon: const Icon(CupertinoIcons.mail_solid),
|
||||
errorMsg: _errorMsg,
|
||||
// FIX: tidak lagi pass errorMsg ke field individual
|
||||
validator: (val) {
|
||||
if (val!.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Mohon isi field ini';
|
||||
}
|
||||
// FIX: tambah validasi format email
|
||||
if (!RegExp(r'^[\w.-]+@[\w.-]+\.\w{2,}$').hasMatch(val)) {
|
||||
return 'Format email tidak valid';
|
||||
}
|
||||
return null;
|
||||
})),
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Password field
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
|
|
@ -70,10 +91,9 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
obscureText: obscurePassword,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||
errorMsg: _errorMsg,
|
||||
validator: (val) {
|
||||
if (val!.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Mohon isi field ini';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
|
@ -81,39 +101,51 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
onPressed: () {
|
||||
setState(() {
|
||||
obscurePassword = !obscurePassword;
|
||||
if (obscurePassword) {
|
||||
iconPassword = CupertinoIcons.eye_fill;
|
||||
} else {
|
||||
iconPassword = CupertinoIcons.eye_slash_fill;
|
||||
}
|
||||
iconPassword = obscurePassword
|
||||
? CupertinoIcons.eye_fill
|
||||
: CupertinoIcons.eye_slash_fill;
|
||||
});
|
||||
},
|
||||
icon: Icon(iconPassword),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// FIX: Error message satu baris di bawah kedua field
|
||||
if (_errorMsg != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: Text(
|
||||
_errorMsg!,
|
||||
style: const TextStyle(color: Colors.red, fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 20),
|
||||
!signInRequired
|
||||
? SizedBox(
|
||||
|
||||
// FIX: Tombol Sign In dan Google dinonaktifkan saat loading
|
||||
if (!signInRequired) ...[
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
context.read<SignInBloc>().add(SignInRequired(
|
||||
emailController.text,
|
||||
passwordController.text));
|
||||
emailController.text, passwordController.text));
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
elevation: 3.0,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(60))),
|
||||
borderRadius: BorderRadius.circular(60)),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 25, vertical: 5),
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 25, vertical: 5),
|
||||
child: Text(
|
||||
'Sign In',
|
||||
textAlign: TextAlign.center,
|
||||
|
|
@ -122,17 +154,16 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
)),
|
||||
)
|
||||
: const CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
/// ======= Tombol Login Google ======= ///
|
||||
const SizedBox(height: 20),
|
||||
// Tombol Google — hanya muncul ketika tidak loading
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
// == Mengirim event login Google ke Bloc == //
|
||||
context.read<SignInBloc>().add(GoogleSignInRequired());
|
||||
},
|
||||
icon: SizedBox(
|
||||
|
|
@ -155,11 +186,19 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
side: const BorderSide(color: Colors.grey))),
|
||||
side: const BorderSide(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else
|
||||
const CircularProgressIndicator(),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:user_repository/user_repository.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:user_repository/user_repository.dart';
|
||||
|
||||
import '../../../components/my_text_field.dart';
|
||||
import '../blocs/sign_up_bloc/sign_up_bloc.dart' as signUp;
|
||||
// import '../../../blocs/authentication_bloc/authentication_bloc.dart';
|
||||
import '../blocs/sign_in_bloc/sign_in_bloc.dart' as signIn;
|
||||
import '../blocs/sign_up_bloc/sign_up_bloc.dart';
|
||||
|
||||
class SignUpScreen extends StatefulWidget {
|
||||
final VoidCallback? onSignInTap;
|
||||
|
|
@ -24,86 +23,60 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||
IconData iconPassword = CupertinoIcons.eye_fill;
|
||||
bool obscurePassword = true;
|
||||
bool signUpRequired = false;
|
||||
// FIX: Hapus 5 variabel password strength yang tidak dipakai
|
||||
|
||||
bool containsUpperCase = false;
|
||||
bool containsLowerCase = false;
|
||||
bool containsNumber = false;
|
||||
bool containsSpecialChar = false;
|
||||
bool contains8Length = false;
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<signUp.SignUpBloc, signUp.SignUpState>(
|
||||
return BlocListener<SignUpBloc, SignUpState>(
|
||||
listener: (context, state) {
|
||||
if (state is signUp.SignUpProcess) {
|
||||
setState(() {
|
||||
signUpRequired = true;
|
||||
});
|
||||
} else if (state is signUp.SignUpFailure) {
|
||||
setState(() {
|
||||
signUpRequired = false; // Matikan loading!
|
||||
});
|
||||
// Tampilkan pesan error lewat Snackbar biar user tahu kenapa gagal
|
||||
if (state is SignUpProcess) {
|
||||
setState(() => signUpRequired = true);
|
||||
} else if (state is SignUpFailure) {
|
||||
setState(() => signUpRequired = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message ?? 'An error ocurred'),
|
||||
backgroundColor: Colors.red),
|
||||
content: Text(state.message ?? 'Terjadi kesalahan'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
} else if (state is signUp.SignUpSuccess) {
|
||||
setState(() {
|
||||
signUpRequired = false;
|
||||
});
|
||||
} else if (state is SignUpSuccess) {
|
||||
setState(() => signUpRequired = false);
|
||||
}
|
||||
},
|
||||
// FIX: Hapus Scaffold — WelcomeScreen sudah punya Scaffold
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.92,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 22),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.04),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Buat Akun',
|
||||
style: TextStyle(
|
||||
fontSize: 22, fontWeight: FontWeight.w700),
|
||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Center(
|
||||
child: Text(
|
||||
'Daftar untuk mulai monitoring',
|
||||
style:
|
||||
TextStyle(fontSize: 13, color: Colors.black54),
|
||||
style: TextStyle(fontSize: 13, color: Colors.black54),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Name
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
// Nama
|
||||
MyTextField(
|
||||
controller: nameController,
|
||||
hintText: 'Masukkan nama lengkap',
|
||||
obscureText: false,
|
||||
|
|
@ -111,20 +84,16 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||
prefixIcon: const Icon(CupertinoIcons.person_fill),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
} else if (val.length > 50) {
|
||||
return 'Name too long';
|
||||
return 'Mohon isi field ini';
|
||||
}
|
||||
if (val.length > 50) return 'Nama terlalu panjang';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Email
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
MyTextField(
|
||||
controller: emailController,
|
||||
hintText: 'nama@email.com',
|
||||
obscureText: false,
|
||||
|
|
@ -132,18 +101,19 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||
prefixIcon: const Icon(CupertinoIcons.mail_solid),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
return 'Mohon isi field ini';
|
||||
}
|
||||
// FIX: tambah validasi format email
|
||||
if (!RegExp(r'^[\w.-]+@[\w.-]+\.\w{2,}$').hasMatch(val)) {
|
||||
return 'Format email tidak valid';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Password
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
MyTextField(
|
||||
controller: passwordController,
|
||||
hintText: 'Minimal 6 karakter',
|
||||
obscureText: obscurePassword,
|
||||
|
|
@ -162,18 +132,19 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||
),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
return 'Mohon isi field ini';
|
||||
}
|
||||
// FIX: tambah validasi panjang minimum
|
||||
if (val.length < 6) {
|
||||
return 'Password minimal 6 karakter';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Confirm Password
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
child: MyTextField(
|
||||
MyTextField(
|
||||
controller: confirmPasswordController,
|
||||
hintText: 'Ulangi password',
|
||||
obscureText: obscurePassword,
|
||||
|
|
@ -181,42 +152,41 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||
prefixIcon: const Icon(CupertinoIcons.lock_fill),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Please fill in this field';
|
||||
} else if (val != passwordController.text) {
|
||||
return 'Mohon isi field ini';
|
||||
}
|
||||
if (val != passwordController.text) {
|
||||
return 'Password tidak cocok';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 18),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
/// == Sign Up == ///
|
||||
// Button
|
||||
!signUpRequired
|
||||
? Center(
|
||||
child: SizedBox(
|
||||
width:
|
||||
MediaQuery.of(context).size.width * 0.5,
|
||||
// Tombol Sign Up
|
||||
Center(
|
||||
child: !signUpRequired
|
||||
? SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
MyUser myUser = MyUser.empty;
|
||||
myUser.email = emailController.text;
|
||||
myUser.name = nameController.text;
|
||||
|
||||
setState(() {
|
||||
context.read<signUp.SignUpBloc>().add(
|
||||
signUp.SignUpRequired(
|
||||
myUser,
|
||||
passwordController.text,
|
||||
),
|
||||
// FIX: buat MyUser langsung, jangan mutasi MyUser.empty
|
||||
final myUser = MyUser(
|
||||
userId: '',
|
||||
email: emailController.text.trim(),
|
||||
name: nameController.text.trim(),
|
||||
hasActiveCart: false,
|
||||
);
|
||||
|
||||
// FIX: add() dipanggil di luar setState
|
||||
context.read<SignUpBloc>().add(
|
||||
SignUpRequired(
|
||||
myUser, passwordController.text),
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
|
|
@ -224,54 +194,39 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||
borderRadius: BorderRadius.circular(60),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 25, vertical: 5),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Sign Up',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const CircularProgressIndicator(),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Text('Atau'),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Row(
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Sudah punya akun? '),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
widget.onSignInTap?.call();
|
||||
},
|
||||
onPressed: widget.onSignInTap,
|
||||
child: const Text(
|
||||
'Login di sini',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
|
||||
import 'package:user_repository/user_repository.dart';
|
||||
|
||||
import '../blocs/sign_in_bloc/sign_in_bloc.dart';
|
||||
import '../blocs/sign_up_bloc/sign_up_bloc.dart';
|
||||
import 'sign_in_screen.dart';
|
||||
|
|
@ -19,86 +20,68 @@ class _WelcomeScreenState extends State<WelcomeScreen>
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
tabController = TabController(initialIndex: 0, length: 2, vsync: this);
|
||||
super.initState();
|
||||
tabController = TabController(initialIndex: 0, length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tabController.dispose(); // FIX: dispose controller agar tidak memory leak
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height / 1.8,
|
||||
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
|
||||
.onBackground
|
||||
.withOpacity(0.5),
|
||||
labelColor:
|
||||
Theme.of(context).colorScheme.onBackground,
|
||||
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,
|
||||
),
|
||||
),
|
||||
child: Text('Sign In', style: TextStyle(fontSize: 18)),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
'Sign Up',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
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<SignInBloc>(
|
||||
create: (context) => SignInBloc(context
|
||||
.read<AuthenticationBloc>()
|
||||
.userRepository),
|
||||
create: (context) =>
|
||||
// FIX: akses UserRepository langsung, bukan via AuthBloc
|
||||
SignInBloc(context.read<UserRepository>()),
|
||||
child: const SignInScreen(),
|
||||
),
|
||||
BlocProvider<SignUpBloc>(
|
||||
create: (context) => SignUpBloc(context
|
||||
.read<AuthenticationBloc>()
|
||||
.userRepository),
|
||||
child: SignUpScreen(onSignInTap: () {
|
||||
tabController.animateTo(
|
||||
0); // Ini yang bikin dia pindah ke tab Login
|
||||
}),
|
||||
create: (context) =>
|
||||
SignUpBloc(context.read<UserRepository>()),
|
||||
child: SignUpScreen(
|
||||
onSignInTap: () => tabController.animateTo(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,11 @@ class Evaporasi {
|
|||
);
|
||||
|
||||
factory Evaporasi.fromJson(Map<dynamic, dynamic> json) {
|
||||
final now = DateTime.now();
|
||||
DateTime timestamp = now;
|
||||
// ✅ Handle perbedaan field name antara history dan realtime:
|
||||
// History path : suhu, tinggi, evaporasi, waktu
|
||||
// Realtime path : suhu_air, tinggi_air, evaporasi, waktu, status
|
||||
final suhu = (json['suhu'] ?? json['suhu_air'] ?? 0).toDouble();
|
||||
final tinggi = (json['tinggi'] ?? json['tinggi_air_cm'] ?? 0).toDouble();
|
||||
|
||||
// ✅ Prioritas waktu sesuai firmware ESP32:
|
||||
// 1) timestamp (Unix atau ISO string, kalau pernah dikirim)
|
||||
|
|
@ -58,7 +61,14 @@ class Evaporasi {
|
|||
final jam = int.tryParse(parts[0]) ?? 0;
|
||||
final menit = int.tryParse(parts[1]) ?? 0;
|
||||
final detik = parts.length >= 3 ? (int.tryParse(parts[2]) ?? 0) : 0;
|
||||
timestamp = DateTime(now.year, now.month, now.day, jam, menit, detik);
|
||||
timestamp = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
jam,
|
||||
menit,
|
||||
detik,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -80,10 +90,11 @@ class Evaporasi {
|
|||
}
|
||||
|
||||
return Evaporasi(
|
||||
evaporasi: toDoubleSafe(evaporasiVal),
|
||||
suhu: toDoubleSafe(suhuVal),
|
||||
tinggiAir: toDoubleSafe(tinggiVal),
|
||||
timestamp: timestamp,
|
||||
evaporasi: (json['evaporasi_mm'] ?? 0).toDouble(),
|
||||
suhu: suhu,
|
||||
tinggiAir: tinggi,
|
||||
status: json['status'] ?? '',
|
||||
timestamp: TimestampParser.parse(rawTime), // ✅ pakai TimestampParser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,9 @@ class FirebaseUserRepo implements UserRepository {
|
|||
final FirebaseAuth _firebaseAuth;
|
||||
final userCollection = FirebaseFirestore.instance.collection('users');
|
||||
|
||||
FirebaseUserRepo({
|
||||
FirebaseAuth? firebaseAuth,
|
||||
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
|
||||
FirebaseUserRepo({FirebaseAuth? firebaseAuth})
|
||||
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
|
||||
|
||||
/// == Melakukan Implement User == ///
|
||||
@override
|
||||
Stream<MyUser?> get user {
|
||||
return _firebaseAuth.authStateChanges().flatMap((firebaseUser) async* {
|
||||
|
|
@ -27,16 +25,9 @@ class FirebaseUserRepo implements UserRepository {
|
|||
yield MyUser.fromEntity(
|
||||
MyUserEntity.fromDocument(userData.data()!));
|
||||
} else {
|
||||
// User baru yang belum ada data di Firestore, tunggu sebentar
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
final retryData = await userCollection.doc(firebaseUser.uid).get();
|
||||
if (retryData.exists && retryData.data() != null) {
|
||||
yield MyUser.fromEntity(
|
||||
MyUserEntity.fromDocument(retryData.data()!));
|
||||
} else {
|
||||
// Data Firestore belum ada (seharusnya tidak terjadi dengan flow yang sudah diperbaiki)
|
||||
yield MyUser.empty;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log('Error fetching user data: $e');
|
||||
yield MyUser.empty;
|
||||
|
|
@ -45,7 +36,6 @@ class FirebaseUserRepo implements UserRepository {
|
|||
});
|
||||
}
|
||||
|
||||
/// == Melakukan Implement Sign in == ///
|
||||
@override
|
||||
Future<void> signIn(String email, String password) async {
|
||||
try {
|
||||
|
|
@ -57,7 +47,6 @@ class FirebaseUserRepo implements UserRepository {
|
|||
}
|
||||
}
|
||||
|
||||
/// == Melakukan Implement Sign Up == ///
|
||||
@override
|
||||
Future<MyUser> signUp(MyUser myUser, String password) async {
|
||||
try {
|
||||
|
|
@ -71,7 +60,6 @@ class FirebaseUserRepo implements UserRepository {
|
|||
}
|
||||
}
|
||||
|
||||
/// == Melakukan Implement Log Out == ///
|
||||
@override
|
||||
Future<void> logOut() async {
|
||||
final GoogleSignIn googleSignIn = GoogleSignIn();
|
||||
|
|
@ -85,27 +73,26 @@ class FirebaseUserRepo implements UserRepository {
|
|||
await userCollection
|
||||
.doc(myUser.userId)
|
||||
.set(myUser.toEntity().toDocument());
|
||||
// Tunggu sebentar agar data tersimpan dengan baik sebelum stream update
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// == Sign in with Google == ///
|
||||
@override
|
||||
Future<void> signInWithGoogle() async {
|
||||
try {
|
||||
UserCredential userCredential;
|
||||
|
||||
if (kIsWeb) {
|
||||
final googleProvider = GoogleAuthProvider();
|
||||
await _firebaseAuth.signInWithPopup(googleProvider);
|
||||
userCredential = await _firebaseAuth.signInWithPopup(googleProvider);
|
||||
} else {
|
||||
final GoogleSignIn googleSignIn = GoogleSignIn();
|
||||
final googleUser = await googleSignIn.signIn();
|
||||
|
||||
if (googleUser == null) {
|
||||
throw Exception("cancelled");
|
||||
throw Exception('cancelled');
|
||||
}
|
||||
|
||||
final GoogleSignInAuthentication googleAuth =
|
||||
|
|
@ -115,7 +102,22 @@ class FirebaseUserRepo implements UserRepository {
|
|||
accessToken: googleAuth.accessToken,
|
||||
idToken: googleAuth.idToken,
|
||||
);
|
||||
await _firebaseAuth.signInWithCredential(credential);
|
||||
userCredential = await _firebaseAuth.signInWithCredential(credential);
|
||||
}
|
||||
|
||||
// FIX: Cek apakah user sudah punya data Firestore
|
||||
// Jika belum (user baru), buat dokumen sekarang juga
|
||||
final firebaseUser = userCredential.user!;
|
||||
final doc = await userCollection.doc(firebaseUser.uid).get();
|
||||
|
||||
if (!doc.exists) {
|
||||
final newUser = MyUser(
|
||||
userId: firebaseUser.uid,
|
||||
email: firebaseUser.email ?? '',
|
||||
name: firebaseUser.displayName ?? '',
|
||||
hasActiveCart: false,
|
||||
);
|
||||
await setUserData(newUser);
|
||||
}
|
||||
} catch (e) {
|
||||
log('Google sign-in error: $e');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import '../entities/entities.dart';
|
||||
|
||||
class MyUser {
|
||||
|
|
@ -7,12 +6,11 @@ class MyUser {
|
|||
String name;
|
||||
bool hasActiveCart;
|
||||
|
||||
MyUser ({
|
||||
MyUser({
|
||||
required this.userId,
|
||||
required this.email,
|
||||
required this.name,
|
||||
required this.hasActiveCart,
|
||||
|
||||
});
|
||||
|
||||
static final empty = MyUser(
|
||||
|
|
@ -40,9 +38,19 @@ class MyUser {
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MyUser &&
|
||||
runtimeType == other.runtimeType &&
|
||||
userId == other.userId &&
|
||||
email == other.email;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(userId, email);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MyUser: $userId, email: $email, name: $name, hasActiveCart: $hasActiveCart}';
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue