diff --git a/.github/workflows/leaderboard_repository.yaml b/.github/workflows/leaderboard_repository.yaml new file mode 100644 index 00000000..6eddf283 --- /dev/null +++ b/.github/workflows/leaderboard_repository.yaml @@ -0,0 +1,18 @@ +name: leaderboard_repository + +on: + push: + paths: + - "packages/leaderboard_repository/**" + - ".github/workflows/leaderboard_repository.yaml" + + pull_request: + paths: + - "packages/leaderboard_repository/**" + - ".github/workflows/leaderboard_repository.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 + with: + working_directory: packages/leaderboard_repository diff --git a/packages/leaderboard_repository/.gitignore b/packages/leaderboard_repository/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/leaderboard_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/leaderboard_repository/README.md b/packages/leaderboard_repository/README.md new file mode 100644 index 00000000..c3123fe0 --- /dev/null +++ b/packages/leaderboard_repository/README.md @@ -0,0 +1,11 @@ +# leaderboard_repository + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Repository to access leaderboard data in Firebase Cloud Firestore. + +[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/leaderboard_repository/analysis_options.yaml b/packages/leaderboard_repository/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/leaderboard_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/leaderboard_repository/lib/leaderboard_repository.dart b/packages/leaderboard_repository/lib/leaderboard_repository.dart new file mode 100644 index 00000000..d299569a --- /dev/null +++ b/packages/leaderboard_repository/lib/leaderboard_repository.dart @@ -0,0 +1,4 @@ +library leaderboard_repository; + +export 'src/leaderboard_repository.dart'; +export 'src/models/models.dart'; diff --git a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart new file mode 100644 index 00000000..5a578358 --- /dev/null +++ b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart @@ -0,0 +1,152 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; + +/// {@template leaderboard_exception} +/// Base exception for leaderboard repository failures. +/// {@endtemplate} +abstract class LeaderboardException implements Exception { + /// {@macro leaderboard_exception} + const LeaderboardException(this.error, this.stackTrace); + + /// The error that was caught. + final Object error; + + /// The Stacktrace associated with the [error]. + final StackTrace stackTrace; +} + +/// {@template leaderboard_deserialization_exception} +/// Exception thrown when leaderboard data cannot be deserialized in the +/// expected way. +/// {@endtemplate} +class LeaderboardDeserializationException extends LeaderboardException { + /// {@macro leaderboard_deserialization_exception} + const LeaderboardDeserializationException( + Object error, + StackTrace stackTrace, + ) : super( + error, + stackTrace, + ); +} + +/// {@template fetch_top_10_leaderboard_exception} +/// Exception thrown when failure occurs while fetching top 10 leaderboard. +/// {@endtemplate} +class FetchTop10LeaderboardException extends LeaderboardException { + /// {@macro fetch_top_10_leaderboard_exception} + const FetchTop10LeaderboardException( + Object error, + StackTrace stackTrace, + ) : super( + error, + stackTrace, + ); +} + +/// {@template add_leaderboard_entry_exception} +/// Exception thrown when failure occurs while adding entry to leaderboard. +/// {@endtemplate} +class AddLeaderboardEntryException extends LeaderboardException { + /// {@macro add_leaderboard_entry_exception} + const AddLeaderboardEntryException( + Object error, + StackTrace stackTrace, + ) : super( + error, + stackTrace, + ); +} + +/// {@template fetch_player_ranking_exception} +/// Exception thrown when failure occurs while fetching player ranking. +/// {@endtemplate} +class FetchPlayerRankingException extends LeaderboardException { + /// {@macro fetch_player_ranking_exception} + const FetchPlayerRankingException( + Object error, + StackTrace stackTrace, + ) : super( + error, + stackTrace, + ); +} + +/// {@template leaderboard_repository} +/// Repository to access leaderboard data in Firebase Cloud Firestore. +/// {@endtemplate} +class LeaderboardRepository { + /// {@macro leaderboard_repository} + const LeaderboardRepository( + FirebaseFirestore firebaseFirestore, + ) : _firebaseFirestore = firebaseFirestore; + + final FirebaseFirestore _firebaseFirestore; + + /// Acquires top 10 [LeaderboardEntry]s. + Future> fetchTop10Leaderboard() async { + final leaderboardEntries = []; + late List documents; + + try { + final querySnapshot = await _firebaseFirestore + .collection('leaderboard') + .orderBy('score') + .limit(10) + .get(); + documents = querySnapshot.docs; + } on Exception catch (error, stackTrace) { + throw FetchTop10LeaderboardException(error, stackTrace); + } + + for (final document in documents) { + final data = document.data() as Map?; + if (data != null) { + try { + leaderboardEntries.add(LeaderboardEntry.fromJson(data)); + } catch (error, stackTrace) { + throw LeaderboardDeserializationException(error, stackTrace); + } + } + } + + return leaderboardEntries; + } + + /// Adds player's score entry to the leaderboard and gets their + /// [LeaderboardRanking]. + Future addLeaderboardEntry(LeaderboardEntry entry) async { + late DocumentReference entryReference; + try { + entryReference = await _firebaseFirestore + .collection('leaderboard') + .add(entry.toJson()); + } on Exception catch (error, stackTrace) { + throw AddLeaderboardEntryException(error, stackTrace); + } + + try { + final querySnapshot = await _firebaseFirestore + .collection('leaderboard') + .orderBy('score') + .get(); + + final documents = querySnapshot.docs; + final ranking = documents.indexWhere( + (document) => document.id == entryReference.id, + ) + + 1; + + if (ranking > 0) { + return LeaderboardRanking(ranking: ranking, outOf: documents.length); + } else { + throw FetchPlayerRankingException( + 'Player score could not be found and ranking cannot be provided.', + StackTrace.current, + ); + } + } on Exception catch (error, stackTrace) { + throw FetchPlayerRankingException(error, stackTrace); + } + } +} diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_entry.dart new file mode 100644 index 00000000..16c17fb0 --- /dev/null +++ b/packages/leaderboard_repository/lib/src/models/leaderboard_entry.dart @@ -0,0 +1,83 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'leaderboard_entry.g.dart'; + +/// Google character type associated with a [LeaderboardEntry]. +enum CharacterType { + /// Dash character. + dash, + + /// Sparky character. + + sparky, + + /// Android character. + + android, + + /// Dino character. + + dino, +} + +/// {@template leaderboard_entry} +/// A model representing a leaderboard entry containing the player's initials, +/// score, and chosen character. +/// +/// Stored in Firestore `leaderboard` collection. +/// +/// Example: +/// ```json +/// { +/// "playerInitials" : "ABC", +/// "score" : 1500, +/// "character" : "dash" +/// } +/// ``` +/// {@endtemplate} +@JsonSerializable() +class LeaderboardEntry extends Equatable { + /// {@macro leaderboard_entry} + const LeaderboardEntry({ + required this.playerInitials, + required this.score, + required this.character, + }); + + /// Factory which converts a [Map] into a [LeaderboardEntry]. + factory LeaderboardEntry.fromJson(Map json) { + return _$LeaderboardEntryFromJson(json); + } + + /// Converts the [LeaderboardEntry] to [Map]. + Map toJson() => _$LeaderboardEntryToJson(this); + + /// Player's chosen initials for [LeaderboardEntry]. + /// + /// Example: 'ABC'. + @JsonKey(name: 'playerInitials') + final String playerInitials; + + /// Score for [LeaderboardEntry]. + /// + /// Example: 1500. + @JsonKey(name: 'score') + final int score; + + /// [CharacterType] for [LeaderboardEntry]. + /// + /// Example: [CharacterType.dash]. + @JsonKey(name: 'character') + final CharacterType character; + + /// An empty [LeaderboardEntry] object. + static const empty = LeaderboardEntry( + playerInitials: '', + score: 0, + character: CharacterType.dash, + ); + + @override + List get props => [playerInitials, score, character]; +} diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.g.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_entry.g.dart new file mode 100644 index 00000000..fc685220 --- /dev/null +++ b/packages/leaderboard_repository/lib/src/models/leaderboard_entry.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'leaderboard_entry.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LeaderboardEntry _$LeaderboardEntryFromJson(Map json) => + LeaderboardEntry( + playerInitials: json['playerInitials'] as String, + score: json['score'] as int, + character: $enumDecode(_$CharacterTypeEnumMap, json['character']), + ); + +Map _$LeaderboardEntryToJson(LeaderboardEntry instance) => + { + 'playerInitials': instance.playerInitials, + 'score': instance.score, + 'character': _$CharacterTypeEnumMap[instance.character], + }; + +const _$CharacterTypeEnumMap = { + CharacterType.dash: 'dash', + CharacterType.sparky: 'sparky', + CharacterType.android: 'android', + CharacterType.dino: 'dino', +}; diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart new file mode 100644 index 00000000..7ec90ef4 --- /dev/null +++ b/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; + +/// {@template leaderboard_ranking} +/// Contains [ranking] for a single [LeaderboardEntry] and the number of players +/// the [ranking] is [outOf]. +/// {@endtemplate} +class LeaderboardRanking extends Equatable { + /// {@macro leaderboard_ranking} + const LeaderboardRanking({required this.ranking, required this.outOf}); + + /// Place ranking by score for a [LeaderboardEntry]. + final int ranking; + + /// Number of [LeaderboardEntry]s at the time of score entry. + final int outOf; + + @override + List get props => [ranking, outOf]; +} diff --git a/packages/leaderboard_repository/lib/src/models/models.dart b/packages/leaderboard_repository/lib/src/models/models.dart new file mode 100644 index 00000000..3dabe2bf --- /dev/null +++ b/packages/leaderboard_repository/lib/src/models/models.dart @@ -0,0 +1,2 @@ +export 'leaderboard_entry.dart'; +export 'leaderboard_ranking.dart'; diff --git a/packages/leaderboard_repository/pubspec.yaml b/packages/leaderboard_repository/pubspec.yaml new file mode 100644 index 00000000..1d3869f4 --- /dev/null +++ b/packages/leaderboard_repository/pubspec.yaml @@ -0,0 +1,23 @@ +name: leaderboard_repository +description: Repository to access leaderboard data in Firebase Cloud Firestore. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + cloud_firestore: ^3.1.10 + equatable: ^2.0.3 + flutter: + sdk: flutter + json_annotation: ^4.4.0 + +dev_dependencies: + build_runner: ^2.1.8 + flutter_test: + sdk: flutter + json_serializable: ^6.1.5 + mocktail: ^0.2.0 + test: ^1.19.2 + very_good_analysis: ^2.4.0 \ No newline at end of file diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart new file mode 100644 index 00000000..cd632638 --- /dev/null +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -0,0 +1,226 @@ +// ignore_for_file: prefer_const_constructors, subtype_of_sealed_class + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockFirebaseFirestore extends Mock implements FirebaseFirestore {} + +class MockCollectionReference extends Mock + implements CollectionReference> {} + +class MockQuery extends Mock implements Query> {} + +class MockQuerySnapshot extends Mock + implements QuerySnapshot> {} + +class MockQueryDocumentSnapshot extends Mock + implements QueryDocumentSnapshot> {} + +class MockDocumentReference extends Mock + implements DocumentReference> {} + +void main() { + group('LeaderboardRepository', () { + late FirebaseFirestore firestore; + + setUp(() { + firestore = MockFirebaseFirestore(); + }); + + test('can be instantiated', () { + expect(LeaderboardRepository(firestore), isNotNull); + }); + + group('fetchTop10Leaderboard', () { + late LeaderboardRepository leaderboardRepository; + late CollectionReference> collectionReference; + late Query> query; + late QuerySnapshot> querySnapshot; + late List>> + queryDocumentSnapshots; + + final top10Scores = [ + 2500, + 2200, + 2200, + 2000, + 1800, + 1400, + 1300, + 1000, + 600, + 300, + 100, + ]; + + final top10Leaderboard = top10Scores + .map( + (score) => LeaderboardEntry( + playerInitials: 'user$score', + score: score, + character: CharacterType.dash, + ), + ) + .toList(); + + setUp(() { + leaderboardRepository = LeaderboardRepository(firestore); + collectionReference = MockCollectionReference(); + query = MockQuery(); + querySnapshot = MockQuerySnapshot(); + queryDocumentSnapshots = top10Scores.map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'playerInitials': 'user$score', + 'score': score + }); + return queryDocumentSnapshot; + }).toList(); + + when(() => firestore.collection('leaderboard')) + .thenAnswer((_) => collectionReference); + when(() => collectionReference.orderBy('score')) + .thenAnswer((_) => query); + when(() => query.limit(10)).thenAnswer((_) => query); + when(query.get).thenAnswer((_) async => querySnapshot); + when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); + }); + + test( + 'returns top 10 entries when ' + 'retrieving information from firestore succeeds', () async { + final top10LeaderboardResults = + await leaderboardRepository.fetchTop10Leaderboard(); + + expect(top10LeaderboardResults, equals(top10Leaderboard)); + }); + + test( + 'throws FetchTop10LeaderboardException when Exception occurs ' + 'when trying to retrieve information from firestore', () async { + when(() => firestore.collection('leaderboard')).thenThrow(Exception()); + + expect( + () => leaderboardRepository.fetchTop10Leaderboard(), + throwsA(isA()), + ); + }); + + test( + 'throws LeaderboardDeserializationException when Exception occurs ' + 'during deserialization', () async { + final top10LeaderboardDataMalformed = { + 'playerInitials': 'ABC', + 'score': 1500, + }; + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(() => querySnapshot.docs).thenReturn([queryDocumentSnapshot]); + when(queryDocumentSnapshot.data) + .thenReturn(top10LeaderboardDataMalformed); + + expect( + () => leaderboardRepository.fetchTop10Leaderboard(), + throwsA(isA()), + ); + }); + }); + + group('addLeaderboardEntry', () { + late LeaderboardRepository leaderboardRepository; + late CollectionReference> collectionReference; + late DocumentReference> documentReference; + late Query> query; + late QuerySnapshot> querySnapshot; + late List>> + queryDocumentSnapshots; + + const entryScore = 1500; + final leaderboardScores = [ + 2500, + 2200, + entryScore, + 1000, + ]; + final leaderboardEntry = LeaderboardEntry( + playerInitials: 'ABC', + score: entryScore, + character: CharacterType.dash, + ); + const entryDocumentId = 'id$entryScore'; + final ranking = LeaderboardRanking(ranking: 3, outOf: 4); + + setUp(() { + leaderboardRepository = LeaderboardRepository(firestore); + collectionReference = MockCollectionReference(); + documentReference = MockDocumentReference(); + query = MockQuery(); + querySnapshot = MockQuerySnapshot(); + queryDocumentSnapshots = leaderboardScores.map((score) { + final queryDocumentSnapshot = MockQueryDocumentSnapshot(); + when(queryDocumentSnapshot.data).thenReturn({ + 'character': 'dash', + 'username': 'user$score', + 'score': score + }); + when(() => queryDocumentSnapshot.id).thenReturn('id$score'); + return queryDocumentSnapshot; + }).toList(); + + when(() => firestore.collection('leaderboard')) + .thenAnswer((_) => collectionReference); + when(() => collectionReference.add(any())) + .thenAnswer((_) async => documentReference); + when(() => collectionReference.orderBy('score')) + .thenAnswer((_) => query); + when(query.get).thenAnswer((_) async => querySnapshot); + when(() => querySnapshot.docs).thenReturn(queryDocumentSnapshots); + when(() => documentReference.id).thenReturn(entryDocumentId); + }); + + test( + 'adds leaderboard entry and returns player ranking when ' + 'firestore operations succeed', () async { + final rankingResult = + await leaderboardRepository.addLeaderboardEntry(leaderboardEntry); + + expect(rankingResult, equals(ranking)); + }); + + test( + 'throws AddLeaderboardEntryException when Exception occurs ' + 'when trying to add entry to firestore', () async { + when(() => firestore.collection('leaderboard')).thenThrow(Exception()); + + expect( + () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), + throwsA(isA()), + ); + }); + + test( + 'throws FetchPlayerRankingException when Exception occurs ' + 'when trying to retrieve information from firestore', () async { + when(() => collectionReference.orderBy('score')).thenThrow(Exception()); + + expect( + () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), + throwsA(isA()), + ); + }); + + test( + 'throws FetchPlayerRankingException when score cannot be found ' + 'in firestore leaderboard data', () async { + when(() => documentReference.id).thenReturn('nonexistentDocumentId'); + + expect( + () => leaderboardRepository.addLeaderboardEntry(leaderboardEntry), + throwsA(isA()), + ); + }); + }); + }); +} diff --git a/packages/leaderboard_repository/test/src/models/leaderboard_entry_test.dart b/packages/leaderboard_repository/test/src/models/leaderboard_entry_test.dart new file mode 100644 index 00000000..21056529 --- /dev/null +++ b/packages/leaderboard_repository/test/src/models/leaderboard_entry_test.dart @@ -0,0 +1,41 @@ +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:test/test.dart'; + +void main() { + group('LeaderboardEntry', () { + const data = { + 'playerInitials': 'ABC', + 'score': 1500, + 'character': 'dash', + }; + + const leaderboardEntry = LeaderboardEntry( + playerInitials: 'ABC', + score: 1500, + character: CharacterType.dash, + ); + + test('can be instantiated', () { + const leaderboardEntry = LeaderboardEntry.empty; + + expect(leaderboardEntry, isNotNull); + }); + + test('supports value equality.', () { + const leaderboardEntry = LeaderboardEntry.empty; + const leaderboardEntry2 = LeaderboardEntry.empty; + + expect(leaderboardEntry, equals(leaderboardEntry2)); + }); + + test('can be converted to json', () { + expect(leaderboardEntry.toJson(), equals(data)); + }); + + test('can be obtained from json', () { + final leaderboardEntryFrom = LeaderboardEntry.fromJson(data); + + expect(leaderboardEntry, equals(leaderboardEntryFrom)); + }); + }); +} diff --git a/packages/leaderboard_repository/test/src/models/leaderboard_ranking_test.dart b/packages/leaderboard_repository/test/src/models/leaderboard_ranking_test.dart new file mode 100644 index 00000000..577251e4 --- /dev/null +++ b/packages/leaderboard_repository/test/src/models/leaderboard_ranking_test.dart @@ -0,0 +1,19 @@ +import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:test/test.dart'; + +void main() { + group('LeaderboardRanking', () { + test('can be instantiated', () { + const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1); + + expect(leaderboardRanking, isNotNull); + }); + + test('supports value equality.', () { + const leaderboardRanking = LeaderboardRanking(ranking: 1, outOf: 1); + const leaderboardRanking2 = LeaderboardRanking(ranking: 1, outOf: 1); + + expect(leaderboardRanking, equals(leaderboardRanking2)); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 73d0fc8d..79aa7138 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,6 +78,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + cloud_firestore: + dependency: transitive + description: + name: cloud_firestore + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.10" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.5.1" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.10" collection: dependency: transitive description: @@ -134,6 +155,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + firebase_core: + dependency: transitive + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.13.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.5" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.1" flame: dependency: "direct main" description: @@ -184,6 +226,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" forge2d: dependency: transitive description: @@ -246,7 +293,21 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.6.3" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + leaderboard_repository: + dependency: "direct main" + description: + path: "packages/leaderboard_repository" + relative: true + source: path + version: "1.0.0+1" logging: dependency: transitive description: @@ -338,6 +399,13 @@ packages: relative: true source: path version: "1.0.0+1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" pool: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 18905a10..abd78157 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: geometry: path: packages/geometry intl: ^0.17.0 + leaderboard_repository: + path: packages/leaderboard_repository pinball_theme: path: packages/pinball_theme