diff --git a/assets/images/Password_invisible.svg b/assets/images/Password_invisible.svg
new file mode 100644
index 00000000..bb190eb3
--- /dev/null
+++ b/assets/images/Password_invisible.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/facebook.svg b/assets/images/facebook.svg
new file mode 100644
index 00000000..929d3861
--- /dev/null
+++ b/assets/images/facebook.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/images/google.svg b/assets/images/google.svg
new file mode 100644
index 00000000..27788e84
--- /dev/null
+++ b/assets/images/google.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/images/lift_line.png b/assets/images/lift_line.png
new file mode 100644
index 00000000..8dacf6d7
Binary files /dev/null and b/assets/images/lift_line.png differ
diff --git a/assets/images/password_visible.svg b/assets/images/password_visible.svg
new file mode 100644
index 00000000..25c55434
--- /dev/null
+++ b/assets/images/password_visible.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/right_line.png b/assets/images/right_line.png
new file mode 100644
index 00000000..805d1c0a
Binary files /dev/null and b/assets/images/right_line.png differ
diff --git a/lib/main.dart b/lib/main.dart
index 09a1c6ab..c7b31392 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,33 +1,58 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
+import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/auth/view/login_page.dart';
import 'package:syncrow_web/services/locator.dart';
+import 'package:syncrow_web/utils/color_manager.dart';
-void main() {
+Future main() async {
WidgetsFlutterBinding.ensureInitialized();
- initialSetup();
- runApp(const MyApp());
+ initialSetup(); // Perform initial setup, e.g., dependency injection
+ String checkToken = await AuthBloc.getTokenAndValidate();
+ runApp(MyApp(
+ isLoggedIn: checkToken,
+ ));
}
class MyApp extends StatelessWidget {
- const MyApp({super.key});
+ final dynamic isLoggedIn;
+ const MyApp({
+ super.key,
+ required this.isLoggedIn,
+ });
@override
Widget build(BuildContext context) {
return MaterialApp(
- debugShowCheckedModeBanner: false,
- scrollBehavior: const MaterialScrollBehavior().copyWith(
+ debugShowCheckedModeBanner: false, // Hide debug banner
+ scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
- PointerDeviceKind.unknown
+ PointerDeviceKind.unknown,
},
),
theme: ThemeData(
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
- useMaterial3: true,
+ textTheme: const TextTheme(
+ bodySmall: TextStyle(
+ fontSize: 13,
+ color: ColorsManager.whiteColors,
+ fontWeight: FontWeight.bold),
+ bodyMedium: TextStyle(color: Colors.black87, fontSize: 14),
+ bodyLarge: TextStyle(fontSize: 16,color: Colors.white),
+ headlineSmall: TextStyle(color: Colors.black87, fontSize: 18),
+ headlineMedium: TextStyle(color: Colors.black87, fontSize: 20),
+ headlineLarge: TextStyle(
+ color: Colors.white,
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: Colors.deepPurple), // Set up color scheme
+ useMaterial3: true, // Enable Material 3
),
- home: const LoginPage(),
+ home:LoginPage(),
);
}
}
diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart
new file mode 100644
index 00000000..00f6227f
--- /dev/null
+++ b/lib/pages/auth/bloc/auth_bloc.dart
@@ -0,0 +1,332 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:syncrow_web/pages/auth/bloc/auth_event.dart';
+import 'package:syncrow_web/pages/auth/bloc/auth_state.dart';
+import 'package:syncrow_web/pages/auth/model/login_with_email_model.dart';
+import 'package:syncrow_web/pages/auth/model/region_model.dart';
+import 'package:syncrow_web/pages/auth/model/token.dart';
+import 'package:syncrow_web/pages/auth/model/user_model.dart';
+import 'package:syncrow_web/services/auth_api.dart';
+import 'package:syncrow_web/utils/constants/strings_manager.dart';
+import 'package:syncrow_web/utils/helpers/shared_preferences_helper.dart';
+import 'package:syncrow_web/utils/snack_bar.dart';
+
+class AuthBloc extends Bloc {
+ AuthBloc() : super(LoginInitial()) {
+ on(_login);
+ on(checkBoxToggle);
+ on(changePassword);
+ on(_onStartTimer);
+ on(_onStopTimer);
+ on(_onUpdateTimer);
+ on(_passwordVisible);
+ on(_fetchRegion);
+ }
+
+ ////////////////////////////// forget password //////////////////////////////////
+ final TextEditingController forgetEmailController = TextEditingController();
+ final TextEditingController forgetPasswordController = TextEditingController();
+ final TextEditingController forgetOtp = TextEditingController();
+ final forgetFormKey = GlobalKey();
+ Timer? _timer;
+ int _remainingTime = 0;
+ List? regionList;
+
+ Future _onStartTimer(StartTimerEvent event, Emitter emit) async {
+ if (_validateInputs(emit)) return;
+ if (_timer != null && _timer!.isActive) {
+ return;
+ }
+ _remainingTime = 60;
+ add(UpdateTimerEvent(
+ remainingTime: _remainingTime, isButtonEnabled: false));
+ await AuthenticationAPI.sendOtp(email: forgetEmailController.text);
+ _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
+ _remainingTime--;
+ if (_remainingTime <= 0) {
+ _timer?.cancel();
+ add(const UpdateTimerEvent(remainingTime: 0, isButtonEnabled: true));
+ } else {
+ add(UpdateTimerEvent(
+ remainingTime: _remainingTime, isButtonEnabled: false));
+ }
+ });
+ }
+
+ void _onStopTimer(StopTimerEvent event, Emitter emit) {
+ _timer?.cancel();
+ emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
+ }
+
+ Future changePassword(
+ ChangePasswordEvent event, Emitter emit) async {
+ try {
+ emit(LoadingForgetState());
+ bool response = await AuthenticationAPI.verifyOtp(
+ email: forgetEmailController.text, otpCode: forgetOtp.text);
+ if (response == true) {
+ await AuthenticationAPI.forgetPassword(
+ password: forgetPasswordController.text,
+ email: forgetEmailController.text);
+ _timer?.cancel();
+ emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
+ }
+ emit(SuccessForgetState());
+ } catch (failure) {
+ emit(FailureForgetState(error: failure.toString()));
+ }
+ }
+
+ void _onUpdateTimer(UpdateTimerEvent event, Emitter emit) {
+ emit(TimerState(
+ isButtonEnabled: event.isButtonEnabled,
+ remainingTime: event.remainingTime));
+ }
+
+
+
+
+
+ ///////////////////////////////////// login /////////////////////////////////////
+ final TextEditingController loginEmailController = TextEditingController();
+ final TextEditingController loginPasswordController = TextEditingController();
+ final loginFormKey = GlobalKey();
+ bool isChecked = false;
+ bool obscureText = true;
+ String newPassword = '';
+ String maskedEmail = '';
+ String otpCode = '';
+ String validate = '';
+ static Token token = Token.emptyConstructor();
+ static UserModel? user;
+ bool showValidationMessage = false;
+
+ void _login(LoginButtonPressed event, Emitter emit) async {
+ emit(AuthLoading());
+ if (isChecked) {
+ try {
+ if (event.username.isEmpty || event.password.isEmpty) {
+ CustomSnackBar.displaySnackBar('Please enter your credentials');
+ emit(const LoginFailure(error: 'Something went wrong'));
+ return;
+ }
+ token = await AuthenticationAPI.loginWithEmail(
+ model: LoginWithEmailModel(
+ email: event.username,
+ password: event.password,
+ ),
+ );
+ } catch (failure) {
+ validate='Something went wrong';
+ emit(const LoginFailure(error: 'Something went wrong'));
+ // emit(LoginFailure(error: failure.toString()));
+ return;
+ }
+ if (token.accessTokenIsNotEmpty) {
+ FlutterSecureStorage storage = const FlutterSecureStorage();
+ await storage.write(
+ key: Token.loginAccessTokenKey, value: token.accessToken);
+ const FlutterSecureStorage().write(
+ key: UserModel.userUuidKey,
+ value: Token.decodeToken(token.accessToken)['uuid'].toString());
+ user = UserModel.fromToken(token);
+ loginEmailController.clear();
+ loginPasswordController.clear();
+ emit(LoginSuccess());
+ } else {
+ emit(const LoginFailure(error: 'Something went wrong'));
+ }
+ } else {
+ emit(const LoginFailure(error: 'Accept terms and condition'));
+ }
+ }
+
+ checkBoxToggle(CheckBoxEvent event, Emitter emit,) {
+ emit(AuthLoading());
+ isChecked = event.newValue!;
+ emit(LoginInitial());
+ }
+
+ checkOtpCode(ChangePasswordEvent event, Emitter emit,) async {
+ emit(LoadingForgetState());
+ await AuthenticationAPI.verifyOtp(
+ email: forgetEmailController.text, otpCode: forgetOtp.text);
+ emit(SuccessForgetState());
+ }
+
+ void _passwordVisible(PasswordVisibleEvent event, Emitter emit) {
+ emit(AuthLoading());
+ obscureText = !event.newValue!;
+ emit(PasswordVisibleState());
+ }
+
+ void launchURL(String url) {
+
+ }
+
+
+
+ /////////////////////////////////////VALIDATORS/////////////////////////////////////
+ String? validatePassword(String? value) {
+ if (value == null || value.isEmpty) {
+ return 'Password is required';
+ } else if (value.length < 8) {
+ return 'Password must be at least 8 characters';
+ }
+ return null;
+ }
+
+ String? validateEmail(String? value) {
+ if (value == null || value.isEmpty) {
+ return 'Email is required';
+ } else if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
+ return 'Enter a valid email address';
+ }
+ return null;
+ }
+
+ String? validateCode(String? value) {
+ if (value == null || value.isEmpty) {
+ return 'Code is required';
+ }
+ return null;
+ }
+
+ bool _validateInputs(Emitter emit) {
+ emit(LoadingForgetState());
+ final nameError = validateEmail(forgetEmailController.text);
+ if (nameError != null) {
+ emit(FailureForgetState(error: nameError));
+ return true;
+ }
+ return false;
+ }
+
+ String? validateRegion(String? value) {
+ if (value == null || value.isEmpty) {
+ return 'Please select a region';
+ }
+ return null;
+ }
+
+ String? passwordValidator(String? value) {
+ if (value == null || value.isEmpty) {
+ return 'Please enter your password';
+ }
+ List validationErrors = [];
+
+ if (!RegExp(r'^(?=.*[a-z])').hasMatch(value)) {
+ validationErrors.add(' - one lowercase letter');
+ }
+ if (!RegExp(r'^(?=.*[A-Z])').hasMatch(value)) {
+ validationErrors.add(' - one uppercase letter');
+ }
+ if (!RegExp(r'^(?=.*\d)').hasMatch(value)) {
+ validationErrors.add(' - one number');
+ }
+ if (!RegExp(r'^(?=.*[@$!%*?&])').hasMatch(value)) {
+ validationErrors.add(' - one special character');
+ }
+ if (value.length < 8) {
+ validationErrors.add(' - minimum 8 characters');
+ }
+
+ if (validationErrors.isNotEmpty) {
+ return 'Password must contain at least:\n${validationErrors.join('\n')}';
+ }
+
+ return null;
+ }
+
+ String? fullNameValidator(String? value) {
+ if (value == null) return 'Full name is required';
+ final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim();
+ if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) {
+ return 'Full name must be between 2 and 30 characters long';
+ }
+ if (RegExp(r"/[^ a-zA-Z0-9-\']/").hasMatch(withoutExtraSpaces)) {
+ return 'Only alphanumeric characters, space, dash and single quote are allowed';
+ }
+ final parts = withoutExtraSpaces.split(' ');
+ if (parts.length < 2) return 'Full name must contain first and last names';
+ if (parts.length > 3) return 'Full name can at most contain 3 parts';
+ if (parts.any((part) => part.length < 2 || part.length > 30)) {
+ return 'Full name parts must be between 2 and 30 characters long';
+ }
+ return null;
+ }
+
+ String maskEmail(String email) {
+ final emailParts = email.split('@');
+ if (emailParts.length != 2) return email;
+
+ final localPart = emailParts[0];
+ final domainPart = emailParts[1];
+
+ if (localPart.length < 3) return email;
+
+ final start = localPart.substring(0, 2);
+ final end = localPart.substring(localPart.length - 1);
+
+ final maskedLocalPart = '$start******$end';
+ return '$maskedLocalPart@$domainPart';
+ }
+
+ final List regions = [
+ 'North America',
+ 'South America',
+ 'Europe',
+ 'Asia',
+ 'Africa',
+ 'Australia',
+ 'Antarctica',
+ ];
+
+
+ static Future getTokenAndValidate() async {
+ try {
+ const storage = FlutterSecureStorage();
+ final firstLaunch = await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true;
+ if (firstLaunch) {
+ storage.deleteAll();
+ }
+ await SharedPreferencesHelper.saveBoolToSP(StringsManager.firstLaunch, false);
+ final value = await storage.read(key: Token.loginAccessTokenKey) ?? '';
+ if (value.isEmpty) {
+ return 'Token not found';
+ }
+ final tokenData = Token.decodeToken(value);
+ if (tokenData.containsKey('exp')) {
+ final exp = tokenData['exp'] ?? 0;
+ final currentTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
+ if (currentTime < exp) {
+ return 'Success';
+ } else {
+ return 'expired';
+ }
+ } else {
+ return 'Something went wrong';
+ }
+ } catch (_) {
+ return 'Something went wrong';
+ }
+ }
+
+ void _fetchRegion(RegionInitialEvent event, Emitter emit) async {
+ try {
+ emit(AuthLoading());
+ regionList = await AuthenticationAPI.fetchRegion();
+ emit(LoginSuccess());
+ } catch (e) {
+ emit( LoginFailure(error: e.toString()));
+
+ }
+ }
+
+
+}
+
+
+
diff --git a/lib/pages/auth/bloc/auth_event.dart b/lib/pages/auth/bloc/auth_event.dart
new file mode 100644
index 00000000..8a410555
--- /dev/null
+++ b/lib/pages/auth/bloc/auth_event.dart
@@ -0,0 +1,55 @@
+import 'package:equatable/equatable.dart';
+
+abstract class AuthEvent extends Equatable {
+ const AuthEvent();
+
+ @override
+ List