Initialized Auth pages for future work

Implemented Login functionality
This commit is contained in:
Mohammad Salameh
2024-03-07 10:29:19 +03:00
parent 4087f9c71c
commit f734801e94
28 changed files with 728 additions and 110 deletions

11
assets/icons/qr_scan.svg Normal file
View File

@ -0,0 +1,11 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.8383 0H15.0391C14.7155 0 14.4531 0.262344 14.4531 0.585938C14.4531 0.909531 14.7155 1.17188 15.0391 1.17188H17.8383C18.3841 1.17188 18.8281 1.61594 18.8281 2.16176V4.96094C18.8281 5.28453 19.0905 5.54688 19.4141 5.54688C19.7377 5.54688 20 5.28453 20 4.96094V2.16176C20 0.969766 19.0302 0 17.8383 0Z" fill="#5D5D5D"/>
<path d="M0.585938 5.54688C0.909531 5.54688 1.17188 5.28453 1.17188 4.96094V2.16176C1.17188 1.61594 1.61594 1.17188 2.16172 1.17188H4.96094C5.28453 1.17188 5.54688 0.909531 5.54688 0.585938C5.54688 0.262344 5.28453 0 4.96094 0H2.16172C0.969766 0 0 0.969766 0 2.16176V4.96094C0 5.28453 0.262344 5.54688 0.585938 5.54688Z" fill="#5D5D5D"/>
<path d="M4.96094 18.8281H2.16172C1.61594 18.8281 1.17188 18.3841 1.17188 17.8382V15.0391C1.17188 14.7155 0.909531 14.4531 0.585938 14.4531C0.262344 14.4531 0 14.7155 0 15.0391V17.8382C0 19.0302 0.969766 20 2.16172 20H4.96094C5.28453 20 5.54688 19.7377 5.54688 19.4141C5.54688 19.0905 5.28453 18.8281 4.96094 18.8281Z" fill="#5D5D5D"/>
<path d="M19.4141 14.4531C19.0905 14.4531 18.8281 14.7155 18.8281 15.0391V17.8382C18.8281 18.3841 18.3841 18.8281 17.8383 18.8281H15.0391C14.7155 18.8281 14.4531 19.0905 14.4531 19.4141C14.4531 19.7377 14.7155 20 15.0391 20H17.8383C19.0303 20 20 19.0302 20 17.8382V15.0391C20 14.7155 19.7377 14.4531 19.4141 14.4531Z" fill="#5D5D5D"/>
<path d="M13.3455 4.29688H15.2039C15.4576 4.29688 15.6641 4.50332 15.6641 4.75711V6.61547C15.6641 6.93906 15.9264 7.20141 16.25 7.20141C16.5736 7.20141 16.836 6.93906 16.836 6.61547V4.75711C16.836 3.85715 16.1038 3.125 15.2039 3.125H13.3455C13.0219 3.125 12.7595 3.38734 12.7595 3.71094C12.7595 4.03453 13.0219 4.29688 13.3455 4.29688Z" fill="#5D5D5D"/>
<path d="M3.16406 4.75711V6.61547C3.16406 6.93906 3.42641 7.20141 3.75 7.20141C4.07359 7.20141 4.33594 6.93906 4.33594 6.61547V4.75711C4.33594 4.50332 4.54242 4.29688 4.79617 4.29688H6.65457C6.97816 4.29688 7.24051 4.03453 7.24051 3.71094C7.24051 3.38734 6.97816 3.125 6.65457 3.125H4.79617C3.89625 3.125 3.16406 3.85715 3.16406 4.75711Z" fill="#5D5D5D"/>
<path d="M6.65457 15.625H4.79617C4.54238 15.625 4.33594 15.4185 4.33594 15.1648V13.3064C4.33594 12.9828 4.07359 12.7205 3.75 12.7205C3.42641 12.7205 3.16406 12.9828 3.16406 13.3064V15.1648C3.16406 16.0647 3.89625 16.7969 4.79617 16.7969H6.65457C6.97816 16.7969 7.24051 16.5345 7.24051 16.2109C7.24051 15.8873 6.97816 15.625 6.65457 15.625Z" fill="#5D5D5D"/>
<path d="M16.836 15.1648V13.3064C16.836 12.9828 16.5736 12.7205 16.25 12.7205C15.9264 12.7205 15.6641 12.9828 15.6641 13.3064V15.1648C15.6641 15.4185 15.4576 15.625 15.2039 15.625H13.3455C13.0219 15.625 12.7595 15.8873 12.7595 16.2109C12.7595 16.5345 13.0219 16.7969 13.3455 16.7969H15.2039C16.1038 16.7969 16.836 16.0647 16.836 15.1648Z" fill="#5D5D5D"/>
<path d="M0.0390625 10C0.0390625 10.3236 0.301406 10.5859 0.625 10.5859H19.375C19.6986 10.5859 19.9609 10.3236 19.9609 10C19.9609 9.67641 19.6986 9.41406 19.375 9.41406H0.625C0.301406 9.41406 0.0390625 9.67641 0.0390625 10Z" fill="#5D5D5D"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6276 20H9.37238C8.35719 20 7.53121 19.1741 7.53121 18.1589V17.7342C7.09961 17.5963 6.68027 17.4223 6.27742 17.2138L5.97648 17.5147C5.24762 18.2445 4.08008 18.2231 3.37238 17.5145L2.48523 16.6273C1.77629 15.9192 1.7559 14.752 2.48547 14.0232L2.78617 13.7225C2.5777 13.3197 2.40367 12.9004 2.26574 12.4688H1.84113C0.825976 12.4688 0 11.6428 0 10.6276V9.37238C0 8.35719 0.825976 7.53125 1.84117 7.53125H2.26578C2.40371 7.09961 2.57773 6.68031 2.78621 6.27746L2.48527 5.97656C1.75613 5.2482 1.77625 4.08094 2.48551 3.37246L3.37273 2.48527C4.08207 1.77496 5.24934 1.75723 5.9768 2.48551L6.27746 2.78617C6.68031 2.57773 7.09965 2.40367 7.53125 2.26574V1.84113C7.53125 0.825937 8.35719 0 9.37242 0H10.6276C11.6428 0 12.4688 0.825937 12.4688 1.84113V2.26578C12.9004 2.40367 13.3197 2.57773 13.7225 2.78621L14.0235 2.48527C14.7523 1.75551 15.9199 1.77691 16.6276 2.48555L17.5147 3.37266C18.2237 4.08082 18.2441 5.24797 17.5145 5.97676L17.2138 6.27746C17.4223 6.68031 17.5963 7.09957 17.7342 7.53125H18.1588C19.174 7.53125 20 8.35719 20 9.37238V10.6276C20 11.6428 19.174 12.4688 18.1588 12.4688H17.7342C17.5963 12.9004 17.4223 13.3197 17.2138 13.7225L17.5147 14.0235C18.2439 14.7518 18.2237 15.9191 17.5145 16.6276L16.6273 17.5148C15.9179 18.2251 14.7507 18.2428 14.0232 17.5145L13.7225 17.2139C13.3197 17.4223 12.9004 17.5964 12.4688 17.7343V18.1589C12.4688 19.1741 11.6428 20 10.6276 20ZM6.47332 15.9832C7.03297 16.3142 7.63531 16.5642 8.26359 16.7264C8.52234 16.7931 8.70312 17.0265 8.70312 17.2937V18.1589C8.70312 18.5279 9.0034 18.8281 9.37242 18.8281H10.6276C10.9966 18.8281 11.2969 18.5279 11.2969 18.1589V17.2937C11.2969 17.0265 11.4777 16.7931 11.7364 16.7264C12.3647 16.5642 12.9671 16.3142 13.5267 15.9832C13.757 15.847 14.0502 15.8841 14.2393 16.0732L14.8521 16.6861C15.1164 16.9506 15.5407 16.9445 15.7984 16.6864L16.6862 15.7986C16.9433 15.5418 16.9519 15.1175 16.6864 14.8523L16.0733 14.2393C15.8842 14.0501 15.8471 13.7569 15.9833 13.5267C16.3143 12.9671 16.5643 12.3647 16.7264 11.7364C16.7932 11.4777 17.0266 11.2969 17.2938 11.2969H18.1589C18.5279 11.2969 18.8282 10.9967 18.8282 10.6277V9.37242C18.8282 9.0034 18.5279 8.70316 18.1589 8.70316H17.2938C17.0265 8.70316 16.7932 8.52238 16.7264 8.26367C16.5643 7.63535 16.3142 7.03301 15.9833 6.4734C15.8471 6.24316 15.8842 5.94996 16.0733 5.76082L16.6862 5.14797C16.9511 4.88336 16.9442 4.45914 16.6864 4.20168L15.7987 3.31398C15.5414 3.05633 15.117 3.04879 14.8524 3.31375L14.2394 3.92684C14.0502 4.11602 13.757 4.15305 13.5268 4.01688C12.9671 3.6859 12.3648 3.43586 11.7365 3.27371C11.4777 3.20695 11.297 2.97359 11.297 2.70637V1.84113C11.297 1.47211 10.9967 1.17188 10.6277 1.17188H9.37246C9.00344 1.17188 8.70316 1.47211 8.70316 1.84113V2.70629C8.70316 2.97352 8.52238 3.20688 8.26363 3.27363C7.63535 3.43578 7.03301 3.68582 6.47336 4.0168C6.24305 4.15293 5.94988 4.1159 5.76074 3.92676L5.14793 3.31391C4.88371 3.04938 4.45938 3.05551 4.20168 3.31363L3.31391 4.20137C3.0568 4.45816 3.0482 4.8825 3.31367 5.14766L3.92676 5.76074C4.1159 5.94988 4.15293 6.24309 4.0168 6.47332C3.68582 7.03293 3.43582 7.63527 3.27367 8.26359C3.20688 8.52234 2.97352 8.70309 2.70633 8.70309H1.84117C1.47215 8.70312 1.17188 9.00336 1.17188 9.37238V10.6276C1.17188 10.9966 1.47215 11.2969 1.84117 11.2969H2.70629C2.97352 11.2969 3.20684 11.4777 3.27363 11.7364C3.43578 12.3647 3.68582 12.967 4.01676 13.5266C4.15289 13.7569 4.11586 14.0501 3.92672 14.2392L3.31387 14.8521C3.04895 15.1167 3.05586 15.5409 3.31363 15.7984L4.20133 16.6861C4.45863 16.9437 4.88301 16.9513 5.14762 16.6863L5.76066 16.0732C5.90004 15.9339 6.19 15.8156 6.47332 15.9832Z" fill="#5D5D5D"/>
<path d="M10 14.3516C7.60051 14.3516 5.64844 12.3995 5.64844 10C5.64844 7.60055 7.60051 5.64844 10 5.64844C12.3995 5.64844 14.3516 7.60055 14.3516 10C14.3516 12.3995 12.3995 14.3516 10 14.3516ZM10 6.82031C8.24668 6.82031 6.82031 8.24672 6.82031 10C6.82031 11.7533 8.24672 13.1797 10 13.1797C11.7533 13.1797 13.1797 11.7533 13.1797 10C13.1797 8.24672 11.7533 6.82031 10 6.82031Z" fill="#5D5D5D"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,7 +1,60 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.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'; part 'auth_state.dart';
class AuthCubit extends Cubit<AuthState> { class AuthCubit extends Cubit<AuthState> {
AuthCubit() : super(AuthInitial()); 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);
}
} }

View File

@ -3,3 +3,19 @@ part of 'auth_cubit.dart';
abstract class AuthState {} abstract class AuthState {}
class AuthInitial extends 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 {}

View File

@ -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};
}

View File

@ -6,19 +6,26 @@ class UserModel {
final String? phoneNumber; final String? phoneNumber;
final bool? isAnonymous;
final bool? isEmailVerified; final bool? isEmailVerified;
final bool? isAgreementAccepted; final bool? isAgreementAccepted;
//token decoded with jwt
//{
// "email": "Test@Test.com",
// "userId": 2,
// "uuid": "e145438c-4c62-4535-a0f4-f77958f9f9f4",
// "sessionId": "0409a7a1-6ef5-42c5-b3a1-1f15c639b301",
// "iat": 1709711675,
// "exp": 1709711975
// }
UserModel({ UserModel({
required this.id, required this.id,
required this.email, required this.email,
required this.name, required this.name,
required this.photoUrl, required this.photoUrl,
required this.phoneNumber, required this.phoneNumber,
required this.isAnonymous,
required this.isEmailVerified, required this.isEmailVerified,
required this.isAgreementAccepted, required this.isAgreementAccepted,
}); });
@ -30,12 +37,24 @@ class UserModel {
name: json['name'], name: json['name'],
photoUrl: json['photoUrl'], photoUrl: json['photoUrl'],
phoneNumber: json['phoneNumber'], phoneNumber: json['phoneNumber'],
isAnonymous: json['isAnonymous'],
isEmailVerified: json['isEmailVerified'], isEmailVerified: json['isEmailVerified'],
isAgreementAccepted: json['isAgreementAccepted'], 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() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
@ -43,7 +62,6 @@ class UserModel {
'name': name, 'name': name,
'photoUrl': photoUrl, 'photoUrl': photoUrl,
'phoneNumber': phoneNumber, 'phoneNumber': phoneNumber,
'isAnonymous': isAnonymous,
'isEmailVerified': isEmailVerified, 'isEmailVerified': isEmailVerified,
'isAgreementAccepted': isAgreementAccepted, 'isAgreementAccepted': isAgreementAccepted,
}; };

View File

@ -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,
};
}

View File

@ -22,7 +22,7 @@ class AuthViewBody extends StatelessWidget {
DefaultTextButton( DefaultTextButton(
text: 'Login', text: 'Login',
onPressed: () { onPressed: () {
Navigator.popAndPushNamed(context, Routes.homeRoute); Navigator.pushNamed(context, Routes.authLogin);
}, },
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
@ -31,18 +31,6 @@ class AuthViewBody extends StatelessWidget {
isSecondary: true, isSecondary: true,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Center(
child: InkWell(
onTap: () {},
child: const Text(
'Try as a Guest',
style: TextStyle(
color: Colors.grey,
),
),
),
),
const SizedBox(height: 30),
], ],
), ),
); );

View File

@ -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();
}
}

View File

@ -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,
),
),
],
),
),
),
],
);
},
);
}
}

View File

@ -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();
},
),
),
],
),
],
),
),
);
},
);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -9,10 +9,10 @@ class LightsCubit extends Cubit<LightsState> {
static LightsCubit get(context) => BlocProvider.of(context); static LightsCubit get(context) => BlocProvider.of(context);
Map<int, LightMode> lightModes = { Map<int, LightMode> lightModes = {
0: LightMode.Doze, 0: LightMode.doze,
1: LightMode.Relax, 1: LightMode.relax,
2: LightMode.Reading, 2: LightMode.reading,
3: LightMode.Energizing, 3: LightMode.energizing,
}; };
setLightingMode(LightModel light, LightMode mode) { setLightingMode(LightModel light, LightMode mode) {
@ -55,8 +55,8 @@ class LightsCubit extends Cubit<LightsState> {
} }
enum LightMode { enum LightMode {
Doze, doze,
Relax, relax,
Reading, reading,
Energizing, energizing,
} }

View File

@ -12,10 +12,11 @@ class CategoriesView extends StatelessWidget {
create: (context) => DevicesCubit(), create: (context) => DevicesCubit(),
child: BlocBuilder<DevicesCubit, DevicesState>( child: BlocBuilder<DevicesCubit, DevicesState>(
builder: (context, state) => Container( builder: (context, state) => Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
width: MediaQuery.sizeOf(context).width, width: MediaQuery.sizeOf(context).width,
height: MediaQuery.sizeOf(context).height, height: MediaQuery.sizeOf(context).height,
child: const DevicesViewBody()), child: const DevicesViewBody(),
),
), ),
); );
} }

View File

@ -4,6 +4,7 @@ import 'package:syncrow_app/utils/resource_manager/color_manager.dart';
class DefaultTextButton extends StatelessWidget { class DefaultTextButton extends StatelessWidget {
const DefaultTextButton({ const DefaultTextButton({
super.key, super.key,
this.enabled = true,
this.onPressed, this.onPressed,
required this.text, required this.text,
this.isSecondary = false, this.isSecondary = false,
@ -13,15 +14,17 @@ class DefaultTextButton extends StatelessWidget {
final String text; final String text;
final bool isSecondary; final bool isSecondary;
final bool enabled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextButton( return TextButton(
onPressed: onPressed, onPressed: enabled ? onPressed : null,
style: isSecondary style: isSecondary
? null ? null
: ButtonStyle( : ButtonStyle(
backgroundColor: backgroundColor: MaterialStateProperty.all(
MaterialStateProperty.all(ColorsManager.primaryColor), enabled ? ColorsManager.primaryColor : Colors.grey),
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@ -30,7 +33,12 @@ class DefaultTextButton extends StatelessWidget {
), ),
child: Text( child: Text(
text, text,
style: TextStyle(color: isSecondary ? Colors.black : Colors.white), style: TextStyle(
color: isSecondary
? Colors.black
: enabled
? Colors.white
: Colors.black),
), ),
); );
} }

View File

@ -33,9 +33,11 @@ class Assets {
static const String iconsMenuFill = 'assets/icons/Menu-fill.svg'; static const String iconsMenuFill = 'assets/icons/Menu-fill.svg';
static const String iconsMinus = 'assets/icons/minus.svg'; static const String iconsMinus = 'assets/icons/minus.svg';
static const String iconsPlus = 'assets/icons/plus.svg'; static const String iconsPlus = 'assets/icons/plus.svg';
static const String iconsQrScan = 'assets/icons/qr_scan.svg';
static const String iconsRoutines = 'assets/icons/Routines.svg'; static const String iconsRoutines = 'assets/icons/Routines.svg';
static const String iconsRoutinesFill = 'assets/icons/Routines-fill.svg'; static const String iconsRoutinesFill = 'assets/icons/Routines-fill.svg';
static const String iconsScreen = 'assets/icons/Screen.svg'; static const String iconsScreen = 'assets/icons/Screen.svg';
static const String iconsSettings = 'assets/icons/settings.svg';
static const String iconsSummer = 'assets/icons/Summer.svg'; static const String iconsSummer = 'assets/icons/Summer.svg';
static const String iconsSummerMode = 'assets/icons/summer_mode.svg'; static const String iconsSummerMode = 'assets/icons/summer_mode.svg';
static const String iconsSunnyMode = 'assets/icons/sunnyMode.svg'; static const String iconsSunnyMode = 'assets/icons/sunnyMode.svg';

View File

@ -1,9 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_app/features/app_layout/view/app_layout.dart';
import 'package:syncrow_app/features/auth/view/auth_view.dart'; import 'package:syncrow_app/features/auth/view/auth_view.dart';
import 'package:syncrow_app/features/auth/view/widgets/didnt_get_code/didnt_get_code_view.dart';
import 'package:syncrow_app/features/auth/view/widgets/login/login_view.dart';
import 'package:syncrow_app/features/auth/view/widgets/one_time_password/one_time_password_view.dart';
import 'package:syncrow_app/features/auth/view/widgets/privacy_policy/privacy_policy_view.dart';
import 'package:syncrow_app/features/auth/view/widgets/sign_up/sign_up_view.dart';
import 'package:syncrow_app/features/auth/view/widgets/user_agreement/user_agreement_view.dart';
import 'package:syncrow_app/features/dashboard/view/dashboard_view.dart'; import 'package:syncrow_app/features/dashboard/view/dashboard_view.dart';
import 'package:syncrow_app/features/devices/view/devices_view.dart'; import 'package:syncrow_app/features/devices/view/devices_view.dart';
import 'package:syncrow_app/features/layout/view/layout_view.dart'; import 'package:syncrow_app/features/layout/view/layout_view.dart';
import 'package:syncrow_app/features/app_layout/view/app_layout.dart';
import 'package:syncrow_app/features/profile/view/profile_view.dart'; import 'package:syncrow_app/features/profile/view/profile_view.dart';
import 'package:syncrow_app/features/scene/view/scene_view.dart'; import 'package:syncrow_app/features/scene/view/scene_view.dart';
import 'package:syncrow_app/features/splash/view/splash_view.dart'; import 'package:syncrow_app/features/splash/view/splash_view.dart';
@ -37,6 +43,30 @@ class Router {
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) => const AuthView(), settings: settings); builder: (_) => const AuthView(), settings: settings);
case Routes.authLogin:
return MaterialPageRoute(
builder: (_) => const LoginView(), settings: settings);
case Routes.authOneTimePassword:
return MaterialPageRoute(
builder: (_) => const OneTimePasswordView(), settings: settings);
case Routes.authSignUp:
return MaterialPageRoute(
builder: (_) => const SignUpView(), settings: settings);
case Routes.policyRoute:
return MaterialPageRoute(
builder: (_) => const PrivacyPolicyView(), settings: settings);
case Routes.termsRoute:
return MaterialPageRoute(
builder: (_) => const UserAgreementView(), settings: settings);
case Routes.authDidNotGetCode:
return MaterialPageRoute(
builder: (_) => const DidntGetCodeView(), settings: settings);
case Routes.dashboardRoute: case Routes.dashboardRoute:
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) => const DashboardView(), settings: settings); builder: (_) => const DashboardView(), settings: settings);

View File

@ -7,4 +7,11 @@ class Routes {
static const String layoutRoute = '/layout'; static const String layoutRoute = '/layout';
static const String profileRoute = '/profile'; static const String profileRoute = '/profile';
static const String authRoute = '/auth'; static const String authRoute = '/auth';
static const String authLogin = '$authRoute/login';
static const String authSignUp = '$authRoute/signup';
static const String authOneTimePassword = '$authRoute/one-time-password';
static const String authForgotPassword = '$authRoute/forgot-password';
static const String authDidNotGetCode = '$authRoute/did-not-get-code';
static const String policyRoute = '/policy';
static const String termsRoute = '/terms';
} }

View File

@ -1,3 +1,11 @@
abstract class ApiEndpoints { abstract class ApiEndpoints {
static const String apiKey = ''; static const String baseUrl = 'http://100.107.182.63:4001';
static const String signUp = '$baseUrl/authentication/user/signup';
static const String login = '$baseUrl/authentication/user/login';
static const String deleteUser = '$baseUrl/authentication/user/delete/{id}';
static const String sendOtp = '$baseUrl/authentication/user/send-otp';
static const String verifyOtp = '$baseUrl/authentication/user/verify-otp';
static const String forgetPassword =
'$baseUrl/authentication/user/forget-password';
} }

View File

@ -0,0 +1,140 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:syncrow_app/features/auth/model/token.dart';
import 'package:syncrow_app/features/auth/model/verify_code.dart';
import 'package:syncrow_app/services/api/api_links_endpoints.dart';
import 'package:syncrow_app/services/api/http_service.dart';
class AuthenticationAPI {
static Future<Token> verifyPassCode(VerifyPassCode data) async {
final response = await HTTPService().post(
path: ApiEndpoints.verifyOtp,
body: data.toJson(),
showServerMessage: false,
expectedResponseModel: (json) {
Token token = Token.fromJson(json);
return token;
});
return response;
}
static Future<Token> loginWithEmail(
{required String email, required String password}) async {
final response = await HTTPService().post(
path: ApiEndpoints.login,
body: jsonEncode({
"email": email,
"password": password,
}),
showServerMessage: false,
expectedResponseModel: (json) {
Token token = Token.fromJson(json['data']);
return token;
});
debugPrint("response: $response");
return response;
}
// static Future<SuccessResponse> updateUserInfo(
// Map<String, dynamic> data) async {
// final response = await HTTPService().postRequest(
// path: APIConstants.updateUserInfo,
// body: data,
// expectedResponseModel: (json) {
// SuccessResponse token = SuccessResponse.fromJson(json);
// return token;
// });
// return response;
// }
//
// static Future<LoginWithPhoneResponse> loginWithPhone(
// {required LoginWithPhone data, String? recaptchaToken}) async {
// final response = await HTTPService().postRequest(
// path: APIConstants.loginWithChannel,
// body: data.toJson(),
// options: Options(headers: {
// 'captcha-token': recaptchaToken,
// 'captcha-site-key': Platform.isAndroid
// ? dotenv.env['RECAPTCH_ANDROID_SITE_KEY'] ?? ''
// : dotenv.env['RECAPTCH_IOS_SITE_KEY'] ?? ''
// }),
// expectedResponseModel: (json) {
// LoginWithPhoneResponse result = LoginWithPhoneResponse.fromJson(json);
// return result;
// });
// return response;
// }
//
// static Future<List<dynamic>> countryCodes() async {
// final response = await HTTPService().postRequest(
// path: APIConstants.getCountyCode,
// expectedResponseModel: (json) {
// List<dynamic> result = json.toList();
// return result;
// });
// return response;
// }
//
// static Future<bool> sendNotificationToken({
// required Map<String, String> data,
// }) async {
// final response = await HTTPService().postRequest(
// path: APIConstants.notificationToken,
// body: data,
// showServerMessage: false,
// expectedResponseModel: (json) {
// bool checked = false;
// if (json != null) {
// if (json['success']) {
// checked = true;
// }
// }
// return checked;
// });
// return response;
// }
//
// static Future<bool> logout() async {
// final response = await HTTPService().postRequest(
// path: APIConstants.logout,
// expectedResponseModel: (json) {
// bool checked = false;
// // print(json);
// if (json != null) {
// if (json['success']) {
// checked = true;
// }
// }
// return checked;
// });
// return response;
// }
//
// static Future<bool> deleteAccount() async {
// final response = await HTTPService().postRequest(
// path: APIConstants.deleteAccount,
// expectedResponseModel: (json) {
// bool checked = false;
// if (json != null) {
// if (json['success']) {
// checked = true;
// }
// }
// return checked;
// });
// return response;
// }
// static Future<Token> refreshToken(Map<String, dynamic> data) async {
// final response = await HTTPService().postRequest(
// path: APIConstants.refreshToken,
// showServerMessage: false,
// body: data,
// expectedResponseModel: (json) {
// Token token = Token.fromJson(json);
// return token;
// });
// return response;
// }
}

View File

@ -1,44 +1,42 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
class HTTPInterceptor extends InterceptorsWrapper { class HTTPInterceptor extends InterceptorsWrapper {
// @override
@override // void onResponse(Response response, ResponseInterceptorHandler handler) async {
void onResponse(Response response, ResponseInterceptorHandler handler) async { // // Pass the response to the next interceptor or response handler.
// Pass the response to the next interceptor or response handler. // return handler.next(response);
return handler.next(response); // }
} //
// @override
@override // void onRequest(RequestOptions options,
void onRequest(RequestOptions options, // RequestInterceptorHandler handler) async {
RequestInterceptorHandler handler) async { // // TODO: Implement logic for adding headers to requests.
// TODO: Implement logic for adding headers to requests. // // This method is called before a request is sent.
// This method is called before a request is sent. // super.onRequest(options, handler);
super.onRequest(options, handler); // }
} //
// @override
@override // void onError(DioException err, ErrorInterceptorHandler handler) async {
void onError(DioException err, ErrorInterceptorHandler handler) async { // // TODO: Implement error handling logic.
// TODO: Implement error handling logic. // // This method is called when an error occurs during a request.
// This method is called when an error occurs during a request. // super.onError(err, handler);
super.onError(err, handler); // }
} //
// /// Validates the response and returns true if it is successful (status code 2xx).
/// Validates the response and returns true if it is successful (status code 2xx). // Future<bool> validateResponse(Response response) async {
Future<bool> validateResponse(Response response) async { // if (response.statusCode != null) {
if (response.statusCode != null) { // if (response.statusCode! >= 200 && response.statusCode! < 300) {
if (response.statusCode! >= 200 && response.statusCode! < 300) { // // If the response status code is within the successful range (2xx),
// If the response status code is within the successful range (2xx), // // return true indicating a successful response.
// return true indicating a successful response. // return true;
return true; // } else {
} else { // // If the response status code is not within the successful range (2xx),
// If the response status code is not within the successful range (2xx), // // return false indicating an unsuccessful response.
// return false indicating an unsuccessful response. // return false;
return false; // }
} // } else {
} else { // // If the response status code is null, return false indicating an unsuccessful response.
// If the response status code is null, return false indicating an unsuccessful response. // return false;
return false; // }
} // }
}
} }

View File

@ -1,42 +1,32 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_app/services/api/api_links_endpoints.dart';
import 'package:syncrow_app/services/api/http_interceptor.dart';
import 'package:syncrow_app/services/locator.dart'; import 'package:syncrow_app/services/locator.dart';
import 'http_interceptor.dart';
class HTTPService { class HTTPService {
final Dio client = locator<Dio>(); late Dio client = serviceLocator.get<Dio>();
final navigatorKey = GlobalKey<NavigatorState>();
// final navigatorKey = GlobalKey<NavigatorState>();
String certificateString = ""; String certificateString = "";
static Dio setupDioClient() { Dio setupDioClient() {
final client = Dio( client = Dio(
BaseOptions( BaseOptions(
// TODO add base url baseUrl: ApiEndpoints.baseUrl,
// baseUrl: URLConstants.baseURL,
receiveDataWhenStatusError: true, receiveDataWhenStatusError: true,
followRedirects: false, followRedirects: false,
connectTimeout: const Duration(milliseconds: 60000), connectTimeout: const Duration(minutes: 1),
receiveTimeout: const Duration(milliseconds: 60000), receiveTimeout: const Duration(minutes: 1),
), ),
); );
// (client.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
// client. = (X509Certificate cert, String host, int port) { client.interceptors.add(serviceLocator.get<HTTPInterceptor>());
// // TODO add SSL certificate
// // if(cert.pem == certificateString){ // Verify the certificate
// // return true;
// // }
// // return false;
// return true;
// };
// return client;
// };
client.interceptors.add(locator<HTTPInterceptor>());
return client; return client;
} }
Future<T> getRequest<T>({ Future<T> get<T>({
required String path, required String path,
Map<String, dynamic>? queryParameters, Map<String, dynamic>? queryParameters,
required T Function(dynamic) expectedResponseModel, required T Function(dynamic) expectedResponseModel,
@ -55,7 +45,7 @@ class HTTPService {
} }
} }
Future<T> postRequest<T>( Future<T> post<T>(
{required String path, {required String path,
Map<String, dynamic>? queryParameters, Map<String, dynamic>? queryParameters,
Options? options, Options? options,
@ -65,16 +55,18 @@ class HTTPService {
try { try {
final response = await client.post(path, final response = await client.post(path,
data: body, queryParameters: queryParameters, options: options); data: body, queryParameters: queryParameters, options: options);
print("post response is $response");
debugPrint("status code is ${response.statusCode}"); debugPrint("status code is ${response.statusCode}");
debugPrint("response data is ${response.data}");
return expectedResponseModel(response.data); return expectedResponseModel(response.data);
} catch (error) { } catch (error) {
debugPrint("******* Error"); debugPrint("******* Error ********");
debugPrint(error.toString()); debugPrint(error.toString());
rethrow; rethrow;
} }
} }
Future<T> patchRequest<T>( Future<T> patch<T>(
{required String path, {required String path,
Map<String, dynamic>? queryParameters, Map<String, dynamic>? queryParameters,
dynamic body, dynamic body,
@ -94,7 +86,7 @@ class HTTPService {
} }
} }
Future<T> downloadRequest<T>( Future<T> download<T>(
{required String path, {required String path,
required String savePath, required String savePath,
Map<String, dynamic>? queryParameters, Map<String, dynamic>? queryParameters,
@ -118,4 +110,24 @@ class HTTPService {
rethrow; rethrow;
} }
} }
//delete
Future<T> delete<T>({
required String path,
Map<String, dynamic>? queryParameters,
required T Function(dynamic) expectedResponseModel,
bool showServerMessage = true,
}) async {
try {
final response = await client.delete(
path,
queryParameters: queryParameters,
);
return expectedResponseModel(response.data);
} catch (error) {
debugPrint("******* Error");
debugPrint(error.toString());
rethrow;
}
}
} }

View File

@ -1,14 +1,13 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:syncrow_app/services/api/http_interceptor.dart';
import 'package:syncrow_app/services/api/http_service.dart'; import 'package:syncrow_app/services/api/http_service.dart';
import 'api/http_interceptor.dart'; final GetIt serviceLocator = GetIt.instance;
GetIt locator = GetIt.instance;
// setupLocator() // to search for dependency injection in flutter // setupLocator() // to search for dependency injection in flutter
initialSetup() async { initialSetup() {
locator.registerSingleton<HTTPInterceptor>(HTTPInterceptor()); serviceLocator.registerSingleton<HTTPInterceptor>(HTTPInterceptor());
//Base classes //Base classes
locator.registerSingleton<Dio>(HTTPService.setupDioClient());
serviceLocator.registerSingleton<Dio>(HTTPService().setupDioClient());
} }

View File

@ -0,0 +1,21 @@
import 'dart:convert';
String decodeBase64(String str) {
//'-', '+' 62nd char of encoding, '_', '/' 63rd char of encoding
String output = str.replaceAll('-', '+').replaceAll('_', '/');
switch (output.length % 4) {
// Pad with trailing '='
case 0: // No pad chars in this case
break;
case 2: // Two pad chars
output += '==';
break;
case 3: // One pad char
output += '=';
break;
default:
throw Exception('Illegal base64url string!"');
}
return utf8.decode(base64Url.decode(output));
}

View File

@ -2,4 +2,8 @@ abstract class Constants {
static const String languageCode = "en"; static const String languageCode = "en";
static const String countryCode = "US"; static const String countryCode = "US";
static const double defaultPadding = 16.0;
static const String tokenKey = 'userToken';
} }