diff --git a/.github/workflows/authentication_repository.yaml b/.github/workflows/authentication_repository.yaml new file mode 100644 index 00000000..74c81d10 --- /dev/null +++ b/.github/workflows/authentication_repository.yaml @@ -0,0 +1,22 @@ +name: authentication_repository + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + paths: + - "packages/authentication_repository/**" + - ".github/workflows/authentication_repository.yaml" + + pull_request: + paths: + - "packages/authentication_repository/**" + - ".github/workflows/authentication_repository.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/authentication_repository diff --git a/packages/authentication_repository/.gitignore b/packages/authentication_repository/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/authentication_repository/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/authentication_repository/README.md b/packages/authentication_repository/README.md new file mode 100644 index 00000000..8f56b868 --- /dev/null +++ b/packages/authentication_repository/README.md @@ -0,0 +1,11 @@ +# authentication_repository + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Repository to manage user 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 00000000..3742fc3d --- /dev/null +++ b/packages/authentication_repository/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/authentication_repository/lib/authentication_repository.dart b/packages/authentication_repository/lib/authentication_repository.dart new file mode 100644 index 00000000..77b1b6b9 --- /dev/null +++ b/packages/authentication_repository/lib/authentication_repository.dart @@ -0,0 +1,3 @@ +library authentication_repository; + +export 'src/authentication_repository.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 00000000..9f252518 --- /dev/null +++ b/packages/authentication_repository/lib/src/authentication_repository.dart @@ -0,0 +1,36 @@ +import 'package:firebase_auth/firebase_auth.dart'; + +/// {@template authentication_exception} +/// Exception for authentication repository failures. +/// {@endtemplate} +class AuthenticationException implements Exception { + /// {@macro authentication_exception} + const AuthenticationException(this.error, this.stackTrace); + + /// The error that was caught. + final Object error; + + /// The Stacktrace associated with the [error]. + final StackTrace stackTrace; +} + +/// {@template authentication_repository} +/// Repository to manage user authentication. +/// {@endtemplate} +class AuthenticationRepository { + /// {@macro authentication_repository} + AuthenticationRepository(this._firebaseAuth); + + final FirebaseAuth _firebaseAuth; + + /// Sign in the existing user anonymously using [FirebaseAuth]. If the + /// authentication process can't be completed, it will throw an + /// [AuthenticationException]. + Future authenticateAnonymously() async { + try { + await _firebaseAuth.signInAnonymously(); + } on Exception catch (error, stackTrace) { + throw AuthenticationException(error, stackTrace); + } + } +} diff --git a/packages/authentication_repository/pubspec.yaml b/packages/authentication_repository/pubspec.yaml new file mode 100644 index 00000000..bac20507 --- /dev/null +++ b/packages/authentication_repository/pubspec.yaml @@ -0,0 +1,18 @@ +name: authentication_repository +description: Repository to manage user authentication. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + firebase_auth: ^3.3.16 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^0.2.0 + very_good_analysis: ^2.4.0 \ No newline at end of file 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 00000000..a179bb68 --- /dev/null +++ b/packages/authentication_repository/test/src/authentication_repository_test.dart @@ -0,0 +1,40 @@ +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +class MockUserCredential extends Mock implements UserCredential {} + +void main() { + late FirebaseAuth firebaseAuth; + late UserCredential userCredential; + late AuthenticationRepository authenticationRepository; + + group('AuthenticationRepository', () { + setUp(() { + firebaseAuth = MockFirebaseAuth(); + userCredential = MockUserCredential(); + authenticationRepository = AuthenticationRepository(firebaseAuth); + }); + + group('authenticateAnonymously', () { + test('completes if no exception is thrown', () async { + when(() => firebaseAuth.signInAnonymously()) + .thenAnswer((_) async => userCredential); + await authenticationRepository.authenticateAnonymously(); + verify(() => firebaseAuth.signInAnonymously()).called(1); + }); + + test('throws AuthenticationException when firebase auth fails', () async { + when(() => firebaseAuth.signInAnonymously()) + .thenThrow(Exception('oops')); + expect( + () => authenticationRepository.authenticateAnonymously(), + throwsA(isA()), + ); + }); + }); + }); +} diff --git a/packages/pinball_flame/lib/src/contact_behavior.dart b/packages/pinball_flame/lib/src/contact_behavior.dart index 79112398..ff715b12 100644 --- a/packages/pinball_flame/lib/src/contact_behavior.dart +++ b/packages/pinball_flame/lib/src/contact_behavior.dart @@ -1,95 +1,105 @@ import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:pinball_flame/pinball_flame.dart'; /// Appends a new [ContactCallbacks] to the parent. /// /// This is a convenience class for adding a [ContactCallbacks] to the parent. -/// In constract with just adding a [ContactCallbacks] to the parent's body -/// userData, this class respects the previous [ContactCallbacks] in the -/// parent's body userData, if any. Hence, it avoids overriding any previous -/// [ContactCallbacks] in the parent. +/// In contrast with just assigning a [ContactCallbacks] to a userData, this +/// class respects the previous userData. /// -/// It does so by grouping the [ContactCallbacks] in a [_ContactCallbacksGroup], -/// and resetting the parent's userData accordingly. +/// It does so by grouping the userData in a [_UserData], and resetting the +/// parent's userData accordingly. // TODO(alestiago): Make use of generics to infer the type of the contact. // https://github.com/VGVentures/pinball/pull/234#discussion_r859182267 -// TODO(alestiago): Consider if there is a need to support adjusting a fixture's -// userData. class ContactBehavior extends Component with ContactCallbacks, ParentIsA { + final _fixturesUserData = {}; + + /// Specifies which fixtures should be considered for contact. + /// + /// Fixtures are identifiable by their userData. + /// + /// If no fixtures are specified, the [ContactCallbacks] is applied to the + /// entire body, hence all fixtures are considered. + void applyTo(Iterable userData) => _fixturesUserData.addAll(userData); + @override - @mustCallSuper Future onLoad() async { - final userData = parent.body.userData; - if (userData is _ContactCallbacksGroup) { - userData.addContactCallbacks(this); - } else if (userData is ContactCallbacks) { - final contactCallbacksGroup = _ContactCallbacksGroup() - ..addContactCallbacks(userData) - ..addContactCallbacks(this); - parent.body.userData = contactCallbacksGroup; + if (_fixturesUserData.isNotEmpty) { + for (final fixture in _targetedFixtures) { + fixture.userData = _UserData.fromFixture(fixture)..add(this); + } } else { - parent.body.userData = this; + parent.body.userData = _UserData.fromBody(parent.body)..add(this); } } + + Iterable get _targetedFixtures => + parent.body.fixtures.where((fixture) { + if (_fixturesUserData.contains(fixture.userData)) return true; + + final userData = fixture.userData; + if (userData is _UserData) { + return _fixturesUserData.contains(userData.value); + } + + return false; + }); } -class _ContactCallbacksGroup implements ContactCallbacks { - final List _contactCallbacks = []; +class _UserData with ContactCallbacks { + _UserData._(Object? userData) : _userData = [userData]; + + factory _UserData._fromUserData(Object? userData) { + if (userData is _UserData) return userData; + return _UserData._(userData); + } + + factory _UserData.fromFixture(Fixture fixture) => + _UserData._fromUserData(fixture.userData); + + factory _UserData.fromBody(Body body) => + _UserData._fromUserData(body.userData); + + final List _userData; + + Iterable get _contactCallbacks => + _userData.whereType(); + + Object? get value => _userData.first; + + void add(Object? userData) => _userData.add(userData); @override - @mustCallSuper void beginContact(Object other, Contact contact) { - onBeginContact?.call(other, contact); + super.beginContact(other, contact); for (final callback in _contactCallbacks) { callback.beginContact(other, contact); } } @override - @mustCallSuper void endContact(Object other, Contact contact) { - onEndContact?.call(other, contact); + super.endContact(other, contact); for (final callback in _contactCallbacks) { callback.endContact(other, contact); } } @override - @mustCallSuper void preSolve(Object other, Contact contact, Manifold oldManifold) { - onPreSolve?.call(other, contact, oldManifold); + super.preSolve(other, contact, oldManifold); for (final callback in _contactCallbacks) { callback.preSolve(other, contact, oldManifold); } } @override - @mustCallSuper void postSolve(Object other, Contact contact, ContactImpulse impulse) { - onPostSolve?.call(other, contact, impulse); + super.postSolve(other, contact, impulse); for (final callback in _contactCallbacks) { callback.postSolve(other, contact, impulse); } } - - void addContactCallbacks(ContactCallbacks callback) { - _contactCallbacks.add(callback); - } - - @override - void Function(Object other, Contact contact)? onBeginContact; - - @override - void Function(Object other, Contact contact)? onEndContact; - - @override - void Function(Object other, Contact contact, ContactImpulse impulse)? - onPostSolve; - - @override - void Function(Object other, Contact contact, Manifold oldManifold)? - onPreSolve; } diff --git a/packages/pinball_flame/test/src/contact_behavior_test.dart b/packages/pinball_flame/test/src/contact_behavior_test.dart index 630156ed..cf7fe35a 100644 --- a/packages/pinball_flame/test/src/contact_behavior_test.dart +++ b/packages/pinball_flame/test/src/contact_behavior_test.dart @@ -58,28 +58,78 @@ void main() { late Contact contact; late Manifold manifold; late ContactImpulse contactImpulse; + late FixtureDef fixtureDef; setUp(() { other = Object(); contact = _MockContact(); manifold = _MockManifold(); contactImpulse = _MockContactImpulse(); + fixtureDef = FixtureDef(CircleShape()); }); flameTester.test( - 'should add a new ContactCallbacks to the parent', + "should add a new ContactCallbacks to the parent's body userData " + 'when not applied to fixtures', (game) async { final parent = _TestBodyComponent(); final contactBehavior = ContactBehavior(); await parent.add(contactBehavior); await game.ensureAdd(parent); - expect(parent.body.userData, contactBehavior); + expect(parent.body.userData, isA()); }, ); flameTester.test( - "should respect the previous ContactCallbacks in the parent's userData", + 'should add a new ContactCallbacks to the targeted fixture ', + (game) async { + final parent = _TestBodyComponent(); + + await game.ensureAdd(parent); + final fixture1 = + parent.body.createFixture(fixtureDef..userData = 'foo'); + final fixture2 = parent.body.createFixture(fixtureDef..userData = null); + final contactBehavior = ContactBehavior() + ..applyTo( + [fixture1.userData!], + ); + + await parent.ensureAdd(contactBehavior); + + expect(parent.body.userData, isNull); + expect(fixture1.userData, isA()); + expect(fixture2.userData, isNull); + }, + ); + + flameTester.test( + 'should add a new ContactCallbacks to the targeted fixtures ', + (game) async { + final parent = _TestBodyComponent(); + + await game.ensureAdd(parent); + final fixture1 = + parent.body.createFixture(fixtureDef..userData = 'foo'); + final fixture2 = + parent.body.createFixture(fixtureDef..userData = 'boo'); + final contactBehavior = ContactBehavior() + ..applyTo([ + fixture1.userData!, + fixture2.userData!, + ]); + + await parent.ensureAdd(contactBehavior); + + expect(parent.body.userData, isNull); + expect(fixture1.userData, isA()); + expect(fixture2.userData, isA()); + }, + ); + + flameTester.test( + "should respect the previous ContactCallbacks in the parent's userData " + 'when not applied to fixtures', (game) async { final parent = _TestBodyComponent(); await game.ensureAdd(parent); @@ -113,41 +163,94 @@ void main() { }, ); - flameTester.test('can group multiple ContactBehaviors and keep listening', - (game) async { - final parent = _TestBodyComponent(); - await game.ensureAdd(parent); - - final contactBehavior1 = _TestContactBehavior(); - final contactBehavior2 = _TestContactBehavior(); - final contactBehavior3 = _TestContactBehavior(); - await parent.ensureAddAll([ - contactBehavior1, - contactBehavior2, - contactBehavior3, - ]); - - final contactCallbacks = parent.body.userData! as ContactCallbacks; - - contactCallbacks.beginContact(other, contact); - expect(contactBehavior1.beginContactCallsCount, equals(1)); - expect(contactBehavior2.beginContactCallsCount, equals(1)); - expect(contactBehavior3.beginContactCallsCount, equals(1)); - - contactCallbacks.endContact(other, contact); - expect(contactBehavior1.endContactCallsCount, equals(1)); - expect(contactBehavior2.endContactCallsCount, equals(1)); - expect(contactBehavior3.endContactCallsCount, equals(1)); - - contactCallbacks.preSolve(other, contact, manifold); - expect(contactBehavior1.preSolveContactCallsCount, equals(1)); - expect(contactBehavior2.preSolveContactCallsCount, equals(1)); - expect(contactBehavior3.preSolveContactCallsCount, equals(1)); - - contactCallbacks.postSolve(other, contact, contactImpulse); - expect(contactBehavior1.postSolveContactCallsCount, equals(1)); - expect(contactBehavior2.postSolveContactCallsCount, equals(1)); - expect(contactBehavior3.postSolveContactCallsCount, equals(1)); - }); + flameTester.test( + 'can group multiple ContactBehaviors and keep listening', + (game) async { + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + + final contactBehavior1 = _TestContactBehavior(); + final contactBehavior2 = _TestContactBehavior(); + final contactBehavior3 = _TestContactBehavior(); + await parent.ensureAddAll([ + contactBehavior1, + contactBehavior2, + contactBehavior3, + ]); + + final contactCallbacks = parent.body.userData! as ContactCallbacks; + + contactCallbacks.beginContact(other, contact); + expect(contactBehavior1.beginContactCallsCount, equals(1)); + expect(contactBehavior2.beginContactCallsCount, equals(1)); + expect(contactBehavior3.beginContactCallsCount, equals(1)); + + contactCallbacks.endContact(other, contact); + expect(contactBehavior1.endContactCallsCount, equals(1)); + expect(contactBehavior2.endContactCallsCount, equals(1)); + expect(contactBehavior3.endContactCallsCount, equals(1)); + + contactCallbacks.preSolve(other, contact, manifold); + expect(contactBehavior1.preSolveContactCallsCount, equals(1)); + expect(contactBehavior2.preSolveContactCallsCount, equals(1)); + expect(contactBehavior3.preSolveContactCallsCount, equals(1)); + + contactCallbacks.postSolve(other, contact, contactImpulse); + expect(contactBehavior1.postSolveContactCallsCount, equals(1)); + expect(contactBehavior2.postSolveContactCallsCount, equals(1)); + expect(contactBehavior3.postSolveContactCallsCount, equals(1)); + }, + ); + + flameTester.test( + 'can group multiple ContactBehaviors and keep listening ' + 'when applied to a fixture', + (game) async { + final parent = _TestBodyComponent(); + await game.ensureAdd(parent); + + final fixture = parent.body.createFixture(fixtureDef..userData = 'foo'); + + final contactBehavior1 = _TestContactBehavior() + ..applyTo( + [fixture.userData!], + ); + final contactBehavior2 = _TestContactBehavior() + ..applyTo( + [fixture.userData!], + ); + final contactBehavior3 = _TestContactBehavior() + ..applyTo( + [fixture.userData!], + ); + await parent.ensureAddAll([ + contactBehavior1, + contactBehavior2, + contactBehavior3, + ]); + + final contactCallbacks = fixture.userData! as ContactCallbacks; + + contactCallbacks.beginContact(other, contact); + expect(contactBehavior1.beginContactCallsCount, equals(1)); + expect(contactBehavior2.beginContactCallsCount, equals(1)); + expect(contactBehavior3.beginContactCallsCount, equals(1)); + + contactCallbacks.endContact(other, contact); + expect(contactBehavior1.endContactCallsCount, equals(1)); + expect(contactBehavior2.endContactCallsCount, equals(1)); + expect(contactBehavior3.endContactCallsCount, equals(1)); + + contactCallbacks.preSolve(other, contact, manifold); + expect(contactBehavior1.preSolveContactCallsCount, equals(1)); + expect(contactBehavior2.preSolveContactCallsCount, equals(1)); + expect(contactBehavior3.preSolveContactCallsCount, equals(1)); + + contactCallbacks.postSolve(other, contact, contactImpulse); + expect(contactBehavior1.postSolveContactCallsCount, equals(1)); + expect(contactBehavior2.postSolveContactCallsCount, equals(1)); + expect(contactBehavior3.postSolveContactCallsCount, equals(1)); + }, + ); }); } diff --git a/pubspec.lock b/pubspec.lock index ab39378a..3b6d5d63 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -36,6 +36,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.20.1" + authentication_repository: + dependency: "direct main" + description: + path: "packages/authentication_repository" + relative: true + source: path + version: "1.0.0+1" bloc: dependency: "direct main" description: @@ -169,6 +176,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.16" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "6.2.4" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.13" firebase_core: dependency: transitive description: @@ -189,7 +217,7 @@ packages: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "1.6.1" + version: "1.6.2" flame: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 51c85cd5..c6866b2e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,9 +7,12 @@ environment: sdk: ">=2.16.0 <3.0.0" dependencies: + authentication_repository: + path: packages/authentication_repository bloc: ^8.0.2 cloud_firestore: ^3.1.10 equatable: ^2.0.3 + firebase_auth: ^3.3.16 flame: ^1.1.1 flame_bloc: ^1.2.0 flame_forge2d: