Skip to content

Firebase to custom api migration #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
May 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ca67338
chore: remove firebase.json
fulleni May 25, 2025
cc8a7e3
refactor: rename auth packages in pubspec.yaml
fulleni May 25, 2025
f10bf83
refactor: Refactor app with new data layer
fulleni May 25, 2025
37a1b20
chore: remove unused ht_preferences dependencies
fulleni May 25, 2025
4dd01c1
chore: update analysis options
fulleni May 25, 2025
07fd3cd
build: update dependency order in pubspec.yaml
fulleni May 25, 2025
f4e4817
chore: remove firebase_options.dart
fulleni May 25, 2025
84a33f6
refactor: remove unnecessary casts in App widget
fulleni May 25, 2025
e0bd142
feat(account): load user content preferences
fulleni May 25, 2025
b49d5f4
feat: Migrate to generic data repositories
fulleni May 25, 2025
db19d19
feat(auth): Refactor authentication flow
fulleni May 25, 2025
4da6ac0
refactor: use generic data repository
fulleni May 25, 2025
b6db174
chore(auth): remove email link sent page
fulleni May 25, 2025
8a90183
feat(account): add content preferences page
fulleni May 25, 2025
6afd8c9
feat(account): add saved headlines page
fulleni May 25, 2025
02c2841
feat(auth): add email code verification page
fulleni May 25, 2025
9285415
feat(auth): add email sign-in page
fulleni May 25, 2025
065c01c
refactor(categories): use generic data repo
fulleni May 25, 2025
c5501ae
refactor(countries): use generic data repository
fulleni May 25, 2025
f11a113
refactor: use generic data repository
fulleni May 25, 2025
ef581fd
refactor(sources): use generic data repository
fulleni May 25, 2025
5e60ee7
refactor: use shared models for headline filter
fulleni May 25, 2025
5700d69
refactor(feed): use shared Category model
fulleni May 25, 2025
31cebf3
feat(feed): country filter page update
fulleni May 25, 2025
1a69e44
feat(headlines): use shared models for filtering
fulleni May 25, 2025
728d9cf
refactor(feed): remove unused repository import
fulleni May 25, 2025
8808a9c
feat(feed): Show source headquarters in feed
fulleni May 25, 2025
6c90036
refactor(search): use generic data repository
fulleni May 25, 2025
4821119
refactor: Refactor router and auth flow
fulleni May 25, 2025
fa40ea9
refactor(settings): migrate to data repository
fulleni May 25, 2025
a8067d0
refactor(settings): migrate to ht_shared types
fulleni May 25, 2025
4e596f6
refactor(settings): use AppTextScaleFactor enum
fulleni May 25, 2025
d28b4ec
refactor(settings): use HeadlineImageStyle enum
fulleni May 25, 2025
6bbc947
feat(settings): Disable notification settings UI
fulleni May 25, 2025
89b47cc
refactor(theme): use AppTextScaleFactor
fulleni May 25, 2025
8405113
feat: Add account and email code labels
fulleni May 25, 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
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ analyzer:
errors:
avoid_catches_without_on_clauses: ignore
avoid_print: ignore
document_ignores: ignore
lines_longer_than_80_chars: ignore
use_if_null_to_convert_nulls_to_bools: ignore
include: package:very_good_analysis/analysis_options.7.0.0.yaml
Expand Down
1 change: 0 additions & 1 deletion firebase.json

This file was deleted.

83 changes: 62 additions & 21 deletions lib/account/bloc/account_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:ht_authentication_repository/ht_authentication_repository.dart';
import 'package:ht_auth_repository/ht_auth_repository.dart';
import 'package:ht_data_repository/ht_data_repository.dart';
import 'package:ht_shared/ht_shared.dart'
show HtHttpException, User, UserContentPreferences;

part 'account_event.dart';
part 'account_state.dart';
Expand All @@ -10,41 +15,77 @@ part 'account_state.dart';
/// {@endtemplate}
class AccountBloc extends Bloc<AccountEvent, AccountState> {
/// {@macro account_bloc}
AccountBloc({required HtAuthenticationRepository authenticationRepository})
: _authenticationRepository = authenticationRepository,
super(const AccountState()) {
on<AccountLogoutRequested>(_onLogoutRequested);
AccountBloc({
required HtAuthRepository authenticationRepository,
required HtDataRepository<UserContentPreferences>
userContentPreferencesRepository,
}) : _authenticationRepository = authenticationRepository,
_userContentPreferencesRepository = userContentPreferencesRepository,
super(const AccountState()) {
// Listen to authentication state changes from the repository
_authenticationRepository.authStateChanges.listen(
(user) => add(_AccountUserChanged(user: user)),
);

on<_AccountUserChanged>(_onAccountUserChanged);
on<AccountLoadContentPreferencesRequested>(
_onAccountLoadContentPreferencesRequested,
);
// Handlers for AccountSettingsNavigationRequested and
// AccountBackupNavigationRequested are typically handled in the UI layer
// (e.g., BlocListener navigating) or could emit specific states if needed.
// For now, we only need the logout logic here.
}

final HtAuthenticationRepository _authenticationRepository;
final HtAuthRepository _authenticationRepository;
final HtDataRepository<UserContentPreferences>
_userContentPreferencesRepository;

/// Handles [_AccountUserChanged] events.
///
/// Updates the state with the current user and triggers loading
/// of user preferences if the user is authenticated.
Future<void> _onAccountUserChanged(
_AccountUserChanged event,
Emitter<AccountState> emit,
) async {
emit(state.copyWith(user: event.user));
if (event.user != null) {
// User is authenticated, load preferences
add(AccountLoadContentPreferencesRequested(userId: event.user!.id));
} else {
// User is unauthenticated, clear preferences
emit(state.copyWith());
}
}

/// Handles the [AccountLogoutRequested] event.
/// Handles [AccountLoadContentPreferencesRequested] events.
///
/// Attempts to sign out the user using the [HtAuthenticationRepository].
/// Emits [AccountStatus.loading] before the operation and updates to
/// [AccountStatus.success] or [AccountStatus.failure] based on the outcome.
Future<void> _onLogoutRequested(
AccountLogoutRequested event,
/// Attempts to load the user's content preferences.
Future<void> _onAccountLoadContentPreferencesRequested(
AccountLoadContentPreferencesRequested event,
Emitter<AccountState> emit,
) async {
emit(state.copyWith(status: AccountStatus.loading));
try {
await _authenticationRepository.signOut();
// No need to emit success here. The AppBloc listening to the
// repository's user stream will handle the global state change
// and trigger the necessary UI updates/redirects.
// We can emit an initial state again if needed for this BLoC's
// local state.
emit(state.copyWith(status: AccountStatus.initial));
final preferences = await _userContentPreferencesRepository.read(
id: event.userId,
userId: event.userId, // Preferences are user-scoped
);
emit(
state.copyWith(status: AccountStatus.success, preferences: preferences),
);
} on HtHttpException catch (e) {
emit(
state.copyWith(
status: AccountStatus.failure,
errorMessage: 'Failed to load preferences: ${e.message}',
),
);
} catch (e) {
emit(
state.copyWith(
status: AccountStatus.failure,
errorMessage: 'Logout failed: $e',
errorMessage: 'An unexpected error occurred: $e',
),
);
}
Expand Down
42 changes: 29 additions & 13 deletions lib/account/bloc/account_event.dart
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
part of 'account_bloc.dart';

/// Base class for all events related to the Account feature.
abstract class AccountEvent extends Equatable {
/// {@template account_event}
/// Base class for Account events.
/// {@endtemplate}
sealed class AccountEvent extends Equatable {
/// {@macro account_event}
const AccountEvent();

@override
List<Object> get props => [];
List<Object?> get props => [];
}

/// Event triggered when the user requests to navigate to the settings page.
class AccountSettingsNavigationRequested extends AccountEvent {
const AccountSettingsNavigationRequested();
}
/// {@template _account_user_changed}
/// Internal event triggered when the authenticated user changes.
/// {@endtemplate}
final class _AccountUserChanged extends AccountEvent {
/// {@macro _account_user_changed}
const _AccountUserChanged({required this.user});

/// The current authenticated user, or null if unauthenticated.
final User? user;

/// Event triggered when the user requests to log out.
class AccountLogoutRequested extends AccountEvent {
const AccountLogoutRequested();
@override
List<Object?> get props => [user];
}

/// Event triggered when the user (anonymous) requests to backup/link account.
class AccountBackupNavigationRequested extends AccountEvent {
const AccountBackupNavigationRequested();
/// {@template account_load_content_preferences_requested}
/// Event triggered when the user's content preferences need to be loaded.
/// {@endtemplate}
final class AccountLoadContentPreferencesRequested extends AccountEvent {
/// {@macro account_load_content_preferences_requested}
const AccountLoadContentPreferencesRequested({required this.userId});

/// The ID of the user whose content preferences should be loaded.
final String userId;

@override
List<Object> get props => [userId];
}
53 changes: 43 additions & 10 deletions lib/account/bloc/account_state.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,59 @@
part of 'account_bloc.dart';

/// Enum representing the status of the Account feature.
enum AccountStatus { initial, loading, success, failure }
/// Defines the status of the account state.
enum AccountStatus {
/// The initial state.
initial,

/// Represents the state of the Account feature.
class AccountState extends Equatable {
const AccountState({this.status = AccountStatus.initial, this.errorMessage});
/// An operation is in progress.
loading,

/// The current status of the account feature operations.
/// An operation was successful.
success,

/// An operation failed.
failure,
}

/// {@template account_state}
/// State for the Account feature.
/// {@endtemplate}
final class AccountState extends Equatable {
/// {@macro account_state}
const AccountState({
this.status = AccountStatus.initial,
this.user,
this.preferences,
this.errorMessage,
});

/// The current status of the account state.
final AccountStatus status;

/// An optional error message if an operation failed.
/// The currently authenticated user.
final User? user;

/// The user's content preferences.
final UserContentPreferences? preferences;

/// An error message if an operation failed.
final String? errorMessage;

/// Creates a copy of the current state with updated values.
AccountState copyWith({AccountStatus? status, String? errorMessage}) {
/// Creates a copy of this [AccountState] with the given fields replaced.
AccountState copyWith({
AccountStatus? status,
User? user,
UserContentPreferences? preferences,
String? errorMessage,
}) {
return AccountState(
status: status ?? this.status,
user: user ?? this.user,
preferences: preferences ?? this.preferences,
errorMessage: errorMessage ?? this.errorMessage,
);
}

@override
List<Object?> get props => [status, errorMessage];
List<Object?> get props => [status, user, preferences, errorMessage];
}
Loading
Loading