mirror of
https://github.com/SyncrowIOT/syncrow-app.git
synced 2026-10-25 06:17:55 +00:00
Initialized Auth pages for future work
Implemented Login functionality
This commit is contained in:
@@ -1,7 +1,60 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_app/features/auth/model/token.dart';
|
||||
import 'package:syncrow_app/features/auth/model/user_model.dart';
|
||||
import 'package:syncrow_app/services/api/authentication_api.dart';
|
||||
import 'package:syncrow_app/utils/helpers/decode_base64.dart';
|
||||
|
||||
part 'auth_state.dart';
|
||||
|
||||
class AuthCubit extends Cubit<AuthState> {
|
||||
AuthCubit() : super(AuthInitial());
|
||||
|
||||
static AuthCubit get(context) => BlocProvider.of(context);
|
||||
|
||||
TextEditingController emailController = TextEditingController();
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
bool isPasswordVisible = false;
|
||||
|
||||
void changePasswordVisibility() {
|
||||
isPasswordVisible = !isPasswordVisible;
|
||||
emit(AuthPasswordVisibilityChanged());
|
||||
}
|
||||
|
||||
bool agreeToTerms = false;
|
||||
|
||||
void changeAgreeToTerms() {
|
||||
agreeToTerms = !agreeToTerms;
|
||||
emit(AuthAgreeToTermsChanged());
|
||||
}
|
||||
|
||||
UserModel? user;
|
||||
|
||||
Token token = Token.emptyConstructor();
|
||||
|
||||
// (FlutterSecureStorage().read(key :'token'));
|
||||
|
||||
login() async {
|
||||
emit(AuthLoading());
|
||||
try {
|
||||
token = await AuthenticationAPI.loginWithEmail(
|
||||
email: emailController.text.toLowerCase(),
|
||||
password: passwordController.text,
|
||||
);
|
||||
|
||||
emit(AuthSuccess());
|
||||
} catch (e) {
|
||||
emit(AuthError(e.toString()));
|
||||
}
|
||||
|
||||
final parts = token.accessToken.split('.');
|
||||
if (parts.length != 3) {
|
||||
throw Exception('invalid access token');
|
||||
}
|
||||
final payload = decodeBase64(parts[1]);
|
||||
final payloadMap = json.decode(payload); //Map dictionary
|
||||
user = UserModel.fromToken(payloadMap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,3 +3,19 @@ part of 'auth_cubit.dart';
|
||||
abstract class AuthState {}
|
||||
|
||||
class AuthInitial extends AuthState {}
|
||||
|
||||
class AuthLoading extends AuthState {}
|
||||
|
||||
class AuthError extends AuthState {
|
||||
final String message;
|
||||
|
||||
AuthError(this.message) {
|
||||
debugPrint(message);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthSuccess extends AuthState {}
|
||||
|
||||
class AuthPasswordVisibilityChanged extends AuthState {}
|
||||
|
||||
class AuthAgreeToTermsChanged extends AuthState {}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class Token {
|
||||
static const String loginAccessToken = 'access_token';
|
||||
static const String loginRefreshToken = 'refreshToken';
|
||||
|
||||
final String accessToken;
|
||||
final String refreshToken;
|
||||
|
||||
Token.emptyConstructor()
|
||||
: accessToken = '',
|
||||
refreshToken = '';
|
||||
|
||||
Token(
|
||||
this.accessToken,
|
||||
this.refreshToken,
|
||||
);
|
||||
|
||||
Token.refreshToken(this.refreshToken) : accessToken = '';
|
||||
|
||||
factory Token.fromJson(Map<String, dynamic> json) {
|
||||
//save token to secure storage
|
||||
var storage = const FlutterSecureStorage();
|
||||
storage.write(key: loginAccessToken, value: json[loginAccessToken] ?? '');
|
||||
|
||||
//create token object ?
|
||||
return Token(json[loginAccessToken] ?? '', json[loginRefreshToken] ?? '');
|
||||
}
|
||||
|
||||
Map<String, String> toJson() => {loginRefreshToken: refreshToken};
|
||||
}
|
||||
@@ -6,19 +6,26 @@ class UserModel {
|
||||
|
||||
final String? phoneNumber;
|
||||
|
||||
final bool? isAnonymous;
|
||||
|
||||
final bool? isEmailVerified;
|
||||
|
||||
final bool? isAgreementAccepted;
|
||||
|
||||
//token decoded with jwt
|
||||
//{
|
||||
// "email": "[email protected]",
|
||||
// "userId": 2,
|
||||
// "uuid": "e145438c-4c62-4535-a0f4-f77958f9f9f4",
|
||||
// "sessionId": "0409a7a1-6ef5-42c5-b3a1-1f15c639b301",
|
||||
// "iat": 1709711675,
|
||||
// "exp": 1709711975
|
||||
// }
|
||||
|
||||
UserModel({
|
||||
required this.id,
|
||||
required this.email,
|
||||
required this.name,
|
||||
required this.photoUrl,
|
||||
required this.phoneNumber,
|
||||
required this.isAnonymous,
|
||||
required this.isEmailVerified,
|
||||
required this.isAgreementAccepted,
|
||||
});
|
||||
@@ -30,12 +37,24 @@ class UserModel {
|
||||
name: json['name'],
|
||||
photoUrl: json['photoUrl'],
|
||||
phoneNumber: json['phoneNumber'],
|
||||
isAnonymous: json['isAnonymous'],
|
||||
isEmailVerified: json['isEmailVerified'],
|
||||
isAgreementAccepted: json['isAgreementAccepted'],
|
||||
);
|
||||
}
|
||||
|
||||
//from token
|
||||
factory UserModel.fromToken(Map<String, dynamic> json) {
|
||||
return UserModel(
|
||||
id: json['userId'].toString(),
|
||||
email: json['email'],
|
||||
name: null,
|
||||
photoUrl: null,
|
||||
phoneNumber: null,
|
||||
isEmailVerified: null,
|
||||
isAgreementAccepted: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
@@ -43,7 +62,6 @@ class UserModel {
|
||||
'name': name,
|
||||
'photoUrl': photoUrl,
|
||||
'phoneNumber': phoneNumber,
|
||||
'isAnonymous': isAnonymous,
|
||||
'isEmailVerified': isEmailVerified,
|
||||
'isAgreementAccepted': isAgreementAccepted,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
class VerifyPassCode {
|
||||
static const String verificationPhone = 'phone';
|
||||
static const String verificationPassCode = 'passCode';
|
||||
static const String verificationAgent = 'agent';
|
||||
static const String verificationDeviceId = 'deviceId';
|
||||
|
||||
final String phone;
|
||||
final String passCode;
|
||||
final String agent;
|
||||
final String deviceId;
|
||||
|
||||
VerifyPassCode(
|
||||
{required this.phone, required this.passCode, required this.agent, required this.deviceId});
|
||||
|
||||
factory VerifyPassCode.fromJson(Map<String, dynamic> json) => VerifyPassCode(
|
||||
phone: json[verificationPhone],
|
||||
passCode: json[verificationPassCode],
|
||||
agent: json[verificationAgent],
|
||||
deviceId: json[verificationDeviceId]);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
verificationPhone: phone,
|
||||
verificationPassCode: passCode,
|
||||
verificationAgent: agent,
|
||||
verificationDeviceId: deviceId,
|
||||
};
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class AuthViewBody extends StatelessWidget {
|
||||
DefaultTextButton(
|
||||
text: 'Login',
|
||||
onPressed: () {
|
||||
Navigator.popAndPushNamed(context, Routes.homeRoute);
|
||||
Navigator.pushNamed(context, Routes.authLogin);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
@@ -31,18 +31,6 @@ class AuthViewBody extends StatelessWidget {
|
||||
isSecondary: true,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Center(
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: const Text(
|
||||
'Try as a Guest',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DidntGetCodeView extends StatelessWidget {
|
||||
const DidntGetCodeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart';
|
||||
import 'package:syncrow_app/navigation/routing_constants.dart';
|
||||
import 'package:syncrow_app/utils/context_extension.dart';
|
||||
|
||||
class LoginUserAgreement extends StatelessWidget {
|
||||
const LoginUserAgreement({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AuthCubit, AuthState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: AuthCubit.get(context).agreeToTerms,
|
||||
onChanged: (value) => AuthCubit.get(context).changeAgreeToTerms(),
|
||||
),
|
||||
Expanded(
|
||||
child: RichText(
|
||||
softWrap: true,
|
||||
maxLines: 2,
|
||||
text: TextSpan(
|
||||
text: 'I Agree to the ',
|
||||
style: context.bodySmall,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Privacy Policy',
|
||||
style: context.bodySmall.copyWith(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Colors.blue,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
Navigator.pushNamed(context, Routes.policyRoute);
|
||||
},
|
||||
),
|
||||
TextSpan(
|
||||
text: ' and ',
|
||||
style: context.bodySmall,
|
||||
),
|
||||
TextSpan(
|
||||
text: 'User Agreement',
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
Navigator.pushNamed(context, Routes.termsRoute);
|
||||
},
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Colors.blue,
|
||||
),
|
||||
),
|
||||
const TextSpan(
|
||||
text: '.',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart';
|
||||
import 'package:syncrow_app/features/auth/view/widgets/login/login_user_agreement.dart';
|
||||
import 'package:syncrow_app/features/shared_widgets/default_text_button.dart';
|
||||
import 'package:syncrow_app/features/shared_widgets/text_widgets/title_medium.dart';
|
||||
import 'package:syncrow_app/navigation/routing_constants.dart';
|
||||
import 'package:syncrow_app/utils/resource_manager/color_manager.dart';
|
||||
import 'package:syncrow_app/utils/resource_manager/constants.dart';
|
||||
|
||||
class LoginView extends StatelessWidget {
|
||||
const LoginView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//TODO move to strings manager
|
||||
|
||||
return BlocConsumer<AuthCubit, AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state is AuthError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
),
|
||||
);
|
||||
} else if (state is AuthSuccess) {
|
||||
Navigator.pushNamed(context, Routes.homeRoute);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Constants.defaultPadding,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const TitleMedium(
|
||||
text: 'Login',
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
TextField(
|
||||
controller: AuthCubit.get(context).emailController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.backgroundColor.withAlpha(100),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
TextField(
|
||||
controller: AuthCubit.get(context).passwordController,
|
||||
obscureText: !AuthCubit.get(context).isPasswordVisible,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
AuthCubit.get(context).isPasswordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
AuthCubit.get(context).changePasswordVisibility();
|
||||
},
|
||||
),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.backgroundColor.withAlpha(100),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const LoginUserAgreement(),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
state is AuthLoading
|
||||
? const CircularProgressIndicator()
|
||||
: Expanded(
|
||||
child: DefaultTextButton(
|
||||
enabled: AuthCubit.get(context).agreeToTerms,
|
||||
text: "Login",
|
||||
onPressed: () {
|
||||
AuthCubit.get(context).login();
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OneTimePasswordView extends StatelessWidget {
|
||||
const OneTimePasswordView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PrivacyPolicyView extends StatelessWidget {
|
||||
const PrivacyPolicyView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SignUpView extends StatelessWidget {
|
||||
const SignUpView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class UserAgreementView extends StatelessWidget {
|
||||
const UserAgreementView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Placeholder();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user