Skip to content

Commit 9718541

Browse files
committed
feat: implement account management feature
- Added account page and linking - Implemented logout functionality - Handle anonymous account linking
1 parent 5992103 commit 9718541

15 files changed

+668
-63
lines changed

lib/account/bloc/account_bloc.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:equatable/equatable.dart';
3+
import 'package:ht_authentication_repository/ht_authentication_repository.dart';
4+
5+
part 'account_event.dart';
6+
part 'account_state.dart';
7+
8+
/// {@template account_bloc}
9+
/// BLoC responsible for managing the state and logic for the Account feature.
10+
/// {@endtemplate}
11+
class AccountBloc extends Bloc<AccountEvent, AccountState> {
12+
/// {@macro account_bloc}
13+
AccountBloc({required HtAuthenticationRepository authenticationRepository})
14+
: _authenticationRepository = authenticationRepository,
15+
super(const AccountState()) {
16+
on<AccountLogoutRequested>(_onLogoutRequested);
17+
// Handlers for AccountSettingsNavigationRequested and
18+
// AccountBackupNavigationRequested are typically handled in the UI layer
19+
// (e.g., BlocListener navigating) or could emit specific states if needed.
20+
// For now, we only need the logout logic here.
21+
}
22+
23+
final HtAuthenticationRepository _authenticationRepository;
24+
25+
/// Handles the [AccountLogoutRequested] event.
26+
///
27+
/// Attempts to sign out the user using the [HtAuthenticationRepository].
28+
/// Emits [AccountStatus.loading] before the operation and updates to
29+
/// [AccountStatus.success] or [AccountStatus.failure] based on the outcome.
30+
Future<void> _onLogoutRequested(
31+
AccountLogoutRequested event,
32+
Emitter<AccountState> emit,
33+
) async {
34+
emit(state.copyWith(status: AccountStatus.loading));
35+
try {
36+
await _authenticationRepository.signOut();
37+
// No need to emit success here. The AppBloc listening to the
38+
// repository's user stream will handle the global state change
39+
// and trigger the necessary UI updates/redirects.
40+
// We can emit an initial state again if needed for this BLoC's
41+
// local state.
42+
emit(state.copyWith(status: AccountStatus.initial));
43+
} catch (e) {
44+
emit(
45+
state.copyWith(
46+
status: AccountStatus.failure,
47+
errorMessage: 'Logout failed: $e',
48+
),
49+
);
50+
}
51+
}
52+
}

lib/account/bloc/account_event.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
part of 'account_bloc.dart';
2+
3+
/// Base class for all events related to the Account feature.
4+
abstract class AccountEvent extends Equatable {
5+
const AccountEvent();
6+
7+
@override
8+
List<Object> get props => [];
9+
}
10+
11+
/// Event triggered when the user requests to navigate to the settings page.
12+
class AccountSettingsNavigationRequested extends AccountEvent {
13+
const AccountSettingsNavigationRequested();
14+
}
15+
16+
/// Event triggered when the user requests to log out.
17+
class AccountLogoutRequested extends AccountEvent {
18+
const AccountLogoutRequested();
19+
}
20+
21+
/// Event triggered when the user (anonymous) requests to backup/link account.
22+
class AccountBackupNavigationRequested extends AccountEvent {
23+
const AccountBackupNavigationRequested();
24+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:equatable/equatable.dart';
3+
import 'package:ht_authentication_repository/ht_authentication_repository.dart';
4+
5+
part 'account_linking_event.dart';
6+
part 'account_linking_state.dart';
7+
8+
/// {@template account_linking_bloc}
9+
/// BLoC responsible for handling the logic for linking an anonymous account
10+
/// to a permanent one (e.g., Google, Email Link).
11+
/// {@endtemplate}
12+
class AccountLinkingBloc
13+
extends Bloc<AccountLinkingEvent, AccountLinkingState> {
14+
/// {@macro account_linking_bloc}
15+
AccountLinkingBloc({
16+
required HtAuthenticationRepository authenticationRepository,
17+
}) : _authenticationRepository = authenticationRepository,
18+
super(const AccountLinkingState()) {
19+
on<AccountLinkingGoogleSignInRequested>(_onGoogleSignInRequested);
20+
on<AccountLinkingEmailLinkSignInRequested>(_onEmailLinkSignInRequested);
21+
}
22+
23+
final HtAuthenticationRepository _authenticationRepository;
24+
25+
/// Handles the [AccountLinkingGoogleSignInRequested] event.
26+
Future<void> _onGoogleSignInRequested(
27+
AccountLinkingGoogleSignInRequested event,
28+
Emitter<AccountLinkingState> emit,
29+
) async {
30+
emit(state.copyWith(status: AccountLinkingStatus.loading));
31+
try {
32+
await _authenticationRepository.signInWithGoogle();
33+
// Success is implicit via AppBloc observing the user stream change.
34+
// Reset state locally or emit success if specific UI feedback needed.
35+
emit(state.copyWith(status: AccountLinkingStatus.success));
36+
} catch (e) {
37+
emit(
38+
state.copyWith(
39+
status: AccountLinkingStatus.failure,
40+
errorMessage: 'Google Sign-In failed: $e',
41+
),
42+
);
43+
}
44+
}
45+
46+
/// Handles the [AccountLinkingEmailLinkSignInRequested] event.
47+
Future<void> _onEmailLinkSignInRequested(
48+
AccountLinkingEmailLinkSignInRequested event,
49+
Emitter<AccountLinkingState> emit,
50+
) async {
51+
emit(state.copyWith(status: AccountLinkingStatus.loading));
52+
try {
53+
await _authenticationRepository.sendSignInLinkToEmail(email: event.email);
54+
emit(state.copyWith(status: AccountLinkingStatus.emailLinkSent));
55+
} catch (e) {
56+
emit(
57+
state.copyWith(
58+
status: AccountLinkingStatus.failure,
59+
errorMessage: 'Failed to send email link: $e',
60+
),
61+
);
62+
}
63+
}
64+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
part of 'account_linking_bloc.dart';
2+
3+
/// Base class for all events related to the Account Linking feature.
4+
abstract class AccountLinkingEvent extends Equatable {
5+
const AccountLinkingEvent();
6+
7+
@override
8+
List<Object> get props => [];
9+
}
10+
11+
/// Event triggered when the user attempts to sign in/link with Google.
12+
class AccountLinkingGoogleSignInRequested extends AccountLinkingEvent {
13+
const AccountLinkingGoogleSignInRequested();
14+
}
15+
16+
/// Event triggered when the user attempts to sign in/link with Email Link.
17+
class AccountLinkingEmailLinkSignInRequested extends AccountLinkingEvent {
18+
const AccountLinkingEmailLinkSignInRequested({required this.email});
19+
20+
final String email;
21+
22+
@override
23+
List<Object> get props => [email];
24+
}
25+
26+
// Add other events if different authentication methods are needed (e.g., Apple)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
part of 'account_linking_bloc.dart'; // Renamed part of directive
2+
3+
/// Enum representing the status of the Account Linking operations. // Updated doc
4+
enum AccountLinkingStatus {
5+
// Renamed enum
6+
/// Initial state, nothing has happened yet.
7+
initial,
8+
9+
/// An authentication/linking operation is in progress.
10+
loading,
11+
12+
/// An authentication/linking operation succeeded.
13+
/// The global AppBloc state change will handle navigation.
14+
success,
15+
16+
/// An authentication/linking operation failed.
17+
failure,
18+
19+
/// Email link has been sent successfully (for email link flow).
20+
emailLinkSent,
21+
}
22+
23+
/// Represents the state of the Account Linking feature.
24+
class AccountLinkingState extends Equatable {
25+
const AccountLinkingState({
26+
this.status = AccountLinkingStatus.initial,
27+
this.errorMessage,
28+
});
29+
30+
/// The current status of the linking operations.
31+
final AccountLinkingStatus status;
32+
33+
/// An optional error message if an operation failed.
34+
final String? errorMessage;
35+
36+
/// Creates a copy of the current state with updated values.
37+
AccountLinkingState copyWith({
38+
// Renamed return type
39+
AccountLinkingStatus? status,
40+
String? errorMessage,
41+
bool forceErrorMessage = false,
42+
}) {
43+
// Determine if the error message should be cleared.
44+
// Clear it if the status is changing *unless* the new status is failure
45+
// or the forceErrorMessage flag is true.
46+
final shouldClearError =
47+
status != null &&
48+
status != this.status &&
49+
status != AccountLinkingStatus.failure &&
50+
!forceErrorMessage;
51+
52+
return AccountLinkingState(
53+
status: status ?? this.status,
54+
errorMessage:
55+
shouldClearError ? null : (errorMessage ?? this.errorMessage),
56+
);
57+
}
58+
59+
@override
60+
List<Object?> get props => [status, errorMessage];
61+
}

lib/account/bloc/account_state.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
part of 'account_bloc.dart';
2+
3+
/// Enum representing the status of the Account feature.
4+
enum AccountStatus { initial, loading, success, failure }
5+
6+
/// Represents the state of the Account feature.
7+
class AccountState extends Equatable {
8+
const AccountState({this.status = AccountStatus.initial, this.errorMessage});
9+
10+
/// The current status of the account feature operations.
11+
final AccountStatus status;
12+
13+
/// An optional error message if an operation failed.
14+
final String? errorMessage;
15+
16+
/// Creates a copy of the current state with updated values.
17+
AccountState copyWith({AccountStatus? status, String? errorMessage}) {
18+
return AccountState(
19+
status: status ?? this.status,
20+
errorMessage: errorMessage ?? this.errorMessage,
21+
);
22+
}
23+
24+
@override
25+
List<Object?> get props => [status, errorMessage];
26+
}

0 commit comments

Comments
 (0)