feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
import 'dart:developer';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/data/static/email_validation_constants.dart';
import 'package:krow/features/profile/email_verification/domain/bloc/email_verification_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
@injectable
class EmailVerificationService {
static const recentLoginRequired = 'requires-recent-login';
static const reLoginRequired = 'requires-re-login';
static const tokenExpired = 'user-token-expired';
final FirebaseAuth _auth = FirebaseAuth.instance;
String? _currentUserPhone;
String get currentUserPhone {
_currentUserPhone ??= _auth.currentUser?.phoneNumber;
return _currentUserPhone ?? '';
}
Future<void> sendVerificationEmail({required String email}) async {
log('Sending email verification $email');
await _auth.currentUser?.verifyBeforeUpdateEmail(
email,
// TODO: Enabling this code will require enabling Firebase Dynamic Links.
// ActionCodeSettings(
// url: 'https://staging.app.krow.develop.express/emailLinkAuth/',
// androidPackageName: dotenv.get('ANDROID_PACKAGE'),
// iOSBundleId: dotenv.get('IOS_BUNDLE_ID'),
// androidInstallApp: true,
// handleCodeInApp: true,
// linkDomain: 'krow-staging.firebaseapp.com',
// ),
);
final sharedPrefs = await SharedPreferences.getInstance();
sharedPrefs.setString(
EmailValidationConstants.storedEmailKey,
email,
);
}
bool checkEmailForVerification({required String email}) {
final user = _auth.currentUser;
if (user == null || user.email == null) return false;
return email == user.email && user.emailVerified;
}
Future<bool> isUserEmailVerified({
required String newEmail,
}) async {
await _auth.currentUser?.reload();
final user = _auth.currentUser;
log(
'Current user email: ${user?.email}, '
'verified: ${user?.emailVerified}, '
'Expected email: $newEmail',
);
if (user == null) {
throw FirebaseAuthException(code: reLoginRequired);
}
return (user.emailVerified) && user.email == newEmail;
}
ReAuthRequirement isReAuthenticationRequired(String errorCode) {
return switch (errorCode) {
recentLoginRequired => ReAuthRequirement.recent,
reLoginRequired || tokenExpired => ReAuthRequirement.immediate,
_ => ReAuthRequirement.none,
};
}
}

View File

@@ -0,0 +1,132 @@
import 'dart:async';
import 'dart:developer';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/sevices/auth_state_service/auth_service.dart';
import 'package:krow/features/profile/email_verification/data/email_verification_service.dart';
part 'email_verification_event.dart';
part 'email_verification_state.dart';
/// This BLoC is currently unfinished.
/// We faced an issue with the Firebase email verification process. As a
/// security-sensitive action, it requires recent user authentication (seems
/// like around 5-10 minutes), so we have to prompt the user to re-login. Only
/// after that can the verification email be sent.
/// But upon receiving the email and confirming it, another issue arises: the
/// current auth token becomes invalid, and the user is silently signed out from
/// Firebase. On mobile, it can be solved by integrating a deep-link into the
/// verification email, which will allow us to re-authenticate the user with
/// an email link credential in the background.
/// However, there is a possibility that the user might verify his email on a
/// desktop (or any other device), so the link won't be received. In this case,
/// we need to prompt the user to re-authenticate yet again. This will probably
/// be a bad user experience.
class EmailVerificationBloc
extends Bloc<EmailVerificationEvent, EmailVerificationState> {
EmailVerificationBloc() : super(const EmailVerificationState()) {
on<SetVerifiedData>((event, emit) async {
emit(state.copyWith(
email: event.verifiedEmail,
userPhone: event.userPhone,
));
add(const SendVerificationEmail());
});
on<SendVerificationEmail>((event, emit) async {
try {
await _verificationService.sendVerificationEmail(email: state.email);
emailCheckTimer?.cancel();
emailCheckTimer = Timer.periodic(
const Duration(seconds: 3),
(_) => add(const CheckEmailStatus()),
);
} catch (except) {
log(
'Error in EmailVerificationBloc. On SetVerifiedEmail event',
error: except,
);
if (except is FirebaseAuthException) {
emit(
state.copyWith(
reAuthRequirement:
_verificationService.isReAuthenticationRequired(except.code),
),
);
}
} finally {
emit(state.copyWith(reAuthRequirement: ReAuthRequirement.none));
}
});
on<CheckEmailStatus>((event, emit) async {
bool isEmailVerified = false;
try {
isEmailVerified = await _verificationService.isUserEmailVerified(
newEmail: state.email,
);
} catch (except) {
log(
'Error in EmailVerificationBloc. On CheckEmailStatus event',
error: except,
);
if (except is FirebaseAuthException) {
emit(
state.copyWith(
reAuthRequirement:
_verificationService.isReAuthenticationRequired(except.code),
),
);
}
}
emit(
state.copyWith(
status: isEmailVerified ? StateStatus.success : StateStatus.idle,
),
);
if (state.status == StateStatus.success) emailCheckTimer?.cancel();
});
on<ResendVerificationEmail>((event, emit) {
emit(state.copyWith(reAuthRequirement: ReAuthRequirement.none));
add(const SendVerificationEmail());
});
on<IncomingVerificationLink>((event, emit) async {
try {
await getIt<AuthService>().signInWithEmailLink(
link: event.verificationLink,
);
emit(state.copyWith(status: StateStatus.success));
} catch (except) {
log(
'Error in EmailVerificationBloc. On IncomingVerificationLink event',
error: except,
);
}
if (state.status == StateStatus.success) emailCheckTimer?.cancel();
});
}
final _verificationService = getIt<EmailVerificationService>();
Timer? emailCheckTimer;
@override
Future<void> close() {
emailCheckTimer?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,31 @@
part of 'email_verification_bloc.dart';
@immutable
sealed class EmailVerificationEvent {
const EmailVerificationEvent();
}
class SetVerifiedData extends EmailVerificationEvent {
const SetVerifiedData({required this.verifiedEmail, required this.userPhone});
final String verifiedEmail;
final String userPhone;
}
class SendVerificationEmail extends EmailVerificationEvent {
const SendVerificationEmail();
}
class CheckEmailStatus extends EmailVerificationEvent {
const CheckEmailStatus();
}
class ResendVerificationEmail extends EmailVerificationEvent {
const ResendVerificationEmail();
}
class IncomingVerificationLink extends EmailVerificationEvent {
const IncomingVerificationLink({required this.verificationLink});
final Uri verificationLink;
}

View File

@@ -0,0 +1,32 @@
part of 'email_verification_bloc.dart';
@immutable
class EmailVerificationState {
const EmailVerificationState({
this.email = '',
this.status = StateStatus.idle,
this.reAuthRequirement = ReAuthRequirement.none,
this.userPhone = '',
});
final String email;
final StateStatus status;
final ReAuthRequirement reAuthRequirement;
final String userPhone;
EmailVerificationState copyWith({
String? email,
StateStatus? status,
ReAuthRequirement? reAuthRequirement,
String? userPhone,
}) {
return EmailVerificationState(
email: email ?? this.email,
status: status ?? this.status,
reAuthRequirement: reAuthRequirement ?? this.reAuthRequirement,
userPhone: userPhone ?? this.userPhone,
);
}
}
enum ReAuthRequirement { none, recent, immediate }

View File

@@ -0,0 +1,105 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/scroll_layout_helper.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/features/profile/email_verification/domain/bloc/email_verification_bloc.dart';
import 'package:krow/features/profile/email_verification/presentation/widgets/bottom_control_button.dart';
import 'package:krow/features/profile/email_verification/presentation/widgets/verification_actions_widget.dart';
@RoutePage()
class EmailVerificationScreen extends StatefulWidget
implements AutoRouteWrapper {
const EmailVerificationScreen({
super.key,
required this.verifiedEmail,
required this.userPhone,
});
final String verifiedEmail;
final String userPhone;
@override
State<EmailVerificationScreen> createState() =>
_EmailVerificationScreenState();
@override
Widget wrappedRoute(BuildContext context) {
return BlocProvider<EmailVerificationBloc>(
create: (context) => EmailVerificationBloc()
..add(SetVerifiedData(
verifiedEmail: verifiedEmail,
userPhone: userPhone,
)),
child: this,
);
}
}
class _EmailVerificationScreenState extends State<EmailVerificationScreen> {
StreamSubscription<Uri>? _appLinkSubscription;
@override
void initState() {
super.initState();
final context = this.context;
_appLinkSubscription = AppLinks().uriLinkStream.listen(
(link) {
if (!context.mounted) return;
context
.read<EmailVerificationBloc>()
.add(IncomingVerificationLink(verificationLink: link));
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(
titleText: 'email_verification'.tr(),
),
body: ScrollLayoutHelper(
padding: const EdgeInsets.all(16),
upperWidget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(4),
Text(
'check_your_email'.tr(),
style: AppTextStyles.headingH1,
textAlign: TextAlign.start,
),
const Gap(8),
Text(
'verification_link_sent'
.tr(args: [widget.verifiedEmail]),
style: AppTextStyles.bodyMediumReg.copyWith(
color: AppColors.blackGray,
),
textAlign: TextAlign.start,
),
const Gap(24),
const VerificationActionsWidget(),
],
),
lowerWidget: const BottomControlButton(),
),
);
}
@override
void dispose() {
_appLinkSubscription?.cancel();
super.dispose();
}
}

View File

@@ -0,0 +1,68 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_loading_overlay.dart';
import 'package:krow/features/profile/email_verification/domain/bloc/email_verification_bloc.dart';
class BottomControlButton extends StatelessWidget {
const BottomControlButton({super.key});
void _listenHandler(BuildContext context, EmailVerificationState state) {
if (state.reAuthRequirement == ReAuthRequirement.none) return;
KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.negative,
barrierDismissible: false,
title: 'additional_action_needed'.tr(),
message: state.reAuthRequirement == ReAuthRequirement.recent
? 'email_verification_security_sensitive'.tr(args: [state.userPhone])
: 'unable_to_validate_email_status'.tr(),
primaryButtonLabel: 'Continue'.tr(),
onPrimaryButtonPressed: (dialogContext) async {
final isReAuthenticated = await dialogContext.pushRoute<bool>(
PhoneReLoginFlowRoute(userPhone: state.userPhone),
);
if (dialogContext.mounted) Navigator.maybePop(dialogContext);
if (isReAuthenticated == true &&
context.mounted &&
state.reAuthRequirement == ReAuthRequirement.recent) {
context
.read<EmailVerificationBloc>()
.add(const ResendVerificationEmail());
}
},
);
}
@override
Widget build(BuildContext context) {
return BlocConsumer<EmailVerificationBloc, EmailVerificationState>(
buildWhen: (previous, current) => previous.status != current.status,
listenWhen: (previous, current) =>
previous.reAuthRequirement != current.reAuthRequirement,
listener: _listenHandler,
builder: (context, state) {
return KwLoadingOverlay(
shouldShowLoading: state.status == StateStatus.loading,
child: KwButton.primary(
label: 'Continue'.tr(),
disabled: state.status != StateStatus.success,
onPressed: () {
context.maybePop(true);
},
),
);
},
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/profile/email_verification/domain/bloc/email_verification_bloc.dart';
class VerificationActionsWidget extends StatefulWidget {
const VerificationActionsWidget({super.key});
@override
State<VerificationActionsWidget> createState() =>
_VerificationActionsWidgetState();
}
//TODO: Finish this widget incorporating increasing timer on each code resend.
class _VerificationActionsWidgetState extends State<VerificationActionsWidget> {
double secondsToHold = 1;
bool onHold = false;
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.sizeOf(context).height;
return Center(
child: Padding(
padding: EdgeInsets.only(top: screenHeight / 4.6),
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
text: 'didnt_receive'.tr(),
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
children: [
TextSpan(
text: 'resend'.tr(),
style: AppTextStyles.bodyMediumSmb,
recognizer: TapGestureRecognizer()
..onTap = () {
context
.read<EmailVerificationBloc>()
.add(const ResendVerificationEmail());
},
),
TextSpan(
text: ' ${'or'.tr()} ',
style: AppTextStyles.bodyMediumReg
.copyWith(color: AppColors.blackGray),
),
TextSpan(
text: 'contact_support_1'.tr(),
style: AppTextStyles.bodyMediumSmb,
recognizer: TapGestureRecognizer()..onTap = () {
//TODO: Add Contact support action.
},
),
],
),
),
),
);
}
}