diff --git a/.vscode/launch.json b/.vscode/launch.json index b4e33ce..c23b0e6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,13 +16,6 @@ "lib/main_development.dart" ] }, - { - "name": "Launch staging", - "request": "launch", - "type": "dart", - "program": "lib/main_staging.dart", - "args": ["--flavor", "staging", "--target", "lib/main_staging.dart"] - }, { "name": "Launch production", "request": "launch", diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 246797c..b8d2e31 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,28 +1,35 @@ import 'package:api_client/api_client.dart'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:dash_ai_search/counter/counter.dart'; import 'package:dash_ai_search/l10n/l10n.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class App extends StatelessWidget { const App({ required this.apiClient, + required this.user, super.key, }); final ApiClient apiClient; + final User user; @override Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData( - appBarTheme: AppBarTheme( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, + return Provider.value( + value: user, + child: MaterialApp( + theme: ThemeData( + appBarTheme: AppBarTheme( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + useMaterial3: true, ), - useMaterial3: true, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const CounterPage(), ), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - home: const CounterPage(), ); } } diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 558f731..02e7424 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:bloc/bloc.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/widgets.dart'; class AppBlocObserver extends BlocObserver { @@ -20,14 +21,20 @@ class AppBlocObserver extends BlocObserver { } } -Future bootstrap(FutureOr Function() builder) async { +typedef BootstrapBuilder = FutureOr Function(FirebaseAuth); + +Future bootstrap(BootstrapBuilder builder) async { + WidgetsFlutterBinding.ensureInitialized(); + FlutterError.onError = (details) { log(details.exceptionAsString(), stackTrace: details.stack); }; Bloc.observer = const AppBlocObserver(); - // Add cross-flavor configuration here - - runApp(await builder()); + runApp( + await builder( + FirebaseAuth.instance, + ), + ); } diff --git a/lib/main_development.dart b/lib/main_development.dart index 268fc3f..3464455 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -1,19 +1,33 @@ +import 'dart:async'; + import 'package:api_client/api_client.dart'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:dash_ai_search/app/app.dart'; import 'package:dash_ai_search/bootstrap.dart'; +import 'package:firebase_core/firebase_core.dart'; + +void main() async { + await Firebase.initializeApp(); + + unawaited( + bootstrap((firebaseAuth) async { + final authenticationRepository = AuthenticationRepository( + firebaseAuth: firebaseAuth, + ); -void main() { - bootstrap( - () { final apiClient = ApiClient( baseUrl: 'http://localhost:8080', - idTokenStream: const Stream.empty(), - refreshIdToken: () async => Future.value(), + idTokenStream: authenticationRepository.idToken, + refreshIdToken: authenticationRepository.refreshIdToken, ); + await authenticationRepository.signInAnonymously(); + await authenticationRepository.idToken.first; + return App( apiClient: apiClient, + user: await authenticationRepository.user.first, ); - }, + }), ); } diff --git a/lib/main_production.dart b/lib/main_production.dart index 42aeb07..b9e68bc 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -1,19 +1,33 @@ +import 'dart:async'; + import 'package:api_client/api_client.dart'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:dash_ai_search/app/app.dart'; import 'package:dash_ai_search/bootstrap.dart'; +import 'package:firebase_core/firebase_core.dart'; + +void main() async { + await Firebase.initializeApp(); + + unawaited( + bootstrap((firebaseAuth) async { + final authenticationRepository = AuthenticationRepository( + firebaseAuth: firebaseAuth, + ); -void main() { - bootstrap( - () { final apiClient = ApiClient( baseUrl: 'http://production', - idTokenStream: const Stream.empty(), - refreshIdToken: () async => Future.value(), + idTokenStream: authenticationRepository.idToken, + refreshIdToken: authenticationRepository.refreshIdToken, ); + await authenticationRepository.signInAnonymously(); + await authenticationRepository.idToken.first; + return App( apiClient: apiClient, + user: await authenticationRepository.user.first, ); - }, + }), ); } diff --git a/lib/main_staging.dart b/lib/main_staging.dart deleted file mode 100644 index 2c70729..0000000 --- a/lib/main_staging.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:api_client/api_client.dart'; -import 'package:dash_ai_search/app/app.dart'; -import 'package:dash_ai_search/bootstrap.dart'; - -void main() { - bootstrap( - () { - final apiClient = ApiClient( - baseUrl: 'http://staging', - idTokenStream: const Stream.empty(), - refreshIdToken: () async => Future.value(), - ); - - return App( - apiClient: apiClient, - ); - }, - ); -} diff --git a/packages/authentication_repository/.gitignore b/packages/authentication_repository/.gitignore new file mode 100644 index 0000000..06ef8e6 --- /dev/null +++ b/packages/authentication_repository/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/* + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Test related +coverage \ No newline at end of file diff --git a/packages/authentication_repository/README.md b/packages/authentication_repository/README.md new file mode 100644 index 0000000..3003c9a --- /dev/null +++ b/packages/authentication_repository/README.md @@ -0,0 +1,12 @@ +# Authentication Repository + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) +[![License: MIT][license_badge]][license_link] + +Repository to manage authentication. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/authentication_repository/analysis_options.yaml b/packages/authentication_repository/analysis_options.yaml new file mode 100644 index 0000000..799268d --- /dev/null +++ b/packages/authentication_repository/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml diff --git a/packages/authentication_repository/coverage_badge.svg b/packages/authentication_repository/coverage_badge.svg new file mode 100644 index 0000000..499e98c --- /dev/null +++ b/packages/authentication_repository/coverage_badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + diff --git a/packages/authentication_repository/lib/authentication_repository.dart b/packages/authentication_repository/lib/authentication_repository.dart new file mode 100644 index 0000000..08ba8bf --- /dev/null +++ b/packages/authentication_repository/lib/authentication_repository.dart @@ -0,0 +1,5 @@ +/// Repository to manage authentication.s +library authentication_repository; + +export 'src/authentication_repository.dart'; +export 'src/models/models.dart'; diff --git a/packages/authentication_repository/lib/src/authentication_repository.dart b/packages/authentication_repository/lib/src/authentication_repository.dart new file mode 100644 index 0000000..ef32716 --- /dev/null +++ b/packages/authentication_repository/lib/src/authentication_repository.dart @@ -0,0 +1,87 @@ +import 'dart:async'; + +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:firebase_auth/firebase_auth.dart' as fb; + +/// {@template authentication_exception} +/// Exception thrown when an authentication process fails. +/// {@endtemplate} +class AuthenticationException implements Exception { + /// {@macro authentication_exception} + const AuthenticationException(this.error, this.stackTrace); + + /// The error that was caught. + final Object error; + + /// The stack trace associated with the error. + final StackTrace stackTrace; +} + +/// {@template authentication_repository} +/// Repository to manage authentication. +/// {@endtemplate} +class AuthenticationRepository { + /// {@macro authentication_repository} + AuthenticationRepository({ + fb.FirebaseAuth? firebaseAuth, + }) : _firebaseAuth = firebaseAuth ?? fb.FirebaseAuth.instance, + _userController = StreamController.broadcast(); + + final fb.FirebaseAuth _firebaseAuth; + final StreamController _userController; + StreamSubscription? _firebaseUserSubscription; + + /// Stream of [User] which will emit the current user when + /// the authentication state changes. + /// + /// Emits [User.unauthenticated] if the user is not authenticated. + Stream get user { + _firebaseUserSubscription ??= + _firebaseAuth.authStateChanges().listen((firebaseUser) { + _userController.add( + firebaseUser?.toUser ?? User.unauthenticated, + ); + }); + + return _userController.stream; + } + + /// Stream of id tokens that can be used to authenticate with Firebase. + Stream get idToken { + return _firebaseAuth + .idTokenChanges() + .asyncMap((user) => user?.getIdToken()); + } + + /// Refreshes the id token. + Future refreshIdToken() async { + final user = _firebaseAuth.currentUser; + return user?.getIdToken(true); + } + + /// Sign in the user anonymously. + /// + /// If the sign in fails, an [AuthenticationException] is thrown. + Future signInAnonymously() async { + try { + final userCredential = await _firebaseAuth.signInAnonymously(); + _userController.add(userCredential.toUser); + } on Exception catch (error, stackTrace) { + throw AuthenticationException(error, stackTrace); + } + } + + /// Disposes any internal resources. + void dispose() { + _firebaseUserSubscription?.cancel(); + _userController.close(); + } +} + +extension on fb.User { + User get toUser => User(id: uid); +} + +extension on fb.UserCredential { + User get toUser => user?.toUser ?? User.unauthenticated; +} diff --git a/packages/authentication_repository/lib/src/models/models.dart b/packages/authentication_repository/lib/src/models/models.dart new file mode 100644 index 0000000..00db202 --- /dev/null +++ b/packages/authentication_repository/lib/src/models/models.dart @@ -0,0 +1 @@ +export 'user.dart'; diff --git a/packages/authentication_repository/lib/src/models/user.dart b/packages/authentication_repository/lib/src/models/user.dart new file mode 100644 index 0000000..0c192e1 --- /dev/null +++ b/packages/authentication_repository/lib/src/models/user.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; + +/// {@template user} +/// User model +/// {@endtemplate} +class User extends Equatable { + /// {@macro user} + const User({ + required this.id, + }); + + /// The current user's id. + final String id; + + /// Represents an unauthenticated user. + static const unauthenticated = User(id: ''); + + @override + List get props => [id]; +} diff --git a/packages/authentication_repository/pubspec.yaml b/packages/authentication_repository/pubspec.yaml new file mode 100644 index 0000000..eb8ba86 --- /dev/null +++ b/packages/authentication_repository/pubspec.yaml @@ -0,0 +1,20 @@ +name: authentication_repository +description: Repository to manage authentication. +version: 0.1.0+1 +publish_to: none + +environment: + sdk: ">=3.1.0 <4.0.0" + flutter: 3.16.0 + +dependencies: + equatable: ^2.0.5 + firebase_auth: ^4.14.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^1.0.0 + very_good_analysis: ^5.1.0 diff --git a/packages/authentication_repository/test/src/authentication_repository_test.dart b/packages/authentication_repository/test/src/authentication_repository_test.dart new file mode 100644 index 0000000..1a1045b --- /dev/null +++ b/packages/authentication_repository/test/src/authentication_repository_test.dart @@ -0,0 +1,167 @@ +import 'dart:async'; + +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:firebase_auth/firebase_auth.dart' as fb; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class _MockFirebaseAuth extends Mock implements fb.FirebaseAuth {} + +class _MockFirebaseCore extends Mock + with MockPlatformInterfaceMixin + implements FirebasePlatform {} + +class _MockFirebaseUser extends Mock implements fb.User {} + +class _MockUserCredential extends Mock implements fb.UserCredential {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('AuthenticationRepository', () { + const userId = 'mock-userId'; + + late fb.FirebaseAuth firebaseAuth; + late fb.UserCredential userCredential; + late AuthenticationRepository authenticationRepository; + + setUp(() { + const options = FirebaseOptions( + apiKey: 'apiKey', + appId: 'appId', + messagingSenderId: 'messagingSenderId', + projectId: 'projectId', + ); + final platformApp = FirebaseAppPlatform(defaultFirebaseAppName, options); + final firebaseCore = _MockFirebaseCore(); + + when(() => firebaseCore.apps).thenReturn([platformApp]); + when(firebaseCore.app).thenReturn(platformApp); + when( + () => firebaseCore.initializeApp( + name: defaultFirebaseAppName, + options: options, + ), + ).thenAnswer((_) async => platformApp); + + Firebase.delegatePackingProperty = firebaseCore; + + firebaseAuth = _MockFirebaseAuth(); + userCredential = _MockUserCredential(); + authenticationRepository = AuthenticationRepository( + firebaseAuth: firebaseAuth, + ); + }); + + test('can be instantiated', () { + expect(AuthenticationRepository(), isNotNull); + }); + + group('user', () { + test('emits User.unauthenticated when firebase user is null', () async { + when(() => firebaseAuth.authStateChanges()).thenAnswer( + (_) => Stream.value(null), + ); + await expectLater( + authenticationRepository.user, + emitsInOrder(const [User.unauthenticated]), + ); + }); + + test('emits returning user when firebase user is not null', () async { + final firebaseUser = _MockFirebaseUser(); + when(() => firebaseUser.uid).thenReturn(userId); + when(() => firebaseAuth.authStateChanges()).thenAnswer( + (_) => Stream.value(firebaseUser), + ); + + await expectLater( + authenticationRepository.user, + emitsInOrder(const [User(id: userId)]), + ); + }); + }); + + group('idToken', () { + test('returns stream of idTokens from FirebaseAuth', () async { + final user1 = _MockFirebaseUser(); + final user2 = _MockFirebaseUser(); + when(user1.getIdToken).thenAnswer((_) async => 'token1'); + when(user2.getIdToken).thenAnswer((_) async => 'token2'); + when(() => firebaseAuth.idTokenChanges()).thenAnswer( + (_) => Stream.fromIterable([user1, null, user2]), + ); + await expectLater( + authenticationRepository.idToken, + emitsInOrder(['token1', null, 'token2']), + ); + + verify(user1.getIdToken).called(1); + verify(user2.getIdToken).called(1); + }); + }); + + group('refreshIdToken', () { + test('calls getIdToken on firebase user', () async { + final user = _MockFirebaseUser(); + when(() => firebaseAuth.currentUser).thenReturn(user); + when(() => user.getIdToken(true)).thenAnswer((_) async => 'token'); + + final result = await authenticationRepository.refreshIdToken(); + + expect(result, 'token'); + verify(() => user.getIdToken(true)).called(1); + }); + }); + + group('signInAnonymously', () { + test('calls signInAnonymously on FirebaseAuth', () async { + when(() => firebaseAuth.signInAnonymously()).thenAnswer( + (_) async => userCredential, + ); + + await authenticationRepository.signInAnonymously(); + verify(() => firebaseAuth.signInAnonymously()).called(1); + }); + + test('throws AuthenticationException on failure', () async { + when(() => firebaseAuth.signInAnonymously()).thenThrow( + Exception('oops!'), + ); + + expect( + () => authenticationRepository.signInAnonymously(), + throwsA(isA()), + ); + }); + }); + + group('dispose', () { + test('cancels internal subscriptions', () async { + final controller = StreamController(); + final emittedUsers = []; + final firebaseUser = _MockFirebaseUser(); + + when(() => firebaseUser.uid).thenReturn(userId); + when(() => firebaseAuth.authStateChanges()).thenAnswer( + (_) => controller.stream, + ); + + final subscription = authenticationRepository.user.listen( + emittedUsers.add, + ); + authenticationRepository.dispose(); + + controller.add(firebaseUser); + await Future.delayed(Duration.zero); + + expect(emittedUsers, isEmpty); + + await subscription.cancel(); + }); + }); + }); +} diff --git a/packages/authentication_repository/test/src/models/user_test.dart b/packages/authentication_repository/test/src/models/user_test.dart new file mode 100644 index 0000000..8b4a18b --- /dev/null +++ b/packages/authentication_repository/test/src/models/user_test.dart @@ -0,0 +1,15 @@ +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('User', () { + test('supports value equality', () { + const userA = User(id: 'A'); + const secondUserA = User(id: 'A'); + const userB = User(id: 'B'); + + expect(userA, equals(secondUserA)); + expect(userA, isNot(equals(userB))); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index e929721..45e52b0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,23 +5,31 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "8942974ee5dc932427704ede5e0db8b5f2b8f22172964544b56e6bd37f6c3634" + url: "https://pub.dev" + source: hosted + version: "1.3.12" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" api_client: dependency: "direct main" description: - path: "/Users/ruialonso/dev/flutter/googleAI/dash_ai_search/packages/api_client" - relative: false + path: "packages/api_client" + relative: true source: path version: "0.1.0+1" args: @@ -40,6 +48,13 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + authentication_repository: + dependency: "direct main" + description: + path: "packages/authentication_repository" + relative: true + source: path + version: "0.1.0+1" bloc: dependency: "direct main" description: @@ -84,10 +99,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -100,10 +115,10 @@ packages: dependency: transitive description: name: coverage - sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 url: "https://pub.dev" source: hosted - version: "1.6.4" + version: "1.7.1" crypto: dependency: transitive description: @@ -120,6 +135,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -136,6 +159,54 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: a9281f9b149ad4cffbfe91725ee5159c63aca2f27c05e2e87052fc240abe212c + url: "https://pub.dev" + source: hosted + version: "4.14.0" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: def138b34bb97310ff748d1864f3ccd5b49d7424f35825f0df5455f16218ba99 + url: "https://pub.dev" + source: hosted + version: "7.0.4" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: e5fbb3453fbfff0dd8dfed5e3560248391f76a4c59ae9b331704e3443cffde77 + url: "https://pub.dev" + source: hosted + version: "5.8.7" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "45f3f9babfc6f56fb94c3cd11584cf3c9672868228373b699b94427010e01dc3" + url: "https://pub.dev" + source: hosted + version: "2.22.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + url: "https://pub.dev" + source: hosted + version: "5.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0631a2ec971dbc540275e2fa00c3a8a2676f0a7adbc3c197d6fba569db689d97" + url: "https://pub.dev" + source: hosted + version: "2.8.1" flutter: dependency: "direct main" description: flutter @@ -159,6 +230,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -251,10 +327,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -303,6 +379,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + url: "https://pub.dev" + source: hosted + version: "2.1.7" pool: dependency: transitive description: @@ -312,7 +396,7 @@ packages: source: hosted version: "1.5.1" provider: - dependency: transitive + dependency: "direct main" description: name: provider sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" @@ -392,18 +476,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -424,26 +508,26 @@ packages: dependency: transitive description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.9" typed_data: dependency: transitive description: @@ -472,10 +556,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -488,10 +572,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -517,5 +601,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0 <4.0.0" - flutter: ">=1.16.0" + dart: ">=3.2.0-194.0.dev <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index b4858a0..18373b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,17 +5,23 @@ publish_to: none environment: sdk: ">=3.1.0 <4.0.0" + flutter: 3.16.0 dependencies: api_client: path: packages/api_client + authentication_repository: + path: packages/authentication_repository bloc: ^8.1.2 + firebase_auth: ^4.14.0 + firebase_core: ^2.22.0 flutter: sdk: flutter flutter_bloc: ^8.1.3 flutter_localizations: sdk: flutter intl: ^0.18.0 + provider: ^6.1.1 dev_dependencies: bloc_test: ^9.1.4 diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 8052a14..b6f672c 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -1,4 +1,5 @@ import 'package:api_client/api_client.dart'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:dash_ai_search/app/app.dart'; import 'package:dash_ai_search/counter/counter.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -6,6 +7,8 @@ import 'package:mocktail/mocktail.dart'; class _MockApiClient extends Mock implements ApiClient {} +class _MockUser extends Mock implements User {} + void main() { group('App', () { late ApiClient apiClient; @@ -18,6 +21,7 @@ void main() { await tester.pumpWidget( App( apiClient: apiClient, + user: _MockUser(), ), ); expect(find.byType(CounterPage), findsOneWidget);