Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
3 changes: 1 addition & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ Future<void> main() async {
}

final container = ProviderContainer();
final firebase = container.read(firebaseServiceProvider);

firebase.log('App starting...');

Expand Down Expand Up @@ -83,7 +82,7 @@ Future<void> main() async {
final authRepository = container.read(authRepositoryProvider);
final user = await authRepository.getUser();
final initialLocation = user != null ? AppRoutes.home : AppRoutes.intro;
final router = createAppRouter(firebase, initialLocation: initialLocation);
final router = createAppRouter(initialLocation: initialLocation);

runApp(
UncontrolledProviderScope(
Expand Down
6 changes: 2 additions & 4 deletions lib/router/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ abstract class AppRoutes {
static const about = '/about';
}

/// Creates a configured [GoRouter] with the provided [firebase] service
/// for analytics observers, starting at [initialLocation].
GoRouter createAppRouter(
FirebaseService firebase, {
/// Creates a configured [GoRouter] starting at [initialLocation].
GoRouter createAppRouter({
required String initialLocation,
}) => GoRouter(
navigatorKey: rootNavigatorKey,
Expand Down
25 changes: 12 additions & 13 deletions lib/services/firebase_service.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// Global toggle for Firebase features.
///
Expand All @@ -11,25 +10,27 @@ const bool useFirebase = bool.fromEnvironment(
defaultValue: false,
);

/// Provider for the [FirebaseService].
final firebaseServiceProvider = Provider<FirebaseService>((ref) {
return FirebaseService();
});

/// Unified service for Firebase Analytics and Crashlytics.
///
/// Exposes nullable getters that return real instances when [useFirebase] is
/// true, or `null` when disabled. Callers use null-aware access:
///
/// ```dart
/// ref.read(firebaseServiceProvider).analytics?.logAppOpen();
/// ref.read(firebaseServiceProvider).crashlytics?.recordError(e, stack);
/// firebase.analytics?.logAppOpen();
/// firebase.crashlytics?.recordError(e, stack);
/// ```
class FirebaseService {
const FirebaseService();

/// The [FirebaseAnalytics] instance, or `null` if Firebase is disabled.
FirebaseAnalytics? get analytics =>
useFirebase ? FirebaseAnalytics.instance : null;

/// Returns a [FirebaseAnalyticsObserver] for use with navigation observers, or
/// `null` if Firebase is disabled.
FirebaseAnalyticsObserver? get analyticsObserver =>
useFirebase ? FirebaseAnalyticsObserver(analytics: analytics!) : null;

/// The [FirebaseCrashlytics] instance, or `null` if Firebase is disabled.
FirebaseCrashlytics? get crashlytics =>
useFirebase ? FirebaseCrashlytics.instance : null;
Expand All @@ -41,9 +42,7 @@ class FirebaseService {
void log(String message) {
crashlytics?.log(message);
}

/// Returns a [FirebaseAnalyticsObserver] for use with navigation observers, or
/// `null` if Firebase is disabled.
FirebaseAnalyticsObserver? get analyticsObserver =>
useFirebase ? FirebaseAnalyticsObserver(analytics: analytics!) : null;
}

/// Global [FirebaseService] instance.
const firebase = FirebaseService();
10 changes: 2 additions & 8 deletions lib/services/portal_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:dio_redirect_interceptor/dio_redirect_interceptor.dart';
import 'package:html/parser.dart';
import 'package:http_parser/http_parser.dart';
import 'package:riverpod/riverpod.dart';
import 'package:tattoo/services/firebase_service.dart';
import 'package:tattoo/utils/http.dart';

/// Represents a logged-in NTUT Portal user.
Expand Down Expand Up @@ -44,7 +43,7 @@ enum PortalServiceCode {

/// Provides the singleton [PortalService] instance.
final portalServiceProvider = Provider<PortalService>((ref) {
return PortalService(ref.read(firebaseServiceProvider));
return PortalService();
});

/// Service for authenticating with NTUT Portal and performing SSO.
Expand All @@ -60,9 +59,8 @@ final portalServiceProvider = Provider<PortalService>((ref) {
/// calling [sso] for each required service.
class PortalService {
late final Dio _portalDio;
final FirebaseService _firebase;

PortalService(this._firebase) {
PortalService() {
// Emulate the NTUT iOS app's HTTP client
_portalDio = createDio()
..options.baseUrl = 'https://app.ntut.edu.tw/'
Expand All @@ -83,20 +81,16 @@ class PortalService {
///
/// Throws an [Exception] if login fails due to invalid credentials.
Future<UserDto> login(String username, String password) async {
_firebase.log('Attempting login');
final response = await _portalDio.post(
'login.do',
queryParameters: {'muid': username, 'mpassword': password},
);

final body = jsonDecode(response.data);
if (!body['success']) {
_firebase.log('Login failed');
throw Exception('Login failed. Please check your credentials.');
}

_firebase.log('Login successful');

final String? passwordExpiredRemind = body['passwordExpiredRemind'];

// Normalize empty strings to null for consistency
Expand Down
8 changes: 7 additions & 1 deletion lib/utils/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:intl/intl.dart';
// ignore: implementation_imports
import 'package:dio/src/transformers/util/consolidate_bytes.dart';
import 'package:tattoo/services/firebase_service.dart';

export 'package:dio/dio.dart';

Expand Down Expand Up @@ -83,6 +84,9 @@ class PlainTextTransformer extends BackgroundTransformer {
}

/// One-line log [Interceptor] for requests and responses.
///
/// Logs to both `dart:developer` and Firebase Crashlytics for breadcrumb
/// context in crash reports.
class LogInterceptor extends Interceptor {
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
Expand Down Expand Up @@ -119,7 +123,9 @@ class LogInterceptor extends Interceptor {
if (cookies > 0) "$cookies cookie${cookies != 1 ? 's' : ''}",
].join(' ');

log("$requestLog => $responseLog", name: 'HTTP');
final message = "$requestLog => $responseLog";
log(message, name: 'HTTP');
firebase.log(message);
handler.next(response);
}
}
Expand Down
3 changes: 1 addition & 2 deletions test/services/course_service_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tattoo/models/course.dart';
import 'package:tattoo/services/course_service.dart';
import 'package:tattoo/services/firebase_service.dart';
import 'package:tattoo/services/portal_service.dart';

import '../test_helpers.dart';
Expand All @@ -16,7 +15,7 @@ void main() {
});

setUp(() async {
portalService = PortalService(FirebaseService());
portalService = PortalService();
courseService = CourseService();

await portalService.login(
Expand Down
5 changes: 2 additions & 3 deletions test/services/i_school_plus_service_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tattoo/services/firebase_service.dart';
import 'package:tattoo/services/i_school_plus_service.dart';
import 'package:tattoo/services/portal_service.dart';

Expand All @@ -14,7 +13,7 @@ void main() {
setUpAll(() async {
TestCredentials.validate();

portalService = PortalService(FirebaseService());
portalService = PortalService();
iSchoolPlusService = ISchoolPlusService();

await portalService.login(
Expand All @@ -33,7 +32,7 @@ void main() {
});

setUp(() async {
portalService = PortalService(FirebaseService());
portalService = PortalService();
iSchoolPlusService = ISchoolPlusService();

await portalService.login(
Expand Down
3 changes: 1 addition & 2 deletions test/services/portal_service_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:tattoo/services/firebase_service.dart';
import 'package:tattoo/services/portal_service.dart';
import 'package:tattoo/utils/http.dart';

Expand All @@ -14,7 +13,7 @@ void main() {
});

setUp(() async {
portalService = PortalService(FirebaseService());
portalService = PortalService();
await respectfulDelay();
});

Expand Down
3 changes: 1 addition & 2 deletions test/services/student_query_service_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:tattoo/models/ranking.dart';
import 'package:tattoo/models/score.dart';
import 'package:tattoo/models/user.dart';
import 'package:tattoo/services/firebase_service.dart';
import 'package:tattoo/services/portal_service.dart';
import 'package:tattoo/services/student_query_service.dart';

Expand All @@ -18,7 +17,7 @@ void main() {
});

setUp(() async {
portalService = PortalService(FirebaseService());
portalService = PortalService();
studentQueryService = StudentQueryService();

await portalService.login(
Expand Down