oke pembaruan auth ke google

This commit is contained in:
kleponijo 2026-04-05 20:33:12 +07:00
parent 75d9e3220e
commit 499bf7d568
8 changed files with 434 additions and 339 deletions

View File

@ -8,9 +8,7 @@ part 'sign_in_state.dart';
class SignInBloc extends Bloc<SignInEvent, SignInState> {
final UserRepository _userRepository;
SignInBloc(
this._userRepository
) : super(SignInInitial()) {
SignInBloc(this._userRepository) : super(SignInInitial()) {
on<SignInRequired>((event, emit) async {
emit(SignInProcess());
try {
@ -19,7 +17,16 @@ class SignInBloc extends Bloc<SignInEvent, SignInState> {
emit(SignInFailure());
}
});
on<GoogleSignInRequired>((event, emit) async {
emit(SignInProcess());
try {
await _userRepository.signInWithGoogle();
} catch (e) {
emit(SignInFailure());
}
});
on<SignOutRequired>((event, emit) async => await _userRepository.logOut());
}
}

View File

@ -17,6 +17,6 @@ class SignInRequired extends SignInEvent {
List<Object> get props => [email, password];
}
class SignOutRequired extends SignInEvent {
}
class GoogleSignInRequired extends SignInEvent {}
class SignOutRequired extends SignInEvent {}

View File

@ -12,12 +12,23 @@ class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
on<SignUpRequired>((event, emit) async {
emit(SignUpProcess());
try {
MyUser myUser = await _userRepository.signUp(event.user, event.password);
MyUser myUser =
await _userRepository.signUp(event.user, event.password);
await _userRepository.setUserData(myUser);
emit(SignUpSuccess());
} catch (e) {
emit(SignUpFailure(e.toString()));
}
});
on<GoogleSignInRequired>((event, emit) async {
emit(SignUpProcess());
try {
await _userRepository.signInWithGoogle();
emit(SignUpSuccess());
} catch (e) {
emit(SignUpFailure(e.toString()));
}
});
}
}

View File

@ -15,4 +15,6 @@ class SignUpRequired extends SignUpEvent {
@override
List<Object> get props => [user, password];
}
}
class GoogleSignInRequired extends SignUpEvent {}

View File

@ -13,125 +13,149 @@ class SignInScreen extends StatefulWidget {
}
class _SignInScreenState extends State<SignInScreen> {
final passwordController = TextEditingController();
final passwordController = TextEditingController();
final emailController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool signInRequired = false;
IconData iconPassword = CupertinoIcons.eye_fill;
bool obscurePassword = true;
String? _errorMsg;
bool signInRequired = false;
IconData iconPassword = CupertinoIcons.eye_fill;
bool obscurePassword = true;
String? _errorMsg;
@override
Widget build(BuildContext context) {
return BlocListener<SignInBloc, SignInState>(
listener: (context, state) {
if(state is SignInSuccess) {
setState(() {
signInRequired = false;
});
} else if(state is SignInProcess) {
setState(() {
signInRequired = true;
});
} else if(state is SignInFailure) {
setState(() {
signInRequired = false;
_errorMsg = 'Invalid email or password';
});
}
},
child: Form(
key: _formKey,
child: Column(
children: [
const SizedBox(height: 20),
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: emailController,
hintText: 'Email',
obscureText: false,
keyboardType: TextInputType.emailAddress,
prefixIcon: const Icon(CupertinoIcons.mail_solid),
errorMsg: _errorMsg,
validator: (val) {
if (val!.isEmpty) {
return 'Please fill in this field';
}
return null;
}
)
),
const SizedBox(height: 10),
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: passwordController,
hintText: 'Password',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
errorMsg: _errorMsg,
validator: (val) {
if (val!.isEmpty) {
return 'Please fill in this field';
}
return null;
},
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
if(obscurePassword) {
iconPassword = CupertinoIcons.eye_fill;
} else {
iconPassword = CupertinoIcons.eye_slash_fill;
}
});
listener: (context, state) {
if (state is SignInSuccess) {
setState(() {
signInRequired = false;
});
} else if (state is SignInProcess) {
setState(() {
signInRequired = true;
});
} else if (state is SignInFailure) {
setState(() {
signInRequired = false;
_errorMsg = 'Invalid email or password';
});
}
},
child: Form(
key: _formKey,
child: Column(
children: [
const SizedBox(height: 20),
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: emailController,
hintText: 'Email',
obscureText: false,
keyboardType: TextInputType.emailAddress,
prefixIcon: const Icon(CupertinoIcons.mail_solid),
errorMsg: _errorMsg,
validator: (val) {
if (val!.isEmpty) {
return 'Please fill in this field';
}
return null;
})),
const SizedBox(height: 10),
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: passwordController,
hintText: 'Password',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
errorMsg: _errorMsg,
validator: (val) {
if (val!.isEmpty) {
return 'Please fill in this field';
}
return null;
},
icon: Icon(iconPassword),
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
if (obscurePassword) {
iconPassword = CupertinoIcons.eye_fill;
} else {
iconPassword = CupertinoIcons.eye_slash_fill;
}
});
},
icon: Icon(iconPassword),
),
),
),
),
const SizedBox(height: 20),
!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)
);
}
},
style: TextButton.styleFrom(
elevation: 3.0,
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(60)
)
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 25, vertical: 5),
child: Text(
'Sign In',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600
),
),
const SizedBox(height: 20),
!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));
}
},
style: TextButton.styleFrom(
elevation: 3.0,
backgroundColor:
Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(60))),
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 25, vertical: 5),
child: Text(
'Sign In',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600),
),
)),
)
: const CircularProgressIndicator(),
const SizedBox(height: 20),
SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: TextButton.icon(
onPressed: () {
context.read<SignInBloc>().add(GoogleSignInRequired());
},
icon: const Text(
'G',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
)
: const CircularProgressIndicator(),
],
)
),
);
label: const Text(
'Sign In with Google',
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600),
),
style: TextButton.styleFrom(
elevation: 3.0,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(60),
side: const BorderSide(color: Colors.grey))),
),
),
],
)),
);
}
}
}

View File

@ -23,223 +23,266 @@ class _SignUpScreenState extends State<SignUpScreen> {
bool signUpRequired = false;
bool containsUpperCase = false;
bool containsLowerCase = false;
bool containsNumber = false;
bool containsSpecialChar = false;
bool contains8Length = false;
bool containsLowerCase = false;
bool containsNumber = false;
bool containsSpecialChar = false;
bool contains8Length = false;
@override
Widget build(BuildContext context) {
return BlocListener<SignUpBloc, SignUpState>(
listener: (context, state) {
if (state is SignUpSuccess) {
setState(() {
signUpRequired = false;
});
} else if (state is SignUpProcess) {
setState(() {
signUpRequired = true;
});
} else if (state is SignUpFailure) {
return;
}
},
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),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 6),
const Center(
child: Text(
'Buat Akun',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
),
listener: (context, state) {
if (state is SignUpSuccess) {
setState(() {
signUpRequired = false;
});
} else if (state is SignUpProcess) {
setState(() {
signUpRequired = true;
});
} else if (state is SignUpFailure) {
return;
}
},
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),
)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 6),
const Center(
child: Text(
'Buat Akun',
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),
),
),
const SizedBox(height: 6),
const Center(
child: Text(
'Daftar untuk mulai monitoring',
style:
TextStyle(fontSize: 13, color: Colors.black54),
),
const SizedBox(height: 14),
),
const SizedBox(height: 14),
// Name
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: nameController,
hintText: 'Masukkan nama lengkap',
obscureText: false,
keyboardType: TextInputType.name,
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 null;
// Name
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: nameController,
hintText: 'Masukkan nama lengkap',
obscureText: false,
keyboardType: TextInputType.name,
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 null;
},
),
),
const SizedBox(height: 10),
// Email
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: emailController,
hintText: 'nama@email.com',
obscureText: false,
keyboardType: TextInputType.emailAddress,
prefixIcon: const Icon(CupertinoIcons.mail_solid),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
}
return null;
},
),
),
const SizedBox(height: 10),
// Password
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: passwordController,
hintText: 'Minimal 6 karakter',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
iconPassword = obscurePassword
? CupertinoIcons.eye_fill
: CupertinoIcons.eye_slash_fill;
});
},
icon: Icon(iconPassword),
),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
}
return null;
},
),
const SizedBox(height: 10),
),
const SizedBox(height: 10),
// Email
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: emailController,
hintText: 'nama@email.com',
obscureText: false,
keyboardType: TextInputType.emailAddress,
prefixIcon: const Icon(CupertinoIcons.mail_solid),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
}
return null;
},
),
// Confirm Password
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: confirmPasswordController,
hintText: 'Ulangi password',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
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 'Password tidak cocok';
}
return null;
},
),
const SizedBox(height: 10),
),
// Password
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: passwordController,
hintText: 'Minimal 6 karakter',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
prefixIcon: const Icon(CupertinoIcons.lock_fill),
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscurePassword = !obscurePassword;
iconPassword = obscurePassword
? CupertinoIcons.eye_fill
: CupertinoIcons.eye_slash_fill;
});
},
icon: Icon(iconPassword),
),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Please fill in this field';
}
return null;
},
),
),
const SizedBox(height: 10),
const SizedBox(height: 18),
// Confirm Password
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: MyTextField(
controller: confirmPasswordController,
hintText: 'Ulangi password',
obscureText: obscurePassword,
keyboardType: TextInputType.visiblePassword,
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 'Password tidak cocok';
}
return null;
},
),
),
// Button
!signUpRequired
? SizedBox(
width: MediaQuery.of(context).size.width * 0.92,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
MyUser myUser = MyUser.empty;
myUser.email = emailController.text;
myUser.name = nameController.text;
const SizedBox(height: 18),
// Button
!signUpRequired
? SizedBox(
width: MediaQuery.of(context).size.width * 0.92,
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
MyUser myUser = MyUser.empty;
myUser.email = emailController.text;
myUser.name = nameController.text;
setState(() {
context.read<SignUpBloc>().add(
SignUpRequired(
myUser,
passwordController.text,
),
);
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black87,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Daftar',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
setState(() {
context.read<SignUpBloc>().add(
SignUpRequired(
myUser,
passwordController.text,
),
);
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black87,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
)
: const CircularProgressIndicator(),
child: const Text(
'Daftar',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600),
),
),
)
: const CircularProgressIndicator(),
const SizedBox(height: 12),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Sudah punya akun? '),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Login di sini'),
)
],
const SizedBox(height: 12),
Center(
child: Text('Atau'),
),
const SizedBox(height: 12),
SizedBox(
width: MediaQuery.of(context).size.width * 0.92,
child: TextButton.icon(
onPressed: () {
context
.read<SignUpBloc>()
.add(GoogleSignInRequired());
},
icon: const Text(
'G',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
label: const Text(
'Daftar dengan Google',
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600),
),
style: TextButton.styleFrom(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(color: Colors.grey)),
padding: const EdgeInsets.symmetric(vertical: 16),
),
),
],
),
),
const SizedBox(height: 12),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Sudah punya akun? '),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Login di sini'),
)
],
),
),
],
),
const SizedBox(height: 30),
],
),
),
const SizedBox(height: 30),
],
),
),
),
),
);
),
);
}
}
}

View File

@ -21,13 +21,15 @@ class FirebaseUserRepo implements UserRepository {
try {
final userData = await userCollection.doc(firebaseUser.uid).get();
if (userData.exists && userData.data() != null) {
yield MyUser.fromEntity(MyUserEntity.fromDocument(userData.data()!));
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()!));
yield MyUser.fromEntity(
MyUserEntity.fromDocument(retryData.data()!));
} else {
yield MyUser.empty;
}
@ -40,32 +42,28 @@ class FirebaseUserRepo implements UserRepository {
});
}
@override
@override
Future<void> signIn(String email, String password) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password
);
await _firebaseAuth.signInWithEmailAndPassword(
email: email, password: password);
} catch (e) {
log(e.toString());
log(e.toString());
rethrow;
}
}
@override
Future<MyUser> signUp(MyUser myUser, String password) async {
try {
try {
UserCredential user = await _firebaseAuth.createUserWithEmailAndPassword(
email: myUser.email,
password: password
);
myUser.userId = user.user!.uid;
return myUser;
email: myUser.email, password: password);
myUser.userId = user.user!.uid;
return myUser;
} catch (e) {
log(e.toString());
rethrow;
}
}
}
@override
@ -77,8 +75,8 @@ class FirebaseUserRepo implements UserRepository {
Future<void> setUserData(MyUser myUser) async {
try {
await userCollection
.doc(myUser.userId)
.set(myUser.toEntity().toDocument());
.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) {
@ -87,6 +85,14 @@ class FirebaseUserRepo implements UserRepository {
}
}
}
@override
Future<void> signInWithGoogle() async {
try {
final GoogleAuthProvider googleProvider = GoogleAuthProvider();
await _firebaseAuth.signInWithPopup(googleProvider);
} catch (e) {
log('Google sign-in error: $e');
rethrow;
}
}
}

View File

@ -3,11 +3,13 @@ import 'models/models.dart';
abstract class UserRepository {
Stream<MyUser?> get user;
Future<MyUser> signUp(MyUser myUser, String password);
Future<MyUser> signUp(MyUser myUser, String password);
Future<void> setUserData(MyUser user);
Future<void> setUserData(MyUser user);
Future<void> signIn(String email, String password);
Future<void> signIn(String email, String password);
Future<void> logOut();
}
Future<void> signInWithGoogle();
Future<void> logOut();
}