diff --git a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart index 30f6810f..9d8b2434 100644 --- a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart +++ b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart @@ -72,6 +72,20 @@ class FetchPlayerRankingException extends LeaderboardException { ); } +/// {@template fetch_prohibited_initials_exception} +/// Exception thrown when failure occurs while fetching prohibited initials. +/// {@endtemplate} +class FetchProhibitedInitialsException extends LeaderboardException { + /// {@macro fetch_prohibited_initials_exception} + const FetchProhibitedInitialsException( + Object error, + StackTrace stackTrace, + ) : super( + error, + stackTrace, + ); +} + /// {@template leaderboard_repository} /// Repository to access leaderboard data in Firebase Cloud Firestore. /// {@endtemplate} @@ -152,4 +166,25 @@ class LeaderboardRepository { throw FetchPlayerRankingException(error, stackTrace); } } + + /// Determines if the given [initials] are allowed. + Future areInitialsAllowed({required String initials}) async { + // Initials can only be three uppercase A-Z letters + final initialsRegex = RegExp(r'^[A-Z]{3}$'); + if (!initialsRegex.hasMatch(initials)) { + return false; + } + + try { + final document = await _firebaseFirestore + .collection('prohibitedInitials') + .doc('list') + .get(); + final prohibitedInitials = + document.get('prohibitedInitials') as List; + return !prohibitedInitials.contains(initials); + } on Exception catch (error, stackTrace) { + throw FetchProhibitedInitialsException(error, stackTrace); + } + } } diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart index 1341d3f4..9d31983f 100644 --- a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -21,6 +21,9 @@ class MockQueryDocumentSnapshot extends Mock class MockDocumentReference extends Mock implements DocumentReference> {} +class MockDocumentSnapshot extends Mock + implements DocumentSnapshot> {} + void main() { group('LeaderboardRepository', () { late FirebaseFirestore firestore; @@ -223,5 +226,94 @@ void main() { ); }); }); + + group('areInitialsAllowed', () { + late LeaderboardRepository leaderboardRepository; + late CollectionReference> collectionReference; + late DocumentReference> documentReference; + late DocumentSnapshot> documentSnapshot; + + setUp(() async { + collectionReference = MockCollectionReference(); + documentReference = MockDocumentReference(); + documentSnapshot = MockDocumentSnapshot(); + leaderboardRepository = LeaderboardRepository(firestore); + + when(() => firestore.collection('prohibitedInitials')) + .thenReturn(collectionReference); + when(() => collectionReference.doc('list')) + .thenReturn(documentReference); + when(() => documentReference.get()) + .thenAnswer((_) async => documentSnapshot); + when(() => documentSnapshot.get('prohibitedInitials')) + .thenReturn(['BAD']); + }); + + test('returns true if initials are three letters and allowed', () async { + final isUsernameAllowedResponse = + await leaderboardRepository.areInitialsAllowed( + initials: 'ABC', + ); + expect( + isUsernameAllowedResponse, + isTrue, + ); + }); + + test( + 'returns false if initials are shorter than 3 characters', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'AB'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test( + 'returns false if initials are longer than 3 characters', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'ABCD'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test( + 'returns false if initials contain a lowercase letter', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'AbC'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test( + 'returns false if initials contain a special character', + () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'A@C'); + expect(areInitialsAllowedResponse, isFalse); + }, + ); + + test('returns false if initials are forbidden', () async { + final areInitialsAllowedResponse = + await leaderboardRepository.areInitialsAllowed(initials: 'BAD'); + expect(areInitialsAllowedResponse, isFalse); + }); + + test( + 'throws FetchProhibitedInitialsException when Exception occurs ' + 'when trying to retrieve information from firestore', + () async { + when(() => firestore.collection('prohibitedInitials')) + .thenThrow(Exception('oops')); + expect( + () => leaderboardRepository.areInitialsAllowed(initials: 'ABC'), + throwsA(isA()), + ); + }, + ); + }); }); } diff --git a/packages/pinball_components/assets/images/boundary/bottom.png b/packages/pinball_components/assets/images/boundary/bottom.png index 90bfa493..806f7051 100644 Binary files a/packages/pinball_components/assets/images/boundary/bottom.png and b/packages/pinball_components/assets/images/boundary/bottom.png differ diff --git a/packages/pinball_components/assets/images/dino/bottom-wall.png b/packages/pinball_components/assets/images/dino/bottom-wall.png index 9aa42e12..6a20f1a7 100644 Binary files a/packages/pinball_components/assets/images/dino/bottom-wall.png and b/packages/pinball_components/assets/images/dino/bottom-wall.png differ diff --git a/packages/pinball_components/assets/images/dino/top-wall.png b/packages/pinball_components/assets/images/dino/top-wall.png index 18b92541..cb4c82f2 100644 Binary files a/packages/pinball_components/assets/images/dino/top-wall.png and b/packages/pinball_components/assets/images/dino/top-wall.png differ diff --git a/packages/pinball_components/lib/src/components/dino_walls.dart b/packages/pinball_components/lib/src/components/dino_walls.dart index ca748799..cf6d572d 100644 --- a/packages/pinball_components/lib/src/components/dino_walls.dart +++ b/packages/pinball_components/lib/src/components/dino_walls.dart @@ -34,24 +34,21 @@ class _DinoTopWall extends BodyComponent with InitialPosition { } List _createFixtureDefs() { - final fixturesDef = []; - final topStraightShape = EdgeShape() ..set( Vector2(28.65, -35.1), Vector2(29.5, -35.1), ); final topStraightFixtureDef = FixtureDef(topStraightShape); - fixturesDef.add(topStraightFixtureDef); final topCurveShape = BezierCurveShape( controlPoints: [ topStraightShape.vertex1, - Vector2(17.4, -26.38), - Vector2(25.5, -20.7), + Vector2(18.8, -27), + Vector2(26.6, -21), ], ); - fixturesDef.add(FixtureDef(topCurveShape)); + final topCurveFixtureDef = FixtureDef(topCurveShape); final middleCurveShape = BezierCurveShape( controlPoints: [ @@ -60,16 +57,16 @@ class _DinoTopWall extends BodyComponent with InitialPosition { Vector2(26.8, -19.5), ], ); - fixturesDef.add(FixtureDef(middleCurveShape)); + final middleCurveFixtureDef = FixtureDef(middleCurveShape); final bottomCurveShape = BezierCurveShape( controlPoints: [ middleCurveShape.vertices.last, - Vector2(21.5, -15.8), - Vector2(25.8, -14.8), + Vector2(23, -15), + Vector2(27, -15), ], ); - fixturesDef.add(FixtureDef(bottomCurveShape)); + final bottomCurveFixtureDef = FixtureDef(bottomCurveShape); final bottomStraightShape = EdgeShape() ..set( @@ -77,9 +74,14 @@ class _DinoTopWall extends BodyComponent with InitialPosition { Vector2(31, -14.5), ); final bottomStraightFixtureDef = FixtureDef(bottomStraightShape); - fixturesDef.add(bottomStraightFixtureDef); - return fixturesDef; + return [ + topStraightFixtureDef, + topCurveFixtureDef, + middleCurveFixtureDef, + bottomCurveFixtureDef, + bottomStraightFixtureDef, + ]; } @override @@ -106,12 +108,14 @@ class _DinoTopWallSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.dino.topWall.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.topWall.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; - position = Vector2(22, -41.8); + position = Vector2(22.8, -38.9); } } @@ -129,69 +133,56 @@ class _DinoBottomWall extends BodyComponent with InitialPosition { } List _createFixtureDefs() { - final fixturesDef = []; const restitution = 1.0; - final topStraightControlPoints = [ - Vector2(32.4, -8.8), - Vector2(25, -7.7), - ]; final topStraightShape = EdgeShape() ..set( - topStraightControlPoints.first, - topStraightControlPoints.last, + Vector2(32.4, -8.8), + Vector2(25, -7.7), ); final topStraightFixtureDef = FixtureDef( topStraightShape, restitution: restitution, ); - fixturesDef.add(topStraightFixtureDef); - final topLeftCurveControlPoints = [ - topStraightControlPoints.last, - Vector2(21.8, -7), - Vector2(29.5, 13.8), - ]; final topLeftCurveShape = BezierCurveShape( - controlPoints: topLeftCurveControlPoints, + controlPoints: [ + topStraightShape.vertex2, + Vector2(21.8, -7), + Vector2(29.8, 13.8), + ], ); final topLeftCurveFixtureDef = FixtureDef( topLeftCurveShape, restitution: restitution, ); - fixturesDef.add(topLeftCurveFixtureDef); - final bottomLeftStraightControlPoints = [ - topLeftCurveControlPoints.last, - Vector2(31.8, 44.1), - ]; final bottomLeftStraightShape = EdgeShape() ..set( - bottomLeftStraightControlPoints.first, - bottomLeftStraightControlPoints.last, + topLeftCurveShape.vertices.last, + Vector2(31.9, 44.1), ); final bottomLeftStraightFixtureDef = FixtureDef( bottomLeftStraightShape, restitution: restitution, ); - fixturesDef.add(bottomLeftStraightFixtureDef); - final bottomStraightControlPoints = [ - bottomLeftStraightControlPoints.last, - Vector2(37.8, 44.1), - ]; final bottomStraightShape = EdgeShape() ..set( - bottomStraightControlPoints.first, - bottomStraightControlPoints.last, + bottomLeftStraightShape.vertex2, + Vector2(37.8, 44.1), ); final bottomStraightFixtureDef = FixtureDef( bottomStraightShape, restitution: restitution, ); - fixturesDef.add(bottomStraightFixtureDef); - return fixturesDef; + return [ + topStraightFixtureDef, + topLeftCurveFixtureDef, + bottomLeftStraightFixtureDef, + bottomStraightFixtureDef, + ]; } @override @@ -212,8 +203,10 @@ class _DinoBottomWallSpriteComponent extends SpriteComponent with HasGameRef { @override Future onLoad() async { await super.onLoad(); - final sprite = await gameRef.loadSprite( - Assets.images.dino.bottomWall.keyName, + final sprite = Sprite( + gameRef.images.fromCache( + Assets.images.dino.bottomWall.keyName, + ), ); this.sprite = sprite; size = sprite.originalSize / 10; diff --git a/packages/pinball_components/sandbox/lib/main.dart b/packages/pinball_components/sandbox/lib/main.dart index 8709d694..e5f7f177 100644 --- a/packages/pinball_components/sandbox/lib/main.dart +++ b/packages/pinball_components/sandbox/lib/main.dart @@ -29,6 +29,7 @@ void main() { addLaunchRampStories(dashbook); addScoreTextStories(dashbook); addBackboardStories(dashbook); + addDinoWallStories(dashbook); runApp(dashbook); } diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart new file mode 100644 index 00000000..a6987fcc --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_wall/dino_wall_game.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:flame/input.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:sandbox/stories/ball/basic_ball_game.dart'; + +class DinoWallGame extends BallGame { + DinoWallGame() : super(); + + static const description = ''' + Shows how DinoWalls are rendered. + + - Activate the "trace" parameter to overlay the body. + - Tap anywhere on the screen to spawn a ball into the game. +'''; + + @override + Future onLoad() async { + await super.onLoad(); + + await images.loadAll([ + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, + ]); + + await addFromBlueprint(DinoWalls()); + camera.followVector2(Vector2.zero()); + await traceAllBodies(); + } +} diff --git a/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart new file mode 100644 index 00000000..e24d26cc --- /dev/null +++ b/packages/pinball_components/sandbox/lib/stories/dino_wall/stories.dart @@ -0,0 +1,11 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:sandbox/common/common.dart'; +import 'package:sandbox/stories/dino_wall/dino_wall_game.dart'; + +void addDinoWallStories(Dashbook dashbook) { + dashbook.storiesOf('DinoWall').addGame( + title: 'Traced', + description: DinoWallGame.description, + gameBuilder: (_) => DinoWallGame(), + ); +} diff --git a/packages/pinball_components/sandbox/lib/stories/stories.dart b/packages/pinball_components/sandbox/lib/stories/stories.dart index 90c38150..d8103b4d 100644 --- a/packages/pinball_components/sandbox/lib/stories/stories.dart +++ b/packages/pinball_components/sandbox/lib/stories/stories.dart @@ -4,6 +4,7 @@ export 'ball/stories.dart'; export 'baseboard/stories.dart'; export 'boundaries/stories.dart'; export 'chrome_dino/stories.dart'; +export 'dino_wall/stories.dart'; export 'effects/stories.dart'; export 'flipper/stories.dart'; export 'flutter_forest/stories.dart'; diff --git a/packages/pinball_components/test/src/components/dino_walls_test.dart b/packages/pinball_components/test/src/components/dino_walls_test.dart index b3a58264..ff64fb00 100644 --- a/packages/pinball_components/test/src/components/dino_walls_test.dart +++ b/packages/pinball_components/test/src/components/dino_walls_test.dart @@ -11,15 +11,23 @@ import '../../helpers/helpers.dart'; void main() { group('DinoWalls', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(TestGame.new); + final assets = [ + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, + ]; + final flameTester = FlameTester(() => TestGame(assets)); flameTester.testGameWidget( 'renders correctly', setUp: (game, tester) async { + await game.images.loadAll(assets); await game.addFromBlueprint(DinoWalls()); + await game.ready(); + game.camera.followVector2(Vector2.zero()); game.camera.zoom = 6.5; - await game.ready(); + + await tester.pump(); }, verify: (game, tester) async { await expectLater( diff --git a/packages/pinball_components/test/src/components/golden/boundaries.png b/packages/pinball_components/test/src/components/golden/boundaries.png index 6cb24bbd..2612679a 100644 Binary files a/packages/pinball_components/test/src/components/golden/boundaries.png and b/packages/pinball_components/test/src/components/golden/boundaries.png differ diff --git a/packages/pinball_components/test/src/components/golden/dino-walls.png b/packages/pinball_components/test/src/components/golden/dino-walls.png index 8c2ee569..5956b43b 100644 Binary files a/packages/pinball_components/test/src/components/golden/dino-walls.png and b/packages/pinball_components/test/src/components/golden/dino-walls.png differ diff --git a/packages/share_repository/lib/share_repository.dart b/packages/share_repository/lib/share_repository.dart index 0b6d064c..0a68aff4 100644 --- a/packages/share_repository/lib/share_repository.dart +++ b/packages/share_repository/lib/share_repository.dart @@ -1,3 +1,4 @@ library share_repository; +export 'src/models/models.dart'; export 'src/share_repository.dart'; diff --git a/packages/share_repository/lib/src/models/models.dart b/packages/share_repository/lib/src/models/models.dart new file mode 100644 index 00000000..26819946 --- /dev/null +++ b/packages/share_repository/lib/src/models/models.dart @@ -0,0 +1 @@ +export 'share_platform.dart'; diff --git a/packages/share_repository/lib/src/models/share_platform.dart b/packages/share_repository/lib/src/models/share_platform.dart new file mode 100644 index 00000000..054a4f15 --- /dev/null +++ b/packages/share_repository/lib/src/models/share_platform.dart @@ -0,0 +1,8 @@ +/// The platform that is being used to share a score. +enum SharePlatform { + /// Twitter platform. + twitter, + + /// Facebook platform. + facebook, +} diff --git a/packages/share_repository/lib/src/share_repository.dart b/packages/share_repository/lib/src/share_repository.dart index 6c1f36d0..6e6679c2 100644 --- a/packages/share_repository/lib/src/share_repository.dart +++ b/packages/share_repository/lib/src/share_repository.dart @@ -1,7 +1,30 @@ +import 'package:share_repository/share_repository.dart'; + /// {@template share_repository} /// Repository to facilitate sharing scores. /// {@endtemplate} class ShareRepository { /// {@macro share_repository} - const ShareRepository(); + const ShareRepository({ + required String appUrl, + }) : _appUrl = appUrl; + + final String _appUrl; + + /// Returns a url to share the [value] on the given [platform]. + /// + /// The returned url can be opened using the [url_launcher](https://pub.dev/packages/url_launcher) package. + String shareText({ + required String value, + required SharePlatform platform, + }) { + final encodedUrl = Uri.encodeComponent(_appUrl); + final encodedShareText = Uri.encodeComponent(value); + switch (platform) { + case SharePlatform.twitter: + return 'https://twitter.com/intent/tweet?url=$encodedUrl&text=$encodedShareText'; + case SharePlatform.facebook: + return 'https://www.facebook.com/sharer.php?u=$encodedUrl"e=$encodedShareText'; + } + } } diff --git a/packages/share_repository/test/src/share_repository_test.dart b/packages/share_repository/test/src/share_repository_test.dart index e6cc536b..bdb2c517 100644 --- a/packages/share_repository/test/src/share_repository_test.dart +++ b/packages/share_repository/test/src/share_repository_test.dart @@ -4,8 +4,38 @@ import 'package:test/test.dart'; void main() { group('ShareRepository', () { + const appUrl = 'https://fakeurl.com/'; + late ShareRepository shareRepository; + + setUp(() { + shareRepository = ShareRepository(appUrl: appUrl); + }); + test('can be instantiated', () { - expect(ShareRepository(), isNotNull); + expect(ShareRepository(appUrl: appUrl), isNotNull); + }); + + group('shareText', () { + const value = 'hello world!'; + test('returns the correct share url for twitter', () async { + const shareTextUrl = + 'https://twitter.com/intent/tweet?url=https%3A%2F%2Ffakeurl.com%2F&text=hello%20world!'; + final shareTextResult = shareRepository.shareText( + value: value, + platform: SharePlatform.twitter, + ); + expect(shareTextResult, equals(shareTextUrl)); + }); + + test('returns the correct share url for facebook', () async { + const shareTextUrl = + 'https://www.facebook.com/sharer.php?u=https%3A%2F%2Ffakeurl.com%2F"e=hello%20world!'; + final shareTextResult = shareRepository.shareText( + value: value, + platform: SharePlatform.facebook, + ); + expect(shareTextResult, equals(shareTextUrl)); + }); }); }); } diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 20b6a2cd..f5d27b31 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -56,6 +56,8 @@ void main() { Assets.images.boundary.bottom.keyName, Assets.images.slingshot.upper.keyName, Assets.images.slingshot.lower.keyName, + Assets.images.dino.topWall.keyName, + Assets.images.dino.bottomWall.keyName, ]; final flameTester = FlameTester(() => PinballTestGame(assets)); final debugModeFlameTester = FlameTester(() => DebugPinballTestGame(assets));