diff --git a/assets/icons/qr_scan.svg b/assets/icons/qr_scan.svg new file mode 100644 index 0000000..ba9e926 --- /dev/null +++ b/assets/icons/qr_scan.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg new file mode 100644 index 0000000..5df067d --- /dev/null +++ b/assets/icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 1a4fd4f..c85aed0 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -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 { 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); + } } diff --git a/lib/features/auth/bloc/auth_state.dart b/lib/features/auth/bloc/auth_state.dart index 6c008c2..a48552c 100644 --- a/lib/features/auth/bloc/auth_state.dart +++ b/lib/features/auth/bloc/auth_state.dart @@ -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 {} diff --git a/lib/features/auth/model/token.dart b/lib/features/auth/model/token.dart new file mode 100644 index 0000000..95f45eb --- /dev/null +++ b/lib/features/auth/model/token.dart @@ -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 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 toJson() => {loginRefreshToken: refreshToken}; +} diff --git a/lib/features/auth/model/user_model.dart b/lib/features/auth/model/user_model.dart index 87049a7..bb052da 100644 --- a/lib/features/auth/model/user_model.dart +++ b/lib/features/auth/model/user_model.dart @@ -6,19 +6,26 @@ class UserModel { final String? phoneNumber; - final bool? isAnonymous; - final bool? isEmailVerified; 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({ 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 json) { + return UserModel( + id: json['userId'].toString(), + email: json['email'], + name: null, + photoUrl: null, + phoneNumber: null, + isEmailVerified: null, + isAgreementAccepted: null, + ); + } + Map toJson() { return { 'id': id, @@ -43,7 +62,6 @@ class UserModel { 'name': name, 'photoUrl': photoUrl, 'phoneNumber': phoneNumber, - 'isAnonymous': isAnonymous, 'isEmailVerified': isEmailVerified, 'isAgreementAccepted': isAgreementAccepted, }; diff --git a/lib/features/auth/model/verify_code.dart b/lib/features/auth/model/verify_code.dart new file mode 100644 index 0000000..da29c25 --- /dev/null +++ b/lib/features/auth/model/verify_code.dart @@ -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 json) => VerifyPassCode( + phone: json[verificationPhone], + passCode: json[verificationPassCode], + agent: json[verificationAgent], + deviceId: json[verificationDeviceId]); + + Map toJson() => { + verificationPhone: phone, + verificationPassCode: passCode, + verificationAgent: agent, + verificationDeviceId: deviceId, + }; +} diff --git a/lib/features/auth/view/widgets/auth_view_body.dart b/lib/features/auth/view/widgets/auth_view_body.dart index 9a85799..0673a87 100644 --- a/lib/features/auth/view/widgets/auth_view_body.dart +++ b/lib/features/auth/view/widgets/auth_view_body.dart @@ -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), ], ), ); diff --git a/lib/features/auth/view/widgets/didnt_get_code/didnt_get_code_view.dart b/lib/features/auth/view/widgets/didnt_get_code/didnt_get_code_view.dart new file mode 100644 index 0000000..cc1ec63 --- /dev/null +++ b/lib/features/auth/view/widgets/didnt_get_code/didnt_get_code_view.dart @@ -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(); + } +} diff --git a/lib/features/auth/view/widgets/login/login_user_agreement.dart b/lib/features/auth/view/widgets/login/login_user_agreement.dart new file mode 100644 index 0000000..a4e7ee8 --- /dev/null +++ b/lib/features/auth/view/widgets/login/login_user_agreement.dart @@ -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( + 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, + ), + ), + ], + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/auth/view/widgets/login/login_view.dart b/lib/features/auth/view/widgets/login/login_view.dart new file mode 100644 index 0000000..fd4e35b --- /dev/null +++ b/lib/features/auth/view/widgets/login/login_view.dart @@ -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( + 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(); + }, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/auth/view/widgets/one_time_password/one_time_password_view.dart b/lib/features/auth/view/widgets/one_time_password/one_time_password_view.dart new file mode 100644 index 0000000..1c24b91 --- /dev/null +++ b/lib/features/auth/view/widgets/one_time_password/one_time_password_view.dart @@ -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(); + } +} diff --git a/lib/features/auth/view/widgets/privacy_policy/privacy_policy_view.dart b/lib/features/auth/view/widgets/privacy_policy/privacy_policy_view.dart new file mode 100644 index 0000000..6516769 --- /dev/null +++ b/lib/features/auth/view/widgets/privacy_policy/privacy_policy_view.dart @@ -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(); + } +} diff --git a/lib/features/auth/view/widgets/sign_up/sign_up_view.dart b/lib/features/auth/view/widgets/sign_up/sign_up_view.dart new file mode 100644 index 0000000..ddd94b2 --- /dev/null +++ b/lib/features/auth/view/widgets/sign_up/sign_up_view.dart @@ -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(); + } +} diff --git a/lib/features/auth/view/widgets/user_agreement/user_agreement_view.dart b/lib/features/auth/view/widgets/user_agreement/user_agreement_view.dart new file mode 100644 index 0000000..f22c7a9 --- /dev/null +++ b/lib/features/auth/view/widgets/user_agreement/user_agreement_view.dart @@ -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(); + } +} diff --git a/lib/features/devices/bloc/lights/lights_cubit.dart b/lib/features/devices/bloc/lights/lights_cubit.dart index 0ff1f73..3df5821 100644 --- a/lib/features/devices/bloc/lights/lights_cubit.dart +++ b/lib/features/devices/bloc/lights/lights_cubit.dart @@ -9,10 +9,10 @@ class LightsCubit extends Cubit { static LightsCubit get(context) => BlocProvider.of(context); Map lightModes = { - 0: LightMode.Doze, - 1: LightMode.Relax, - 2: LightMode.Reading, - 3: LightMode.Energizing, + 0: LightMode.doze, + 1: LightMode.relax, + 2: LightMode.reading, + 3: LightMode.energizing, }; setLightingMode(LightModel light, LightMode mode) { @@ -55,8 +55,8 @@ class LightsCubit extends Cubit { } enum LightMode { - Doze, - Relax, - Reading, - Energizing, + doze, + relax, + reading, + energizing, } diff --git a/lib/features/devices/view/devices_view.dart b/lib/features/devices/view/devices_view.dart index e07b786..69358a1 100644 --- a/lib/features/devices/view/devices_view.dart +++ b/lib/features/devices/view/devices_view.dart @@ -12,10 +12,11 @@ class CategoriesView extends StatelessWidget { create: (context) => DevicesCubit(), child: BlocBuilder( builder: (context, state) => Container( - padding: const EdgeInsets.all(8), - width: MediaQuery.sizeOf(context).width, - height: MediaQuery.sizeOf(context).height, - child: const DevicesViewBody()), + padding: const EdgeInsets.all(8), + width: MediaQuery.sizeOf(context).width, + height: MediaQuery.sizeOf(context).height, + child: const DevicesViewBody(), + ), ), ); } diff --git a/lib/features/shared_widgets/default_text_button.dart b/lib/features/shared_widgets/default_text_button.dart index 6ee35de..db6bf49 100644 --- a/lib/features/shared_widgets/default_text_button.dart +++ b/lib/features/shared_widgets/default_text_button.dart @@ -4,6 +4,7 @@ import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; class DefaultTextButton extends StatelessWidget { const DefaultTextButton({ super.key, + this.enabled = true, this.onPressed, required this.text, this.isSecondary = false, @@ -13,15 +14,17 @@ class DefaultTextButton extends StatelessWidget { final String text; final bool isSecondary; + final bool enabled; + @override Widget build(BuildContext context) { return TextButton( - onPressed: onPressed, + onPressed: enabled ? onPressed : null, style: isSecondary ? null : ButtonStyle( - backgroundColor: - MaterialStateProperty.all(ColorsManager.primaryColor), + backgroundColor: MaterialStateProperty.all( + enabled ? ColorsManager.primaryColor : Colors.grey), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -30,7 +33,12 @@ class DefaultTextButton extends StatelessWidget { ), child: Text( text, - style: TextStyle(color: isSecondary ? Colors.black : Colors.white), + style: TextStyle( + color: isSecondary + ? Colors.black + : enabled + ? Colors.white + : Colors.black), ), ); } diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index ef13fa5..0c2d940 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -33,9 +33,11 @@ class Assets { static const String iconsMenuFill = 'assets/icons/Menu-fill.svg'; static const String iconsMinus = 'assets/icons/minus.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 iconsRoutinesFill = 'assets/icons/Routines-fill.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 iconsSummerMode = 'assets/icons/summer_mode.svg'; static const String iconsSunnyMode = 'assets/icons/sunnyMode.svg'; diff --git a/lib/navigation/router.dart b/lib/navigation/router.dart index 3f4ad07..9272f3f 100644 --- a/lib/navigation/router.dart +++ b/lib/navigation/router.dart @@ -1,9 +1,15 @@ 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/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/devices/view/devices_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/scene/view/scene_view.dart'; import 'package:syncrow_app/features/splash/view/splash_view.dart'; @@ -37,6 +43,30 @@ class Router { return MaterialPageRoute( 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: return MaterialPageRoute( builder: (_) => const DashboardView(), settings: settings); diff --git a/lib/navigation/routing_constants.dart b/lib/navigation/routing_constants.dart index 1f5ec34..d8cd7a5 100644 --- a/lib/navigation/routing_constants.dart +++ b/lib/navigation/routing_constants.dart @@ -7,4 +7,11 @@ class Routes { static const String layoutRoute = '/layout'; static const String profileRoute = '/profile'; 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'; } diff --git a/lib/services/api/api_links_endpoints.dart b/lib/services/api/api_links_endpoints.dart index decc473..592fffc 100644 --- a/lib/services/api/api_links_endpoints.dart +++ b/lib/services/api/api_links_endpoints.dart @@ -1,3 +1,11 @@ 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'; } diff --git a/lib/services/api/authentication_api.dart b/lib/services/api/authentication_api.dart new file mode 100644 index 0000000..4f8fea0 --- /dev/null +++ b/lib/services/api/authentication_api.dart @@ -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 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 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 updateUserInfo( +// Map 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 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> countryCodes() async { +// final response = await HTTPService().postRequest( +// path: APIConstants.getCountyCode, +// expectedResponseModel: (json) { +// List result = json.toList(); +// return result; +// }); +// return response; +// } +// +// static Future sendNotificationToken({ +// required Map 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 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 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 refreshToken(Map 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; +// } +} diff --git a/lib/services/api/http_interceptor.dart b/lib/services/api/http_interceptor.dart index a818f87..a247d35 100644 --- a/lib/services/api/http_interceptor.dart +++ b/lib/services/api/http_interceptor.dart @@ -1,44 +1,42 @@ import 'package:dio/dio.dart'; - class HTTPInterceptor extends InterceptorsWrapper { - - @override - void onResponse(Response response, ResponseInterceptorHandler handler) async { - // Pass the response to the next interceptor or response handler. - return handler.next(response); - } - - @override - void onRequest(RequestOptions options, - RequestInterceptorHandler handler) async { - // TODO: Implement logic for adding headers to requests. - // This method is called before a request is sent. - super.onRequest(options, handler); - } - - @override - void onError(DioException err, ErrorInterceptorHandler handler) async { - // TODO: Implement error handling logic. - // This method is called when an error occurs during a request. - super.onError(err, handler); - } - - /// Validates the response and returns true if it is successful (status code 2xx). - Future validateResponse(Response response) async { - if (response.statusCode != null) { - if (response.statusCode! >= 200 && response.statusCode! < 300) { - // If the response status code is within the successful range (2xx), - // return true indicating a successful response. - return true; - } else { - // If the response status code is not within the successful range (2xx), - // return false indicating an unsuccessful response. - return false; - } - } else { - // If the response status code is null, return false indicating an unsuccessful response. - return false; - } - } + // @override + // void onResponse(Response response, ResponseInterceptorHandler handler) async { + // // Pass the response to the next interceptor or response handler. + // return handler.next(response); + // } + // + // @override + // void onRequest(RequestOptions options, + // RequestInterceptorHandler handler) async { + // // TODO: Implement logic for adding headers to requests. + // // This method is called before a request is sent. + // super.onRequest(options, handler); + // } + // + // @override + // void onError(DioException err, ErrorInterceptorHandler handler) async { + // // TODO: Implement error handling logic. + // // This method is called when an error occurs during a request. + // super.onError(err, handler); + // } + // + // /// Validates the response and returns true if it is successful (status code 2xx). + // Future validateResponse(Response response) async { + // if (response.statusCode != null) { + // if (response.statusCode! >= 200 && response.statusCode! < 300) { + // // If the response status code is within the successful range (2xx), + // // return true indicating a successful response. + // return true; + // } else { + // // If the response status code is not within the successful range (2xx), + // // return false indicating an unsuccessful response. + // return false; + // } + // } else { + // // If the response status code is null, return false indicating an unsuccessful response. + // return false; + // } + // } } diff --git a/lib/services/api/http_service.dart b/lib/services/api/http_service.dart index 7e574f3..ffbf72f 100644 --- a/lib/services/api/http_service.dart +++ b/lib/services/api/http_service.dart @@ -1,42 +1,32 @@ import 'package:dio/dio.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 'http_interceptor.dart'; - class HTTPService { - final Dio client = locator(); - final navigatorKey = GlobalKey(); + late Dio client = serviceLocator.get(); + + // final navigatorKey = GlobalKey(); String certificateString = ""; - static Dio setupDioClient() { - final client = Dio( + Dio setupDioClient() { + client = Dio( BaseOptions( - // TODO add base url - // baseUrl: URLConstants.baseURL, + baseUrl: ApiEndpoints.baseUrl, receiveDataWhenStatusError: true, followRedirects: false, - connectTimeout: const Duration(milliseconds: 60000), - receiveTimeout: const Duration(milliseconds: 60000), + connectTimeout: const Duration(minutes: 1), + receiveTimeout: const Duration(minutes: 1), ), ); - // (client.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { - // client. = (X509Certificate cert, String host, int port) { - // // TODO add SSL certificate - // // if(cert.pem == certificateString){ // Verify the certificate - // // return true; - // // } - // // return false; - // return true; - // }; - // return client; - // }; - client.interceptors.add(locator()); + + client.interceptors.add(serviceLocator.get()); return client; } - Future getRequest({ + Future get({ required String path, Map? queryParameters, required T Function(dynamic) expectedResponseModel, @@ -55,7 +45,7 @@ class HTTPService { } } - Future postRequest( + Future post( {required String path, Map? queryParameters, Options? options, @@ -65,16 +55,18 @@ class HTTPService { try { final response = await client.post(path, data: body, queryParameters: queryParameters, options: options); + print("post response is $response"); debugPrint("status code is ${response.statusCode}"); + debugPrint("response data is ${response.data}"); return expectedResponseModel(response.data); } catch (error) { - debugPrint("******* Error"); + debugPrint("******* Error ********"); debugPrint(error.toString()); rethrow; } } - Future patchRequest( + Future patch( {required String path, Map? queryParameters, dynamic body, @@ -94,7 +86,7 @@ class HTTPService { } } - Future downloadRequest( + Future download( {required String path, required String savePath, Map? queryParameters, @@ -118,4 +110,24 @@ class HTTPService { rethrow; } } + + //delete + Future delete({ + required String path, + Map? 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; + } + } } diff --git a/lib/services/locator.dart b/lib/services/locator.dart index 6cbe235..56b4cf0 100644 --- a/lib/services/locator.dart +++ b/lib/services/locator.dart @@ -1,14 +1,13 @@ import 'package:dio/dio.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 'api/http_interceptor.dart'; - -GetIt locator = GetIt.instance; - +final GetIt serviceLocator = GetIt.instance; // setupLocator() // to search for dependency injection in flutter -initialSetup() async { - locator.registerSingleton(HTTPInterceptor()); +initialSetup() { + serviceLocator.registerSingleton(HTTPInterceptor()); //Base classes - locator.registerSingleton(HTTPService.setupDioClient()); + + serviceLocator.registerSingleton(HTTPService().setupDioClient()); } diff --git a/lib/utils/helpers/decode_base64.dart b/lib/utils/helpers/decode_base64.dart new file mode 100644 index 0000000..e047399 --- /dev/null +++ b/lib/utils/helpers/decode_base64.dart @@ -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)); +} diff --git a/lib/utils/resource_manager/constants.dart b/lib/utils/resource_manager/constants.dart index 34e955e..7a2f837 100644 --- a/lib/utils/resource_manager/constants.dart +++ b/lib/utils/resource_manager/constants.dart @@ -2,4 +2,8 @@ abstract class Constants { static const String languageCode = "en"; static const String countryCode = "US"; + + static const double defaultPadding = 16.0; + + static const String tokenKey = 'userToken'; }