Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d29db54
feat: Implemented forgot password and verify OTP functionality
tulsi-7span Jun 26, 2025
6ed300f
Fix: Moved pinput dependency from dev_dependencies to dependencies
tulsi-7span Jun 26, 2025
3435374
Add const keyword for ResendEmailEvent in VerifyOTPBloc
tulsi-7span Jun 26, 2025
e4ce88e
- Added `did_not_receive_otp` key to `en.i18n.json`.
tulsi-7span Jun 26, 2025
7a86f52
Refactor: Improve OTP verification and UI elements
tulsi-7span Jun 26, 2025
9b690e4
Fix: Added mounted checks in VerifyOTPScreen timer callbacks
tulsi-7span Jun 26, 2025
6e7ed9b
Update COUNTER
tulsi-7span Jun 26, 2025
4d53226
Merge remote-tracking branch 'origin/main' into feat/forget-pwd
tulsi-7span Jun 26, 2025
1bc9240
Updated app_textfield.dart
tulsi-7span Jun 26, 2025
a5934fa
Feat: Add AppTimer widget and integrate with OTP verification
tulsi-7span Jun 26, 2025
d3750e0
Refactor: Streamline AppTimer and Verify OTP UI
tulsi-7span Jun 27, 2025
878f64b
Fix: Prevent `AppTimer` from starting if initial seconds is 0
tulsi-7span Jun 27, 2025
d3595cb
Fix: AppTimer displays minutes and seconds
tulsi-7span Jun 27, 2025
cd4c593
Refactor: Improve OTP verification and UI
tulsi-7span Jun 27, 2025
84584fd
Refactor: Update OTP verification and forgot password functionality
tulsi-7span Jun 27, 2025
2843236
Refactor: Standardize email property in SetEmailEvent
tulsi-7span Jun 27, 2025
76986cc
Refactor: Update VerifyOTPScreen and related BLoC
tulsi-7span Jun 27, 2025
c20d64a
Refactor: Removed unused `EmailValidator` import
tulsi-7span Jun 27, 2025
d01a50e
Remove unused initState from VerifyOTPScreen
tulsi-7span Jun 27, 2025
f84714a
Fix: Adjusted button padding on Verify OTP screen
tulsi-7span Jun 27, 2025
f8dc4c3
Refactor: Removed unnecessary Padding from VerifyOTPScreen
tulsi-7span Jun 27, 2025
c36ea3c
Refactor: Introduce AppOtpInput widget and update dependencies
tulsi-7span Jun 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/app_core/lib/app/config/api_endpoints.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class ApiEndpoints {
static const login = '/api/v1/login';
static const signup = '/api/register';
static const forgotPassword = '/api/forgotPassword';
static const verifyOTP = '/api/verifyOTP';
static const profile = '/api/users';
static const logout = '/api/users';
static const socialLogin = '/auth/socialLogin/';
Expand Down
10 changes: 5 additions & 5 deletions apps/app_core/lib/app/routes/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import 'package:app_core/modules/auth/sign_in/screens/sign_in_screen.dart';
import 'package:app_core/modules/auth/sign_up/screens/sign_up_screen.dart';
import 'package:app_core/modules/bottom_navigation_bar.dart';
import 'package:app_core/modules/change_password/screen/change_password_screen.dart';
import 'package:app_core/modules/forgot_password/screens/forgot_password_screen.dart';
import 'package:app_core/modules/home/screen/home_screen.dart';
import 'package:app_core/modules/profile/screen/edit_profile_screen.dart';
import 'package:app_core/modules/profile/screen/profile_screen.dart';
import 'package:app_core/modules/splash/splash_screen.dart';
import 'package:app_core/modules/subscription/screen/subscription_screen.dart';
import 'package:app_core/modules/verify_otp/screens/verify_otp_screen.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/cupertino.dart';

Expand All @@ -22,6 +24,8 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: SubscriptionRoute.page),
AutoRoute(initial: true, page: SplashRoute.page, path: '/'),
AutoRoute(page: SignInRoute.page),
AutoRoute(page: ForgotPasswordRoute.page),
AutoRoute(page: VerifyOTPRoute.page),
AutoRoute(
page: BottomNavigationBarRoute.page,
guards: [AuthGuard()],
Expand All @@ -32,11 +36,7 @@ class AppRouter extends RootStackRouter {
path: 'account',
children: [
AutoRoute(page: ProfileRoute.page),
AutoRoute(
page: ChangePasswordRoute.page,
path: 'change-password',
meta: const {'hideNavBar': true},
),
AutoRoute(page: ChangePasswordRoute.page, path: 'change-password', meta: const {'hideNavBar': true}),
],
),
],
Expand Down
18 changes: 18 additions & 0 deletions apps/app_core/lib/modules/auth/model/auth_request_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class AuthRequestModel {
this.oneSignalPlayerId,
});

AuthRequestModel.verifyOTP({required this.email, required this.token});

AuthRequestModel.forgotPassword({required this.email});

String? email;
String? name;
String? password;
Expand All @@ -18,6 +22,7 @@ class AuthRequestModel {
String? providerId;
String? providerToken;
String? oneSignalPlayerId;
String? token;

Map<String, dynamic> toMap() {
final map = <String, dynamic>{};
Expand All @@ -28,6 +33,13 @@ class AuthRequestModel {
return map;
}

Map<String, dynamic> toVerifyOTPMap() {
final map = <String, dynamic>{};
map['email'] = email;
map['token'] = token;
return map;
}

Map<String, dynamic> toSocialSignInMap() {
final map = <String, dynamic>{};
map['name'] = name;
Expand All @@ -40,4 +52,10 @@ class AuthRequestModel {
map['oneSignalPlayerId'] = oneSignalPlayerId;
return map;
}

Map<String, dynamic> toForgotPasswordMap() {
final map = <String, dynamic>{};
map['email'] = email;
return map;
}
}
118 changes: 59 additions & 59 deletions apps/app_core/lib/modules/auth/repository/auth_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ abstract interface class IAuthRepository {

TaskEither<Failure, bool> logout();

TaskEither<Failure, Unit> socialLogin({
required AuthRequestModel requestModel,
});
TaskEither<Failure, void> forgotPassword(AuthRequestModel authRequestModel);

TaskEither<Failure, Unit> socialLogin({required AuthRequestModel requestModel});

TaskEither<Failure, AuthResponseModel> verifyOTP(AuthRequestModel authRequestModel);
}

// ignore: comment_references
Expand All @@ -31,48 +33,28 @@ class AuthRepository implements IAuthRepository {
const AuthRepository();

@override
TaskEither<Failure, Unit> login(
AuthRequestModel authRequestModel,
) => makeLoginRequest(authRequestModel)
TaskEither<Failure, Unit> login(AuthRequestModel authRequestModel) => makeLoginRequest(authRequestModel)
.chainEither(RepositoryUtils.checkStatusCode)
.chainEither(
(response) => RepositoryUtils.mapToModel(() {
return AuthResponseModel.fromMap(
response.data as Map<String, dynamic>,
);
return AuthResponseModel.fromMap(response.data as Map<String, dynamic>);
}),
)
.flatMap(saveUserToLocal);

TaskEither<Failure, Response> makeLoginRequest(
AuthRequestModel authRequestModel,
) => userApiClient.request(
TaskEither<Failure, Response> makeLoginRequest(AuthRequestModel authRequestModel) => userApiClient.request(
requestType: RequestType.post,
path: ApiEndpoints.login,
body: authRequestModel.toMap(),
options: Options(
headers: {
'x-api-key': 'reqres-free-v1',
'Content-Type': 'application/json',
},
),
options: Options(headers: {'x-api-key': 'reqres-free-v1', 'Content-Type': 'application/json'}),
);

TaskEither<Failure, Unit> saveUserToLocal(
AuthResponseModel authResponseModel,
) => getIt<IHiveService>().setUserData(
UserModel(
name: 'user name',
email: 'user email',
profilePicUrl: '',
id: int.parse(authResponseModel.id),
),
TaskEither<Failure, Unit> saveUserToLocal(AuthResponseModel authResponseModel) => getIt<IHiveService>().setUserData(
UserModel(name: 'user name', email: 'user email', profilePicUrl: '', id: int.parse(authResponseModel.id)),
);

@override
TaskEither<Failure, Unit> signup(
AuthRequestModel authRequestModel,
) => makeSignUpRequest(authRequestModel)
TaskEither<Failure, Unit> signup(AuthRequestModel authRequestModel) => makeSignUpRequest(authRequestModel)
.chainEither(RepositoryUtils.checkStatusCode)
.chainEither(
(r) => RepositoryUtils.mapToModel(() {
Expand All @@ -82,27 +64,20 @@ class AuthRepository implements IAuthRepository {
// return AuthResponseModel.fromMap(
// r.data as Map<String, dynamic>,
// );
return AuthResponseModel(
email: '[email protected]',
id: (r.data as Map<String, dynamic>)['id'].toString(),
);
return AuthResponseModel(email: '[email protected]', id: (r.data as Map<String, dynamic>)['id'].toString());
}),
)
.flatMap(saveUserToLocal);

TaskEither<Failure, Response> makeSignUpRequest(
AuthRequestModel authRequestModel,
) => userApiClient.request(
TaskEither<Failure, Response> makeSignUpRequest(AuthRequestModel authRequestModel) => userApiClient.request(
requestType: RequestType.post,
path: ApiEndpoints.signup,
body: authRequestModel.toMap(),
options: Options(headers: {'Content-Type': 'application/json'}),
);

TaskEither<Failure, Unit> _clearHiveData() => TaskEither.tryCatch(
() => getIt<LogoutService>().logout().run(),
(error, stackTrace) => APIFailure(),
);
TaskEither<Failure, Unit> _clearHiveData() =>
TaskEither.tryCatch(() => getIt<LogoutService>().logout().run(), (error, stackTrace) => APIFailure());

@override
TaskEither<Failure, bool> logout() => makeLogoutRequest().flatMap(
Expand All @@ -111,14 +86,11 @@ class AuthRepository implements IAuthRepository {
}),
);

TaskEither<Failure, String> _getNotificationId() =>
TaskEither.tryCatch(() {
return getIt<NotificationServiceInterface>()
.getNotificationSubscriptionId();
}, APIFailure.new);
TaskEither<Failure, String> _getNotificationId() => TaskEither.tryCatch(() {
return getIt<NotificationServiceInterface>().getNotificationSubscriptionId();
}, APIFailure.new);

TaskEither<Failure, Response>
makeLogoutRequest() => _getNotificationId().flatMap(
TaskEither<Failure, Response> makeLogoutRequest() => _getNotificationId().flatMap(
(playerID) => userApiClient.request(
requestType: RequestType.delete,

Expand All @@ -130,26 +102,54 @@ class AuthRepository implements IAuthRepository {
);

@override
TaskEither<Failure, Unit> socialLogin({
required AuthRequestModel requestModel,
}) => makeSocialLoginRequest(requestModel: requestModel)
TaskEither<Failure, Unit> socialLogin({required AuthRequestModel requestModel}) => makeSocialLoginRequest(
requestModel: requestModel,
)
.chainEither(RepositoryUtils.checkStatusCode)
.chainEither(
(response) => RepositoryUtils.mapToModel<AuthResponseModel>(
() => AuthResponseModel.fromMap(
response.data as Map<String, dynamic>,
),
),
(response) =>
RepositoryUtils.mapToModel<AuthResponseModel>(() => AuthResponseModel.fromMap(response.data as Map<String, dynamic>)),
)
.flatMap(saveUserToLocal);

TaskEither<Failure, Response> makeSocialLoginRequest({
required AuthRequestModel requestModel,
}) {
TaskEither<Failure, Response> makeSocialLoginRequest({required AuthRequestModel requestModel}) {
return userApiClient.request(
requestType: RequestType.post,
path: ApiEndpoints.socialLogin,
body: requestModel.toSocialSignInMap(),
);
}

@override
TaskEither<Failure, void> forgotPassword(AuthRequestModel authRequestModel) => makeForgotPasswordRequest(authRequestModel)
.chainEither(RepositoryUtils.checkStatusCode)
.chainEither(
(response) => RepositoryUtils.mapToModel(() {
return response.data;
}),
)
.map((_) {});

TaskEither<Failure, Response> makeForgotPasswordRequest(AuthRequestModel authRequestModel) => userApiClient.request(
requestType: RequestType.post,
path: ApiEndpoints.forgotPassword,
body: authRequestModel.toForgotPasswordMap(),
options: Options(headers: {'x-api-key': 'reqres-free-v1', 'Content-Type': 'application/json'}),
);

@override
TaskEither<Failure, AuthResponseModel> verifyOTP(AuthRequestModel authRequestModel) => makeVerifyOTPRequest(authRequestModel)
.chainEither(RepositoryUtils.checkStatusCode)
.chainEither(
(response) => RepositoryUtils.mapToModel(() {
return AuthResponseModel.fromMap(response.data as Map<String, dynamic>);
}),
);

TaskEither<Failure, Response> makeVerifyOTPRequest(AuthRequestModel authRequestModel) => userApiClient.request(
requestType: RequestType.post,
path: ApiEndpoints.verifyOTP,
body: authRequestModel.toVerifyOTPMap(),
options: Options(headers: {'Content-Type': 'application/json'}),
);
}
44 changes: 23 additions & 21 deletions apps/app_core/lib/modules/auth/sign_in/screens/sign_in_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ class SignInPage extends StatelessWidget implements AutoRouteWrapper {
providers: [RepositoryProvider(create: (context) => const AuthRepository())],
child: MultiBlocProvider(
providers: [
BlocProvider(
create:
(context) => SignInBloc(
authenticationRepository: RepositoryProvider.of<AuthRepository>(context),
),
),
BlocProvider(create: (context) => SignInBloc(authenticationRepository: RepositoryProvider.of<AuthRepository>(context))),
],
child: this,
),
Expand Down Expand Up @@ -61,21 +56,32 @@ class SignInPage extends StatelessWidget implements AutoRouteWrapper {
children: [
VSpace.xxxxlarge80(),
VSpace.large24(),
const SlideAndFadeAnimationWrapper(
delay: 100,
child: Center(child: FlutterLogo(size: 100)),
),
const SlideAndFadeAnimationWrapper(delay: 100, child: Center(child: FlutterLogo(size: 100))),
VSpace.xxlarge40(),
VSpace.large24(),
SlideAndFadeAnimationWrapper(
delay: 200,
child: AppText.XL(text: context.t.sign_in),
),
SlideAndFadeAnimationWrapper(delay: 200, child: AppText.XL(text: context.t.sign_in)),
VSpace.large24(),
SlideAndFadeAnimationWrapper(delay: 300, child: _EmailInput()),
VSpace.large24(),
SlideAndFadeAnimationWrapper(delay: 400, child: _PasswordInput()),
VSpace.large24(),
AnimatedGestureDetector(
onTap: () {
context.pushRoute(const ForgotPasswordRoute());
},
child: SlideAndFadeAnimationWrapper(
delay: 200,
child: Align(
alignment: Alignment.topRight,
child: AppText.regular10(
fontSize: 14,
text: context.t.forgot_password,
color: context.colorScheme.primary400,
),
),
),
),
VSpace.large24(),
SlideAndFadeAnimationWrapper(delay: 400, child: _UserConsentWidget()),
VSpace.xxlarge40(),
const SlideAndFadeAnimationWrapper(delay: 500, child: _LoginButton()),
Expand All @@ -84,8 +90,7 @@ class SignInPage extends StatelessWidget implements AutoRouteWrapper {
VSpace.large24(),
const SlideAndFadeAnimationWrapper(delay: 600, child: _ContinueWithGoogleButton()),
VSpace.large24(),
if (Platform.isIOS)
const SlideAndFadeAnimationWrapper(delay: 600, child: _ContinueWithAppleButton()),
if (Platform.isIOS) const SlideAndFadeAnimationWrapper(delay: 600, child: _ContinueWithAppleButton()),
],
),
),
Expand Down Expand Up @@ -125,8 +130,7 @@ class _PasswordInput extends StatelessWidget {
label: context.t.password,
textInputAction: TextInputAction.done,
onChanged: (password) => context.read<SignInBloc>().add(SignInPasswordChanged(password)),
errorText:
state.password.displayError != null ? context.t.common_validation_password : null,
errorText: state.password.displayError != null ? context.t.common_validation_password : null,
autofillHints: const [AutofillHints.password],
);
},
Expand All @@ -143,9 +147,7 @@ class _UserConsentWidget extends StatelessWidget {
return UserConsentWidget(
value: isUserConsent,
onCheckBoxValueChanged: (userConsent) {
context.read<SignInBloc>().add(
SignInUserConsentChangedEvent(userConsent: userConsent ?? false),
);
context.read<SignInBloc>().add(SignInUserConsentChangedEvent(userConsent: userConsent ?? false));
},
onTermsAndConditionTap:
() => launchUrl(
Expand Down
Loading