Feat: done features sign up

This commit is contained in:
orangdeso 2025-05-11 02:07:52 +07:00
parent 7a2b8db157
commit 6516cc7c39
13 changed files with 610 additions and 110 deletions

View File

@ -680,7 +680,7 @@
"languageVersion": "3.4"
}
],
"generated": "2025-05-09T09:48:57.570155Z",
"generated": "2025-05-10T18:42:15.590215Z",
"generator": "pub",
"generatorVersion": "3.5.0",
"flutterRoot": "file:///D:/Flutter/flutter_sdk/flutter_3.24.0",

143
assets/images/il_email.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
class Validators {
static String? validatorEmail(String? value) {
if (value == null || value.isEmpty) {
@ -13,6 +15,20 @@ class Validators {
return null;
}
static String? Function(String?) validatorConfirmPassword(
TextEditingController passwordController,
) {
return (String? value) {
if (value == null || value.isEmpty) {
return 'Password tidak boleh kosong';
}
if (value != passwordController.text) {
return 'Password tidak cocok';
}
return null;
};
}
static String? validatorName(String? value) {
if (value == null || value.isEmpty) {
return 'Nama Lengkap tidak boleh kosong';

View File

@ -1,10 +1,10 @@
import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:e_porter/domain/models/user_entity.dart';
import 'package:e_porter/domain/repositories/auth_repository.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../../_core/service/logger_service.dart';
class AuthException implements Exception {
final String message;
AuthException(this.message);
@ -24,11 +24,15 @@ class AuthRepositoryImpl implements AuthRepository {
password: password,
);
final user = userCredential.user!;
await user.reload();
if (!user.emailVerified) {
await _firebaseAuth.signOut();
throw AuthException("email-not-verified");
}
return UserEntity(uid: user.uid, email: user.email ?? "");
} on FirebaseAuthException catch (e) {
logger.w("FirebaseAuthException code: ${e.code}");
logger.w("FirebaseAuthException message: ${e.message}");
switch (e.code) {
case 'invalid-email':
throw AuthException("Format email tidak valid.");
@ -44,6 +48,43 @@ class AuthRepositoryImpl implements AuthRepository {
}
}
@override
Future<UserEntity> registerWithEmailPassword(String email, String password) async {
try {
UserCredential userCredential = await _firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: password,
);
final user = userCredential.user!;
await user.sendEmailVerification();
await user.updateDisplayName(email);
return UserEntity(uid: user.uid, email: user.email ?? "");
} on FirebaseAuthException catch (e) {
log("FirebaseAuthException code: ${e.code}");
log("FirebaseAuthException message: ${e.message}");
throw AuthException(e.code);
} catch (e) {
throw AuthException(e.toString());
}
}
@override
Future<void> saveUserData(UserData userData) async {
try {
await _firestore.collection('users').doc(userData.uid).set(
userData.toMap(),
SetOptions(merge: true),
);
log("User data berhasil disimpan ke Firestore");
} catch (e) {
log("Error saving user data: $e");
throw AuthException("Gagal menyimpan data pengguna.");
}
}
@override
Future<void> signOut() async {
await _firebaseAuth.signOut();

View File

@ -11,14 +11,18 @@ class AuthBinding extends Bindings {
final authRepository = AuthRepositoryImpl(firebaseAuth);
final loginUseCase = LoginUseCase(authRepository);
final getUserRoleUseCase = GetUserRoleUseCase(authRepository);
final getUserRoleUseCase = GetUserRoleUseCase(authRepository);
final getUserDataUseCase = GetUserDataUseCase(authRepository);
final registerUseCase = RegisterUseCase(authRepository);
final saveUserDataUseCase = SaveUserDataUseCase(authRepository);
Get.put<AuthController>(
AuthController(
loginUseCase: loginUseCase,
getUserRoleUseCase: getUserRoleUseCase,
getUserRoleUseCase: getUserRoleUseCase,
getUserDataUseCase: getUserDataUseCase,
registerUseCase: registerUseCase,
saveUserDataUseCase: saveUserDataUseCase,
),
);
}

View File

@ -26,17 +26,17 @@ class UserData {
UserData({
required this.uid,
required this.tipeId,
required this.noId,
required this.name,
required this.email,
required this.phone,
required this.birthDate,
required this.gender,
required this.work,
required this.city,
required this.address,
required this.role,
this.tipeId,
this.noId,
this.name,
this.email,
this.phone,
this.birthDate,
this.gender,
this.work,
this.city,
this.address,
this.role,
});
factory UserData.fromMap(Map<String, dynamic> map) {
@ -83,7 +83,7 @@ class UserData {
'role': role,
};
}
UserData copyWith({
String? uid,
String? tipeId,

View File

@ -3,8 +3,10 @@ import 'package:e_porter/domain/models/user_entity.dart';
abstract class AuthRepository {
Future<UserEntity> signInWithEmailPassword(String email, String password);
Future<void> signOut();
Future<String?> getUserRole(String uid);
Future<UserData?> getUserData(String uid);
}
Future<UserEntity> registerWithEmailPassword(String email, String password);
Future<void> saveUserData(UserData userData);
}

View File

@ -23,8 +23,28 @@ class GetUserRoleUseCase {
class GetUserDataUseCase {
final AuthRepository authRepository;
GetUserDataUseCase(this.authRepository);
Future<UserData?> call(String uid) async {
return await authRepository.getUserData(uid);
}
}
class RegisterUseCase {
final AuthRepository authRepository;
RegisterUseCase(this.authRepository);
Future<UserEntity> call(String email, String password) async {
return await authRepository.registerWithEmailPassword(email, password);
}
}
class SaveUserDataUseCase {
final AuthRepository authRepository;
SaveUserDataUseCase(this.authRepository);
Future<void> call(UserData userData) async {
return await authRepository.saveUserData(userData);
}
}

View File

@ -1,5 +1,8 @@
import 'package:e_porter/_core/utils/snackbar/snackbar_helper.dart';
import 'package:e_porter/data/repositories/auth_repository_impl.dart';
import 'package:e_porter/domain/models/user_entity.dart';
import 'package:e_porter/domain/usecases/auth_usecase.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@ -11,6 +14,8 @@ class AuthController extends GetxController {
final LoginUseCase loginUseCase;
final GetUserRoleUseCase getUserRoleUseCase;
final GetUserDataUseCase getUserDataUseCase;
final RegisterUseCase registerUseCase;
final SaveUserDataUseCase saveUserDataUseCase;
final emailController = TextEditingController();
final passwordController = TextEditingController();
@ -22,13 +27,15 @@ class AuthController extends GetxController {
required this.loginUseCase,
required this.getUserRoleUseCase,
required this.getUserDataUseCase,
required this.registerUseCase,
required this.saveUserDataUseCase,
});
Future<void> login({String? roleFromOnboarding}) async {
errorMessage.value = '';
if (emailController.text.isEmpty || passwordController.text.isEmpty) {
_showErrorSnackbar("Error", "Email/Password tidak boleh kosong");
SnackbarHelper.showError("Error", "Email/Password tidak boleh kosong");
return;
}
isLoading.value = true;
@ -44,7 +51,7 @@ class AuthController extends GetxController {
logger.d("roleFromDB: $roleFromDB, roleFromOnboarding: $roleFromOnboarding, UID: $uid");
if (roleFromDB != null && roleFromOnboarding != null && roleFromDB != roleFromOnboarding) {
_showErrorSnackbar(
SnackbarHelper.showError(
"Role Tidak Sesuai", "Akun ini terdaftar sebagai '$roleFromDB', bukan '$roleFromOnboarding'.");
return;
}
@ -53,12 +60,12 @@ class AuthController extends GetxController {
final userData = await getUserDataUseCase(uid);
if (userData == null) {
_showErrorSnackbar("Login Gagal", "Data user tidak ditemukan.");
SnackbarHelper.showError("Login Gagal", "Data user tidak ditemukan.");
return;
}
if (userData.role!.toLowerCase() != effectiveRole.toLowerCase()) {
_showErrorSnackbar(
SnackbarHelper.showError(
"Role Tidak Sesuai", "Data user menunjukkan role '${userData.role}', bukan '$effectiveRole'.");
return;
}
@ -66,21 +73,140 @@ class AuthController extends GetxController {
await PreferencesService.saveUserData(userData);
Get.offAllNamed(Routes.NAVBAR, arguments: effectiveRole);
} on AuthException catch (e) {
_showErrorSnackbar("Login Gagal", e.message);
if (e.message == 'email-not-verified') {
SnackbarHelper.showError(
"Verifikasi Diperlukan",
"Silakan cek email Anda dan klik link verifikasi sebelum login.",
);
} else {
SnackbarHelper.showError(
"Login Gagal",
"Email atau password anda salah.",
);
}
} catch (e) {
_showErrorSnackbar("Terjadi Kesalahan", e.toString());
SnackbarHelper.showError("Terjadi Kesalahan", e.toString());
} finally {
isLoading.value = false;
}
}
void _showErrorSnackbar(String title, String message) {
Get.snackbar(
title,
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
Future<void> register({
required String name,
required String email,
required String password,
required String role,
}) async {
errorMessage.value = '';
isLoading.value = true;
try {
final userEntity = await registerUseCase(
email.trim(),
password,
);
final userData = UserData(
uid: userEntity.uid,
tipeId: null,
noId: null,
name: name.trim(),
email: email.trim(),
phone: null,
birthDate: null,
gender: null,
work: null,
city: null,
address: null,
role: role,
);
await saveUserDataUseCase(userData);
SnackbarHelper.showSuccess(
"Berhasil",
"Akun berhasil dibuat. Silakan cek email Anda untuk verifikasi terlebih dahulu.",
);
Get.offNamed(Routes.VERIFICATION);
} on AuthException catch (e) {
switch (e.message) {
case 'weak-password':
SnackbarHelper.showError("Error", "Password terlalu lemah.");
break;
case 'email-already-in-use':
SnackbarHelper.showError("Error", "Email sudah terdaftar.");
break;
case 'invalid-email':
SnackbarHelper.showError("Error", "Format email tidak valid.");
break;
default:
SnackbarHelper.showError("Registrasi Gagal", "Terjadi kesalahan saat registrasi.");
}
} catch (e) {
SnackbarHelper.showError("Terjadi Kesalahan", e.toString());
} finally {
isLoading.value = false;
}
}
Future<void> resendEmailVerification() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) {
SnackbarHelper.showError("Error", "User tidak ditemukan.");
return;
}
if (user.emailVerified) {
SnackbarHelper.showSuccess("Sudah Terverifikasi", "Email Anda sudah terverifikasi.");
return;
}
try {
await user.sendEmailVerification();
SnackbarHelper.showSuccess(
"Terkirim",
"Link verifikasi telah dikirim ulang ke email Anda.",
);
} on FirebaseException catch (e) {
if (e.message?.contains('No AppCheckProvider') == true) {
SnackbarHelper.showSuccess(
"Terkirim",
"Link verifikasi telah dikirim ulang ke email Anda.",
);
} else {
SnackbarHelper.showError(
"Gagal",
"Gagal mengirim ulang verifikasi. Silakan coba lagi.",
);
}
} catch (e) {
SnackbarHelper.showError(
"Gagal",
"Gagal mengirim ulang verifikasi. Silakan coba lagi.",
);
}
}
Future<void> completeEmailVerification() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
await user.reload();
if (user.emailVerified) {
final userData = await getUserDataUseCase(user.uid);
if (userData != null) {
await PreferencesService.saveUserData(userData);
}
Get.offAllNamed(Routes.NAVBAR, arguments: userData?.role);
}
}
// void _showErrorSnackbar(String title, String message) {
// Get.snackbar(
// title,
// message,
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red,
// colorText: Colors.white,
// );
// }
}

View File

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:e_porter/_core/component/button/button_fill.dart';
import 'package:e_porter/_core/constants/colors.dart';
import 'package:e_porter/_core/constants/typography.dart';
@ -128,7 +130,8 @@ class _LoginScreenState extends State<LoginScreen> {
firstText: 'Belum punya akun?',
secondText: 'Daftar',
onTab: () {
Get.toNamed(Routes.REGISTER);
log('Role Login: $role');
Get.toNamed(Routes.REGISTER, arguments: role);
},
),
),

View File

@ -1,6 +1,12 @@
import 'dart:developer';
import 'package:e_porter/_core/utils/formatter/uppercase_helper.dart';
import 'package:e_porter/_core/validators/validators.dart';
import 'package:e_porter/presentation/controllers/auth_controller.dart';
import 'package:e_porter/presentation/screens/auth/component/header_text.dart';
import 'package:e_porter/presentation/screens/routes/app_rountes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:zoom_tap_animation/zoom_tap_animation.dart';
@ -19,6 +25,37 @@ class RegisterScreen extends StatefulWidget {
}
class _RegisterScreenState extends State<RegisterScreen> {
final String? role = Get.arguments as String;
final _formKey = GlobalKey<FormState>();
final TextEditingController _name = TextEditingController();
final TextEditingController _email = TextEditingController();
final TextEditingController _password = TextEditingController();
final TextEditingController _verifPassword = TextEditingController();
final AuthController _authController = Get.find<AuthController>();
void _handleRegister() {
if (_formKey.currentState!.validate()) {
if (_password.text != _verifPassword.text) {
Get.snackbar(
'Error',
'Password tidak cocok',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
_authController.register(
name: _name.text,
email: _email.text,
password: _password.text,
role: role.toString(),
);
log('Role Registrasi: $role');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -28,72 +65,86 @@ class _RegisterScreenState extends State<RegisterScreen> {
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HeaderText(
firstText: 'Daftar',
secondText:
'Segera daftarkan diri anda ke aplikasi ini untuk akses penuh ke fitur kami!',
),
SizedBox(height: 50.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: TypographyStyles.body(
'Nama',
color: GrayColors.gray800,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
InputForm(
hintText: 'Suparjo',
svgIconPath: 'assets/icons/ic_account.svg',
),
SizedBox(height: 20.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: TypographyStyles.body(
'Email',
color: GrayColors.gray800,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
InputForm(
hintText: 'example@gmail.com',
svgIconPath: 'assets/icons/ic_email.svg',
),
SizedBox(height: 20.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: TypographyStyles.body(
'Password',
color: GrayColors.gray800,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
InputPassword(
hintText: '••••••••••',
svgIconPath: 'assets/icons/ic_padlock.svg',
),
SizedBox(height: 20.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: TypographyStyles.body(
'Konfirmasi Password',
color: GrayColors.gray800,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
InputPassword(
hintText: '••••••••••',
svgIconPath: 'assets/icons/ic_padlock.svg',
),
],
)),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
HeaderText(
firstText: 'Daftar',
secondText: 'Segera daftarkan diri anda ke aplikasi ini untuk akses penuh ke fitur kami!',
),
SizedBox(height: 50.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: TypographyStyles.body(
'Nama',
color: GrayColors.gray800,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
InputForm(
controller: _name,
hintText: 'SUPARJO',
svgIconPath: 'assets/icons/ic_account.svg',
validator: Validators.validatorName,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
UpperCaseTextFormatter(),
],
textInputType: TextInputType.text,
),
SizedBox(height: 20.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: TypographyStyles.body(
'Email',
color: GrayColors.gray800,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
InputForm(
controller: _email,
hintText: 'example@gmail.com',
svgIconPath: 'assets/icons/ic_email.svg',
validator: Validators.validatorEmail,
textInputType: TextInputType.emailAddress,
),
SizedBox(height: 20.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: TypographyStyles.body(
'Password',
color: GrayColors.gray800,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
InputPassword(
controller: _password,
hintText: '••••••••••',
svgIconPath: 'assets/icons/ic_padlock.svg',
validator: Validators.validatorPassword,
),
SizedBox(height: 20.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: TypographyStyles.body(
'Konfirmasi Password',
color: GrayColors.gray800,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 16.h),
InputPassword(
controller: _verifPassword,
hintText: '••••••••••',
svgIconPath: 'assets/icons/ic_padlock.svg',
validator: Validators.validatorConfirmPassword(_password),
),
],
)),
),
),
),
@ -102,13 +153,13 @@ class _RegisterScreenState extends State<RegisterScreen> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ZoomTapAnimation(
child: ButtonFill(
text: 'Daftar',
textColor: Colors.white,
onTap: () {
Get.toNamed(Routes.STATESUCCES);
},
Obx(
() => ZoomTapAnimation(
child: ButtonFill(
text: _authController.isLoading.value ? 'Loading...' : 'Daftar',
textColor: Colors.white,
onTap: _authController.isLoading.value ? null : _handleRegister,
),
),
),
SizedBox(height: 20.h),

View File

@ -0,0 +1,88 @@
import 'dart:async';
import 'package:e_porter/_core/constants/colors.dart';
import 'package:e_porter/_core/constants/typography.dart';
import 'package:e_porter/presentation/controllers/auth_controller.dart';
import 'package:e_porter/presentation/screens/auth/component/header_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:zoom_tap_animation/zoom_tap_animation.dart';
class VerifikasiScreen extends StatefulWidget {
VerifikasiScreen({super.key});
@override
State<VerifikasiScreen> createState() => _VerifikasiScreenState();
}
class _VerifikasiScreenState extends State<VerifikasiScreen> {
final AuthController _authController = Get.find();
Timer? _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 3), (_) async {
await _authController.completeEmailVerification();
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: SingleChildScrollView(
child: Column(
children: [
HeaderText(
firstText: 'Verifikasi Email',
secondText: 'Kami telah mengirimkan link verifikasi melalui email anda. Silahkan cek email anda',
),
Padding(
padding: EdgeInsets.only(top: 20.h),
child: SvgPicture.asset('assets/images/il_email.svg'),
),
SizedBox(height: 32.h),
_buildSendVerification(
onTap: _authController.resendEmailVerification,
)
],
),
),
),
),
);
}
Widget _buildSendVerification({required VoidCallback onTap}) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TypographyStyles.body(
'Kirim ulang verifikasi?',
color: GrayColors.gray600,
fontWeight: FontWeight.w400,
),
SizedBox(width: 8.w),
ZoomTapAnimation(
child: GestureDetector(
onTap: onTap,
child: TypographyStyles.body('Kirim Ulang', color: Colors.blue.shade600),
),
)
],
);
}
}

View File

@ -12,6 +12,7 @@ import 'package:e_porter/presentation/screens/auth/pages/forget_password_screen.
import 'package:e_porter/presentation/screens/auth/pages/login_screen.dart';
import 'package:e_porter/presentation/screens/auth/pages/register_screen.dart';
import 'package:e_porter/presentation/screens/auth/pages/state_succes_screen.dart';
import 'package:e_porter/presentation/screens/auth/pages/verifikasi_screen.dart';
import 'package:e_porter/presentation/screens/boarding_pass/pages/boarding_pass_screen.dart';
import 'package:e_porter/presentation/screens/boarding_pass/pages/detail_history_porter_screen.dart';
import 'package:e_porter/presentation/screens/boarding_pass/pages/detail_ticket_screen.dart';
@ -66,6 +67,10 @@ class AppRoutes {
page: () => LoginScreen(),
binding: AuthBinding(),
),
GetPage(
name: Routes.VERIFICATION,
page: () => VerifikasiScreen(),
),
GetPage(
name: Routes.HOME,
page: () => MainNavigation(),
@ -196,6 +201,7 @@ class Routes {
static const SPLASH = '/splash';
static const ONBOARDING = '/onboarding';
static const LOGIN = '/login';
static const VERIFICATION = '/verification';
static const HOME = '/home';
static const BOARDINGPASS = '/boarding_pass';
static const PROFILE = '/profile';