feat: Update authentication flow and UI enhancements for sign-in and forgot password screens
This commit is contained in:
parent
0da8d5621c
commit
1967fdb6b8
|
@ -116,7 +116,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger().e('Error in screenRedirect: $e');
|
Logger().e('Error in screenRedirect: $e');
|
||||||
_navigateToRoute(AppRoutes.checkLocation);
|
_navigateToRoute(AppRoutes.signIn);
|
||||||
} finally {
|
} finally {
|
||||||
_isRedirecting = false;
|
_isRedirecting = false;
|
||||||
Logger().d('Screen redirect completed');
|
Logger().d('Screen redirect completed');
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
|
import 'package:sigap/src/shared/widgets/state_screeen/state_screen.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
import 'package:sigap/src/utils/validators/validation.dart';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
|
@ -45,9 +47,6 @@ class ForgotPasswordController extends GetxController {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// Simulate API call
|
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
|
||||||
|
|
||||||
await AuthenticationRepository.instance.sendResetPasswordForEmail(
|
await AuthenticationRepository.instance.sendResetPasswordForEmail(
|
||||||
emailController.text,
|
emailController.text,
|
||||||
);
|
);
|
||||||
|
@ -58,6 +57,19 @@ class ForgotPasswordController extends GetxController {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'Reset password email sent successfully.',
|
message: 'Reset password email sent successfully.',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Get.off(
|
||||||
|
() => StateScreen(
|
||||||
|
title: 'Check Your Email',
|
||||||
|
subtitle: 'Please check your email for the reset link.',
|
||||||
|
image: TImages.customerSupport,
|
||||||
|
isSvg: true,
|
||||||
|
primaryButtonTitle: 'Go Back',
|
||||||
|
showButton: true,
|
||||||
|
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
|
|
|
@ -7,44 +7,44 @@ import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
|
||||||
class SignInController extends GetxController {
|
class SignInController extends GetxController {
|
||||||
static SignInController get instance => Get.find();
|
static SignInController get instance => Get.find();
|
||||||
|
|
||||||
final _logger = Logger();
|
final _logger = Logger();
|
||||||
final _authRepo = Get.find<AuthenticationRepository>();
|
final _authRepo = Get.find<AuthenticationRepository>();
|
||||||
|
|
||||||
// Form controllers
|
// Form controllers
|
||||||
final email = TextEditingController();
|
final email = TextEditingController();
|
||||||
final password = TextEditingController();
|
final password = TextEditingController();
|
||||||
|
|
||||||
// Form error messages
|
// Form error messages
|
||||||
final RxString emailError = RxString('');
|
final RxString emailError = RxString('');
|
||||||
final RxString passwordError = RxString('');
|
final RxString passwordError = RxString('');
|
||||||
|
|
||||||
// States
|
// States
|
||||||
final RxBool isLoading = RxBool(false);
|
final RxBool isLoading = RxBool(false);
|
||||||
final RxBool isPasswordVisible = RxBool(false);
|
final RxBool isPasswordVisible = RxBool(false);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
email.dispose();
|
email.dispose();
|
||||||
password.dispose();
|
password.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle password visibility
|
// Toggle password visibility
|
||||||
void togglePasswordVisibility() {
|
void togglePasswordVisibility() {
|
||||||
isPasswordVisible.value = !isPasswordVisible.value;
|
isPasswordVisible.value = !isPasswordVisible.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to forgot password screen
|
// Navigate to forgot password screen
|
||||||
void goToForgotPassword() {
|
void goToForgotPassword() {
|
||||||
Get.toNamed(AppRoutes.forgotPassword);
|
Get.toNamed(AppRoutes.forgotPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to sign up screen
|
// Navigate to sign up screen
|
||||||
void goToSignUp() {
|
void goToSignUp() {
|
||||||
Get.toNamed(AppRoutes.roleSelection);
|
Get.toNamed(AppRoutes.roleSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear error messages
|
// Clear error messages
|
||||||
void clearErrors() {
|
void clearErrors() {
|
||||||
emailError.value = '';
|
emailError.value = '';
|
||||||
|
@ -55,7 +55,7 @@ class SignInController extends GetxController {
|
||||||
Future<void> signIn(GlobalKey<FormState> formKey) async {
|
Future<void> signIn(GlobalKey<FormState> formKey) async {
|
||||||
// Clear previous errors
|
// Clear previous errors
|
||||||
clearErrors();
|
clearErrors();
|
||||||
|
|
||||||
// Validate form
|
// Validate form
|
||||||
final isValid = formKey.currentState?.validate() ?? false;
|
final isValid = formKey.currentState?.validate() ?? false;
|
||||||
if (!isValid) return;
|
if (!isValid) return;
|
||||||
|
@ -64,20 +64,19 @@ class SignInController extends GetxController {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// Attempt to sign in
|
// Attempt to sign in
|
||||||
final signInResult = await _authRepo.loginWithEmailPassword(
|
await _authRepo.loginWithEmailPassword(
|
||||||
email: email.text.trim(),
|
email: email.text.trim(),
|
||||||
password: password.text.trim(),
|
password: password.text.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle result
|
// Handle result
|
||||||
_logger.i('Sign in successful: $signInResult');
|
// _logger.i('Sign in successful: $signInResult');
|
||||||
|
|
||||||
// Redirect based on user's profile status
|
// Redirect based on user's profile status
|
||||||
_authRepo.screenRedirect();
|
_authRepo.screenRedirect();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
// Handle specific errors
|
// Handle specific errors
|
||||||
if (e.toString().contains('user-not-found')) {
|
if (e.toString().contains('user-not-found')) {
|
||||||
emailError.value = 'No user found with this email';
|
emailError.value = 'No user found with this email';
|
||||||
|
@ -89,36 +88,110 @@ class SignInController extends GetxController {
|
||||||
// Show general error
|
// Show general error
|
||||||
TLoaders.errorSnackBar(title: 'Sign In Failed', message: e.toString());
|
TLoaders.errorSnackBar(title: 'Sign In Failed', message: e.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.e('Sign in error: $e');
|
_logger.e('Sign in error: $e');
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign in with Google
|
// Sign in with Google
|
||||||
Future<void> googleSignIn() async {
|
Future<void> googleSignIn() async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// Attempt to sign in with Google
|
// Attempt to sign in with Google
|
||||||
await _authRepo.signInWithGoogle();
|
await _authRepo.signInWithGoogle();
|
||||||
|
|
||||||
// Redirect based on user's profile status
|
// Redirect based on user's profile status
|
||||||
_authRepo.screenRedirect();
|
_authRepo.screenRedirect();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
// Show error
|
// Show error
|
||||||
TLoaders.errorSnackBar(
|
TLoaders.errorSnackBar(
|
||||||
title: 'Google Sign In Failed',
|
title: 'Google Sign In Failed',
|
||||||
message: e.toString(),
|
message: e.toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
_logger.e('Google sign in error: $e');
|
_logger.e('Google sign in error: $e');
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sign in with github
|
||||||
|
Future<void> githubSignIn() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Attempt to sign in with GitHub
|
||||||
|
await _authRepo.signInWithGithub();
|
||||||
|
|
||||||
|
// Redirect based on user's profile status
|
||||||
|
_authRepo.screenRedirect();
|
||||||
|
} catch (e) {
|
||||||
|
isLoading.value = false;
|
||||||
|
|
||||||
|
// Show error
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'GitHub Sign In Failed',
|
||||||
|
message: e.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_logger.e('GitHub sign in error: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign in with Facebook
|
||||||
|
Future<void> facebookSignIn() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Attempt to sign in with Facebook
|
||||||
|
await _authRepo.signInWithFacebook();
|
||||||
|
|
||||||
|
// Redirect based on user's profile status
|
||||||
|
_authRepo.screenRedirect();
|
||||||
|
} catch (e) {
|
||||||
|
isLoading.value = false;
|
||||||
|
|
||||||
|
// Show error
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Facebook Sign In Failed',
|
||||||
|
message: e.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_logger.e('Facebook sign in error: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign in with Apple
|
||||||
|
Future<void> appleSignIn() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Attempt to sign in with Apple
|
||||||
|
await _authRepo.signInWithApple();
|
||||||
|
|
||||||
|
// Redirect based on user's profile status
|
||||||
|
_authRepo.screenRedirect();
|
||||||
|
} catch (e) {
|
||||||
|
isLoading.value = false;
|
||||||
|
|
||||||
|
// Show error
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Apple Sign In Failed',
|
||||||
|
message: e.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
_logger.e('Apple sign in error: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/forgot-password/forgot_password_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/forgot-password/forgot_password_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart';
|
||||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
|
|
||||||
class ForgotPasswordScreen extends StatelessWidget {
|
class ForgotPasswordScreen extends StatelessWidget {
|
||||||
const ForgotPasswordScreen({super.key});
|
const ForgotPasswordScreen({super.key});
|
||||||
|
@ -15,6 +19,8 @@ class ForgotPasswordScreen extends StatelessWidget {
|
||||||
// Get the controller
|
// Get the controller
|
||||||
final controller = Get.find<ForgotPasswordController>();
|
final controller = Get.find<ForgotPasswordController>();
|
||||||
|
|
||||||
|
final isDarkMode = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
// Set system overlay style
|
// Set system overlay style
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
|
@ -24,26 +30,29 @@ class ForgotPasswordScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: TColors.light,
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(Icons.arrow_back, color: TColors.textPrimary),
|
icon: Icon(
|
||||||
|
Icons.arrow_back,
|
||||||
|
color: isDarkMode ? TColors.accent : TColors.primary,
|
||||||
|
),
|
||||||
onPressed: controller.goBack,
|
onPressed: controller.goBack,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: Center(
|
||||||
child: Padding(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24.0),
|
child: Padding(
|
||||||
child: Obx(
|
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||||
() => Form(
|
child: Obx(
|
||||||
key: controller.formKey,
|
() => Form(
|
||||||
child:
|
key: controller.formKey,
|
||||||
controller.isEmailSent.value
|
child:
|
||||||
? _buildSuccessView(controller)
|
controller.isEmailSent.value
|
||||||
: _buildFormView(controller),
|
? _buildSuccessView(controller)
|
||||||
|
: _buildFormView(controller, context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -52,16 +61,28 @@ class ForgotPasswordScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFormView(ForgotPasswordController controller) {
|
Widget _buildFormView(
|
||||||
|
ForgotPasswordController controller,
|
||||||
|
BuildContext? context,
|
||||||
|
) {
|
||||||
|
final isDark = THelperFunctions.isDarkMode(context!);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// Logo centered
|
||||||
|
SvgPicture.asset(TImages.lightAppBgLogo, height: 100, width: 100),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Header centered
|
||||||
const AuthHeader(
|
const AuthHeader(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
title: 'Forgot Password',
|
title: 'Forgot Password',
|
||||||
subtitle: 'Enter your email to reset your password',
|
subtitle: 'Enter your email to reset your password',
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Email field
|
// Email field
|
||||||
Obx(
|
Obx(
|
||||||
() => CustomTextField(
|
() => CustomTextField(
|
||||||
|
@ -70,6 +91,12 @@ class ForgotPasswordScreen extends StatelessWidget {
|
||||||
validator: controller.validateEmail,
|
validator: controller.validateEmail,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
errorText: controller.emailError.value,
|
errorText: controller.emailError.value,
|
||||||
|
hintText: 'enter your email',
|
||||||
|
prefixIcon: Icon(
|
||||||
|
Icons.email_outlined,
|
||||||
|
size: 20,
|
||||||
|
color: isDark ? TColors.accent : TColors.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -89,8 +116,11 @@ class ForgotPasswordScreen extends StatelessWidget {
|
||||||
|
|
||||||
Widget _buildSuccessView(ForgotPasswordController controller) {
|
Widget _buildSuccessView(ForgotPasswordController controller) {
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
// Logo centered
|
||||||
|
SvgPicture.asset(TImages.lightAppBgLogo, height: 100, width: 100),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// Success icon
|
// Success icon
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
|
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signin/signin_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signin/signin_controller.dart';
|
||||||
|
@ -8,6 +9,7 @@ import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart';
|
||||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/image_strings.dart'; // Added for logo image
|
||||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
import 'package:sigap/src/utils/validators/validation.dart';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
|
@ -26,116 +28,173 @@ class SignInScreen extends StatelessWidget {
|
||||||
final isDarkMode = THelperFunctions.isDarkMode(context);
|
final isDarkMode = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// Use dynamic background color from theme
|
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: Center(
|
||||||
child: Padding(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24.0),
|
child: Padding(
|
||||||
child: Form(
|
padding: const EdgeInsets.all(24.0),
|
||||||
key: formKey,
|
child: Form(
|
||||||
child: Column(
|
key: formKey,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
const SizedBox(height: 16),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
// Header - pass isDarkMode to AuthHeader if needed
|
// Logo centered at the top
|
||||||
const AuthHeader(
|
SvgPicture.asset(
|
||||||
title: 'Welcome Back',
|
TImages.lightAppBgLogo,
|
||||||
subtitle: 'Sign in to your account to continue',
|
height: 100,
|
||||||
),
|
width: 100,
|
||||||
|
|
||||||
// Email field
|
|
||||||
Obx(
|
|
||||||
() => CustomTextField(
|
|
||||||
label: 'Email',
|
|
||||||
controller: controller.email,
|
|
||||||
validator: TValidators.validateEmail,
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
errorText: controller.emailError.value,
|
|
||||||
textInputAction: TextInputAction.next,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Password field
|
// Header centered
|
||||||
Obx(
|
const AuthHeader(
|
||||||
() => PasswordField(
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
label: 'Password',
|
title: 'Welcome Back',
|
||||||
controller: controller.password,
|
subtitle: 'Sign in to your account to continue',
|
||||||
validator: TValidators.validatePassword,
|
|
||||||
isVisible: controller.isPasswordVisible,
|
|
||||||
errorText: controller.passwordError.value,
|
|
||||||
onToggleVisibility: controller.togglePasswordVisibility,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// Forgot password
|
const SizedBox(height: 24),
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
// Email field with icon
|
||||||
child: TextButton(
|
Obx(
|
||||||
onPressed: controller.goToForgotPassword,
|
() => CustomTextField(
|
||||||
child: Text(
|
label: 'Email',
|
||||||
'Forgot Password?',
|
controller: controller.email,
|
||||||
style: TextStyle(
|
validator: TValidators.validateEmail,
|
||||||
color: Theme.of(context).primaryColor,
|
keyboardType: TextInputType.emailAddress,
|
||||||
fontWeight: FontWeight.w500,
|
errorText: controller.emailError.value,
|
||||||
),
|
textInputAction: TextInputAction.next,
|
||||||
|
prefixIcon: const Icon(TablerIcons.mail, size: 20),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
// Password field with icon
|
||||||
|
Obx(
|
||||||
// Sign in button
|
() => PasswordField(
|
||||||
Obx(
|
label: 'Password',
|
||||||
() => AuthButton(
|
controller: controller.password,
|
||||||
text: 'Sign In',
|
validator: TValidators.validatePassword,
|
||||||
onPressed: () => controller.signIn(formKey),
|
isVisible: controller.isPasswordVisible,
|
||||||
isLoading: controller.isLoading.value,
|
errorText: controller.passwordError.value,
|
||||||
),
|
onToggleVisibility: controller.togglePasswordVisibility,
|
||||||
),
|
prefixIcon: const Icon(TablerIcons.lock, size: 20),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Or divider
|
|
||||||
const AuthDivider(text: 'OR'),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Social sign in buttons
|
|
||||||
SocialButton(
|
|
||||||
text: 'Continue with Google',
|
|
||||||
icon: Icon(
|
|
||||||
TablerIcons.brand_google,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
onPressed: () => controller.googleSignIn(),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Don't have an account
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Don\'t have an account?',
|
|
||||||
// Use theme color for text
|
|
||||||
style: TextStyle(color: Theme.of(context).hintColor),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
),
|
||||||
onPressed: controller.goToSignUp,
|
|
||||||
|
// Forgot password
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: controller.goToForgotPassword,
|
||||||
child: Text(
|
child: Text(
|
||||||
'Sign Up',
|
'Forgot Password?',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
|
||||||
],
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Sign in button
|
||||||
|
Obx(
|
||||||
|
() => AuthButton(
|
||||||
|
text: 'Sign In',
|
||||||
|
onPressed: () => controller.signIn(formKey),
|
||||||
|
isLoading: controller.isLoading.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Or divider
|
||||||
|
const AuthDivider(text: 'OR'),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Social sign in buttons - Row of social icons
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
// Google Sign In
|
||||||
|
Expanded(
|
||||||
|
child: SocialButton(
|
||||||
|
shapeType: SocialButtonShapeType.circular,
|
||||||
|
icon: const Icon(
|
||||||
|
TablerIcons.brand_google,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xFFDB4437),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
onPressed: () => controller.googleSignIn(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 0),
|
||||||
|
|
||||||
|
// GitHub Sign In
|
||||||
|
Expanded(
|
||||||
|
child: SocialButton(
|
||||||
|
shapeType: SocialButtonShapeType.circular,
|
||||||
|
icon: const Icon(
|
||||||
|
TablerIcons.brand_apple,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xFF333333),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
onPressed: () => controller.appleSignIn(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 0),
|
||||||
|
|
||||||
|
// Facebook Sign In
|
||||||
|
Expanded(
|
||||||
|
child: SocialButton(
|
||||||
|
shapeType: SocialButtonShapeType.circular,
|
||||||
|
icon: const Icon(
|
||||||
|
TablerIcons.brand_facebook,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xFF1877F2),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
onPressed: () => controller.facebookSignIn(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Don't have an account
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Don\'t have an account?',
|
||||||
|
// Use theme color for text
|
||||||
|
style: TextStyle(color: Theme.of(context).hintColor),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: controller.goToSignUp,
|
||||||
|
child: Text(
|
||||||
|
'Sign Up',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
|
|
||||||
class AuthButton extends StatelessWidget {
|
class AuthButton extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
|
@ -19,6 +21,7 @@ class AuthButton extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 55,
|
height: 55,
|
||||||
|
@ -26,7 +29,8 @@ class AuthButton extends StatelessWidget {
|
||||||
onPressed: isLoading ? null : onPressed,
|
onPressed: isLoading ? null : onPressed,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: backgroundColor ?? Theme.of(context).primaryColor,
|
backgroundColor: backgroundColor ?? Theme.of(context).primaryColor,
|
||||||
foregroundColor: textColor ?? Theme.of(context).colorScheme.onPrimary,
|
foregroundColor:
|
||||||
|
textColor ?? (isDark ? TColors.primary : TColors.accent),
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||||
|
|
|
@ -5,15 +5,21 @@ import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
class AuthHeader extends StatelessWidget {
|
class AuthHeader extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
|
final CrossAxisAlignment crossAxisAlignment;
|
||||||
|
|
||||||
const AuthHeader({super.key, required this.title, required this.subtitle});
|
const AuthHeader({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
this.crossAxisAlignment = CrossAxisAlignment.start,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final dark = THelperFunctions.isDarkMode(context);
|
final dark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: crossAxisAlignment,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
|
||||||
|
enum SocialButtonShapeType { defaultShape, rounded, circular }
|
||||||
|
|
||||||
class SocialButton extends StatelessWidget {
|
class SocialButton extends StatelessWidget {
|
||||||
final String text;
|
final String? text;
|
||||||
final Icon? icon;
|
final Icon? icon;
|
||||||
final String? iconImage;
|
final String? iconImage;
|
||||||
final double? iconImageWidth;
|
final double? iconImageWidth;
|
||||||
|
@ -12,10 +14,11 @@ class SocialButton extends StatelessWidget {
|
||||||
final Color? foregroundColor;
|
final Color? foregroundColor;
|
||||||
final Color? borderColor;
|
final Color? borderColor;
|
||||||
final bool isVisible;
|
final bool isVisible;
|
||||||
|
final SocialButtonShapeType shapeType;
|
||||||
|
|
||||||
const SocialButton({
|
const SocialButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.text,
|
this.text,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.iconImage,
|
this.iconImage,
|
||||||
this.iconImageWidth,
|
this.iconImageWidth,
|
||||||
|
@ -25,53 +28,81 @@ class SocialButton extends StatelessWidget {
|
||||||
this.foregroundColor,
|
this.foregroundColor,
|
||||||
this.borderColor,
|
this.borderColor,
|
||||||
this.isVisible = true,
|
this.isVisible = true,
|
||||||
|
this.shapeType = SocialButtonShapeType.defaultShape,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!isVisible) return const SizedBox.shrink();
|
if (!isVisible) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
OutlinedBorder shape;
|
||||||
|
EdgeInsetsGeometry padding;
|
||||||
|
|
||||||
|
switch (shapeType) {
|
||||||
|
case SocialButtonShapeType.rounded:
|
||||||
|
shape = RoundedRectangleBorder(borderRadius: BorderRadius.circular(30));
|
||||||
|
padding = const EdgeInsets.symmetric(
|
||||||
|
vertical: TSizes.md,
|
||||||
|
horizontal: TSizes.md,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SocialButtonShapeType.circular:
|
||||||
|
shape = const CircleBorder();
|
||||||
|
padding = const EdgeInsets.all(0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
shape = RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||||
|
);
|
||||||
|
padding = const EdgeInsets.symmetric(
|
||||||
|
vertical: TSizes.md,
|
||||||
|
horizontal: TSizes.md,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget iconWidget =
|
||||||
|
iconImage == null
|
||||||
|
? icon ?? const SizedBox.shrink()
|
||||||
|
: Image.asset(
|
||||||
|
iconImage!,
|
||||||
|
width: iconImageWidth,
|
||||||
|
height: iconImageHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget child;
|
||||||
|
if (text == null || text!.isEmpty) {
|
||||||
|
child = iconWidget;
|
||||||
|
} else {
|
||||||
|
child = Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 24, width: 24, child: iconWidget),
|
||||||
|
const SizedBox(width: TSizes.md),
|
||||||
|
Text(
|
||||||
|
text!,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: foregroundColor ?? Colors.black,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: shapeType == SocialButtonShapeType.circular ? 48 : double.infinity,
|
||||||
|
height: shapeType == SocialButtonShapeType.circular ? 48 : null,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
backgroundColor: backgroundColor ?? Colors.white,
|
backgroundColor: backgroundColor ?? Colors.white,
|
||||||
foregroundColor: foregroundColor ?? Colors.black,
|
foregroundColor: foregroundColor ?? Colors.black,
|
||||||
side: BorderSide(color: borderColor ?? Colors.grey.shade300),
|
side: BorderSide(color: borderColor ?? Colors.grey.shade300),
|
||||||
shape: RoundedRectangleBorder(
|
shape: shape,
|
||||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
padding: padding,
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: TSizes.md,
|
|
||||||
horizontal: TSizes.md,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
child:
|
|
||||||
iconImage == null
|
|
||||||
? icon
|
|
||||||
: Image.asset(
|
|
||||||
iconImage!,
|
|
||||||
width: iconImageWidth,
|
|
||||||
height: iconImageHeight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: TSizes.md),
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
|
||||||
color: foregroundColor ?? Colors.black,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:sigap/src/cores/services/location_service.dart';
|
import 'package:sigap/src/cores/services/location_service.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart';
|
||||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
|
@ -11,6 +12,8 @@ class CheckLocationController extends GetxController
|
||||||
final VoidCallback? onSuccess;
|
final VoidCallback? onSuccess;
|
||||||
final LocationService _locationService = LocationService.instance;
|
final LocationService _locationService = LocationService.instance;
|
||||||
|
|
||||||
|
final storage = GetStorage();
|
||||||
|
|
||||||
// Reactive variables
|
// Reactive variables
|
||||||
final RxInt currentPage = 0.obs;
|
final RxInt currentPage = 0.obs;
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
|
@ -146,6 +149,8 @@ class CheckLocationController extends GetxController
|
||||||
// Navigate to communication slide before zooming
|
// Navigate to communication slide before zooming
|
||||||
_navigateToCommunicationSlide();
|
_navigateToCommunicationSlide();
|
||||||
|
|
||||||
|
storage.write('isFirstTime', false);
|
||||||
|
|
||||||
// Give time to see the success message before animating
|
// Give time to see the success message before animating
|
||||||
await Future.delayed(Duration(milliseconds: 800));
|
await Future.delayed(Duration(milliseconds: 800));
|
||||||
animController
|
animController
|
||||||
|
|
|
@ -81,16 +81,16 @@ class OnboardingController extends GetxController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to skip to welcome screen
|
// Method to skip to check location screen
|
||||||
void skipToWelcomeScreen() {
|
void skipToCheckLocation() {
|
||||||
skipOnboarding();
|
skipOnboarding();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to navigate to welcome screen
|
// Method to navigate to check location screen
|
||||||
void skipOnboarding() {
|
void skipOnboarding() {
|
||||||
// Mark onboarding as completed in storage
|
// Mark onboarding as completed in storage
|
||||||
storage.write('isFirstTime', true);
|
storage.write('isFirstTime', false);
|
||||||
Get.offAllNamed(AppRoutes.welcome);
|
Get.offAllNamed(AppRoutes.checkLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to check location validity and proceed with auth flow
|
// Method to check location validity and proceed with auth flow
|
||||||
|
@ -136,7 +136,7 @@ class OnboardingController extends GetxController
|
||||||
}
|
}
|
||||||
|
|
||||||
void goToSignIn() {
|
void goToSignIn() {
|
||||||
storage.write('isFirstTime', true);
|
storage.write('isFirstTime', false);
|
||||||
|
|
||||||
Get.offAllNamed(AppRoutes.signIn);
|
Get.offAllNamed(AppRoutes.signIn);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ class TLoaders {
|
||||||
isDismissible: true,
|
isDismissible: true,
|
||||||
shouldIconPulse: true,
|
shouldIconPulse: true,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
backgroundColor: TColors.primary,
|
backgroundColor: TColors.success,
|
||||||
snackPosition: SnackPosition.TOP,
|
snackPosition: SnackPosition.TOP,
|
||||||
duration: Duration(seconds: duration),
|
duration: Duration(seconds: duration),
|
||||||
margin: const EdgeInsets.all(10),
|
margin: const EdgeInsets.all(10),
|
||||||
|
|
Loading…
Reference in New Issue