diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 1217e1bb6..3399b650e 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -37,6 +37,7 @@ class App extends StatelessWidget { providers: [ Provider.value(value: apiClient.crosswordResource), Provider.value(value: apiClient.leaderboardResource), + Provider.value(value: apiClient.shareResource), Provider.value(value: apiClient.hintResource), Provider.value(value: user), Provider.value(value: crosswordRepository), diff --git a/lib/share/view/share_score_page.dart b/lib/share/view/share_score_page.dart index 3bf0ead01..57da7402b 100644 --- a/lib/share/view/share_score_page.dart +++ b/lib/share/view/share_score_page.dart @@ -1,4 +1,6 @@ +import 'package:api_client/api_client.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:io_crossword/l10n/l10n.dart'; import 'package:io_crossword/player/player.dart'; import 'package:io_crossword/share/share.dart'; @@ -12,10 +14,14 @@ class ShareScorePage extends StatelessWidget { context: context, builder: (context) { final l10n = context.l10n; + final shareResource = context.read(); return ShareDialog( title: l10n.shareYourScore, content: const ShareScorePage(), + facebookShareUrl: shareResource.facebookShareBaseUrl(), + linkedInShareUrl: shareResource.linkedinShareBaseUrl(), + twitterShareUrl: shareResource.twitterShareBaseUrl(), ); }, ); diff --git a/lib/share/view/share_word_page.dart b/lib/share/view/share_word_page.dart index 8c9494d69..2b021f4c6 100644 --- a/lib/share/view/share_word_page.dart +++ b/lib/share/view/share_word_page.dart @@ -1,4 +1,6 @@ +import 'package:api_client/api_client.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:game_domain/game_domain.dart'; import 'package:io_crossword/l10n/l10n.dart'; import 'package:io_crossword/share/share.dart'; @@ -14,12 +16,16 @@ class ShareWordPage extends StatelessWidget { context: context, builder: (context) { final l10n = context.l10n; + final shareResource = context.read(); return ShareDialog( title: l10n.shareThisWord, content: ShareWordPage( word: word, ), + facebookShareUrl: shareResource.facebookShareBaseUrl(), + linkedInShareUrl: shareResource.linkedinShareBaseUrl(), + twitterShareUrl: shareResource.twitterShareBaseUrl(), ); }, ); diff --git a/lib/share/widgets/share_dialog.dart b/lib/share/widgets/share_dialog.dart index 0f495fc82..8bb891730 100644 --- a/lib/share/widgets/share_dialog.dart +++ b/lib/share/widgets/share_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:io_crossword/extensions/context_ext.dart'; import 'package:io_crossword/l10n/l10n.dart'; import 'package:io_crossword_ui/io_crossword_ui.dart'; @@ -6,11 +7,17 @@ class ShareDialog extends StatelessWidget { const ShareDialog({ required this.title, required this.content, + required this.facebookShareUrl, + required this.twitterShareUrl, + required this.linkedInShareUrl, super.key, }); final Widget content; final String title; + final String facebookShareUrl; + final String twitterShareUrl; + final String linkedInShareUrl; @override Widget build(BuildContext context) { @@ -35,24 +42,26 @@ class ShareDialog extends StatelessWidget { l10n.shareOn, ), const SizedBox(height: IoCrosswordSpacing.xlgsm), - const Row( + Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( - onPressed: null, - icon: Icon(IoIcons.linkedin), + onPressed: () { + context.launchUrl(linkedInShareUrl); + }, + icon: const Icon(IoIcons.linkedin), ), IconButton( - onPressed: null, - icon: Icon(IoIcons.instagram), + onPressed: () { + context.launchUrl(twitterShareUrl); + }, + icon: const Icon(IoIcons.twitter), ), IconButton( - onPressed: null, - icon: Icon(IoIcons.twitter), - ), - IconButton( - onPressed: null, - icon: Icon(IoIcons.facebook), + onPressed: () { + context.launchUrl(facebookShareUrl); + }, + icon: const Icon(IoIcons.facebook), ), ], ), diff --git a/packages/api_client/lib/src/api_client.dart b/packages/api_client/lib/src/api_client.dart index 1bf764566..e30cf190c 100644 --- a/packages/api_client/lib/src/api_client.dart +++ b/packages/api_client/lib/src/api_client.dart @@ -59,6 +59,9 @@ class ApiClient { late final CrosswordResource crosswordResource = CrosswordResource(apiClient: this); + /// {@macro share_resource} + late final ShareResource shareResource = ShareResource(apiClient: this); + /// {@macro hint_resource} late final HintResource hintResource = HintResource(apiClient: this); @@ -100,6 +103,11 @@ class ApiClient { }); } + /// Returns the base url. + String get baseUrl { + return _base.toString(); + } + /// Returns the score share url for the score for the specified [userId]. String shareScoreUrl(String userId) { return _base.replace( diff --git a/packages/api_client/lib/src/resources/share_resource.dart b/packages/api_client/lib/src/resources/share_resource.dart index d142f5291..5d8a3b5fb 100644 --- a/packages/api_client/lib/src/resources/share_resource.dart +++ b/packages/api_client/lib/src/resources/share_resource.dart @@ -11,7 +11,9 @@ class ShareResource { final ApiClient _apiClient; - final _tweetContent = 'Check out my score at IOCrossword #GoogleIO!'; + final _tweetScoreContent = 'Check out my score at IOCrossword #GoogleIO!'; + + final _tweetContent = 'Check out IOCrossword #GoogleIO!'; String _twitterShareUrl(String text) => 'https://twitter.com/intent/tweet?text=$text'; @@ -40,7 +42,7 @@ class ShareResource { String twitterShareScoreUrl(String userId) { final shareUrl = _apiClient.shareScoreUrl(userId); final content = [ - _tweetContent, + _tweetScoreContent, shareUrl, ]; return _twitterShareUrl(_encode(content)); @@ -85,4 +87,25 @@ class ShareResource { final shareUrl = _apiClient.shareWordUrl(sectionId, wordId); return _linkedinShareUrl(shareUrl); } + + /// Returns the url to share a facebook post. + String facebookShareBaseUrl() { + return _facebookShareUrl(_apiClient.baseUrl); + } + + /// Returns the url to share a twitter post. + String twitterShareBaseUrl() { + final shareUrl = _apiClient.baseUrl; + final content = [ + _tweetContent, + shareUrl, + ]; + return _twitterShareUrl(_encode(content)); + } + + /// Returns the url to share a linkedin post. + String linkedinShareBaseUrl() { + final shareUrl = _apiClient.baseUrl; + return _linkedinShareUrl(shareUrl); + } } diff --git a/packages/api_client/test/src/api_client_test.dart b/packages/api_client/test/src/api_client_test.dart index 1a53c1e51..8d0605d67 100644 --- a/packages/api_client/test/src/api_client_test.dart +++ b/packages/api_client/test/src/api_client_test.dart @@ -124,6 +124,15 @@ void main() { }); }); + group('shareScoreUrl', () { + test('returns the correct url', () { + expect( + subject.baseUrl, + equals('http://baseurl.com'), + ); + }); + }); + group('shareScoreUrl', () { test('returns the correct url', () { expect( diff --git a/packages/api_client/test/src/resources/share_resource_test.dart b/packages/api_client/test/src/resources/share_resource_test.dart index af73154e7..2fd7aa8cb 100644 --- a/packages/api_client/test/src/resources/share_resource_test.dart +++ b/packages/api_client/test/src/resources/share_resource_test.dart @@ -82,5 +82,38 @@ void main() { contains('wordUrl'), ); }); + + test('facebookShareBaseUrl returns the correct url', () { + when(() => apiClient.baseUrl).thenReturn('https://baseurl'); + + expect( + resource.facebookShareBaseUrl(), + equals('https://www.facebook.com/sharer.php?u=https://baseurl'), + ); + }); + + test('twitterShareBaseUrl returns the correct url', () { + when(() => apiClient.baseUrl).thenReturn('https://baseurl'); + + expect( + resource.twitterShareBaseUrl(), + equals( + 'https://twitter.com/intent/tweet?text=Check%20out' + '%20IOCrossword%20%23GoogleIO!%0ahttps://baseurl', + ), + ); + }); + + test('linkedinShareBaseUrl returns the correct url', () { + when(() => apiClient.baseUrl).thenReturn('https://baseurl'); + + expect( + resource.linkedinShareBaseUrl(), + equals( + 'https://www.linkedin.com/sharing/share-offsite/' + '?url=https://baseurl', + ), + ); + }); }); } diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index abd2e5059..939fb5be3 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -37,6 +37,23 @@ class _MockUser extends Mock implements User { String get id => ''; } +class _MockShareResource extends Mock implements ShareResource { + @override + String facebookShareBaseUrl() { + return 'https://facebook'; + } + + @override + String linkedinShareBaseUrl() { + return 'https://linkedin'; + } + + @override + String twitterShareBaseUrl() { + return 'https://twitter'; + } +} + extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { @@ -47,6 +64,7 @@ extension PumpApp on WidgetTester { LeaderboardRepository? leaderboardRepository, CrosswordResource? crosswordResource, LeaderboardResource? leaderboardResource, + ShareResource? shareResource, HintResource? hintResource, CrosswordBloc? crosswordBloc, PlayerBloc? playerBloc, @@ -77,6 +95,9 @@ extension PumpApp on WidgetTester { Provider.value( value: crosswordResource ?? mockedCrosswordResource, ), + Provider.value( + value: shareResource ?? _MockShareResource(), + ), Provider.value( value: crosswordRepository ?? mockedCrosswordRepository, ), diff --git a/test/share/view/share_score_page_test.dart b/test/share/view/share_score_page_test.dart index d435af6fc..7114cba24 100644 --- a/test/share/view/share_score_page_test.dart +++ b/test/share/view/share_score_page_test.dart @@ -1,12 +1,16 @@ // ignore_for_file: prefer_const_constructors +import 'package:api_client/api_client.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:io_crossword/player/player.dart'; import 'package:io_crossword/share/share.dart'; +import 'package:mocktail/mocktail.dart'; import '../../helpers/helpers.dart'; +class _MockShareResource extends Mock implements ShareResource {} + void main() { group('$ShareScorePage', () { testWidgets( @@ -27,28 +31,144 @@ void main() { }, ); - testWidgets( - 'showModal opens the $ShareScorePage in a $ShareDialog', - (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (BuildContext context) { - return ElevatedButton( - onPressed: () => ShareScorePage.showModal(context), - child: Text('Show ShareScorePage'), - ); - }, + group('showModal', () { + late ShareResource shareResource; + + setUp(() { + shareResource = _MockShareResource(); + }); + + testWidgets( + 'opens the $ShareScorePage in a $ShareDialog', + (tester) async { + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () => ShareScorePage.showModal(context), + child: Text('Show ShareScorePage'), + ); + }, + ), ), - ), - ); + ); - await tester.tap(find.byType(ElevatedButton)); - await tester.pumpAndSettle(); + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); - expect(find.byType(ShareDialog), findsOneWidget); - expect(find.byType(ShareScorePage), findsOneWidget); - }, - ); + expect(find.byType(ShareDialog), findsOneWidget); + expect(find.byType(ShareScorePage), findsOneWidget); + }, + ); + + testWidgets( + 'uses correct facebook url', + (tester) async { + when(() => shareResource.facebookShareBaseUrl()) + .thenReturn('https://facebook'); + when(() => shareResource.twitterShareBaseUrl()) + .thenReturn('https://twitter'); + when(() => shareResource.linkedinShareBaseUrl()) + .thenReturn('https://linkedin'); + + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () => ShareScorePage.showModal(context), + child: Text('open share score'), + ); + }, + ), + ), + shareResource: shareResource, + ); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + expect( + tester + .widget(find.byType(ShareDialog)) + .facebookShareUrl, + equals('https://facebook'), + ); + }, + ); + + testWidgets( + 'uses correct twitter url', + (tester) async { + when(() => shareResource.facebookShareBaseUrl()) + .thenReturn('https://facebook'); + when(() => shareResource.twitterShareBaseUrl()) + .thenReturn('https://twitter'); + when(() => shareResource.linkedinShareBaseUrl()) + .thenReturn('https://linkedin'); + + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () => ShareScorePage.showModal(context), + child: Text('open share score'), + ); + }, + ), + ), + shareResource: shareResource, + ); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + expect( + tester + .widget(find.byType(ShareDialog)) + .twitterShareUrl, + equals('https://twitter'), + ); + }, + ); + + testWidgets( + 'uses correct linkedin url', + (tester) async { + when(() => shareResource.facebookShareBaseUrl()) + .thenReturn('https://facebook'); + when(() => shareResource.twitterShareBaseUrl()) + .thenReturn('https://twitter'); + when(() => shareResource.linkedinShareBaseUrl()) + .thenReturn('https://linkedin'); + + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () => ShareScorePage.showModal(context), + child: Text('open share score'), + ); + }, + ), + ), + shareResource: shareResource, + ); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + expect( + tester + .widget(find.byType(ShareDialog)) + .linkedInShareUrl, + equals('https://linkedin'), + ); + }, + ); + }); }); } diff --git a/test/share/view/share_word_page_test.dart b/test/share/view/share_word_page_test.dart index ebd5db125..15a9fe983 100644 --- a/test/share/view/share_word_page_test.dart +++ b/test/share/view/share_word_page_test.dart @@ -1,10 +1,12 @@ // ignore_for_file: prefer_const_constructors +import 'package:api_client/api_client.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:game_domain/game_domain.dart'; import 'package:io_crossword/share/share.dart'; import 'package:io_crossword_ui/io_crossword_ui.dart'; +import 'package:mocktail/mocktail.dart'; import '../../helpers/helpers.dart'; @@ -19,6 +21,8 @@ class _FakeWord extends Fake implements Word { int get length => 5; } +class _MockShareResource extends Mock implements ShareResource {} + void main() { group('$ShareWordPage', () { testWidgets( @@ -33,32 +37,156 @@ void main() { expect(find.text(word.clue), findsOneWidget); }, ); + group('showModal', () { + late ShareResource shareResource; - testWidgets( - 'showModal opens the $ShareWordPage in a $ShareDialog', - (tester) async { - await tester.pumpApp( - Scaffold( - body: Builder( - builder: (BuildContext context) { - return ElevatedButton( - onPressed: () => ShareWordPage.showModal( - context, - _FakeWord(), - ), - child: Text('Show ShareWordPage'), - ); - }, + setUp(() { + shareResource = _MockShareResource(); + }); + + testWidgets( + 'opens the $ShareWordPage in a $ShareDialog', + (tester) async { + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () => ShareWordPage.showModal( + context, + _FakeWord(), + ), + child: Text('Show ShareWordPage'), + ); + }, + ), ), - ), - ); + ); - await tester.tap(find.byType(ElevatedButton)); - await tester.pumpAndSettle(); + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); - expect(find.byType(ShareDialog), findsOneWidget); - expect(find.byType(ShareWordPage), findsOneWidget); - }, - ); + expect(find.byType(ShareDialog), findsOneWidget); + expect(find.byType(ShareWordPage), findsOneWidget); + }, + ); + + testWidgets( + 'uses correct facebook url', + (tester) async { + when(() => shareResource.facebookShareBaseUrl()) + .thenReturn('https://facebook'); + when(() => shareResource.twitterShareBaseUrl()) + .thenReturn('https://twitter'); + when(() => shareResource.linkedinShareBaseUrl()) + .thenReturn('https://linkedin'); + + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () => ShareWordPage.showModal( + context, + _FakeWord(), + ), + child: Text('open share score'), + ); + }, + ), + ), + shareResource: shareResource, + ); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + expect( + tester + .widget(find.byType(ShareDialog)) + .facebookShareUrl, + equals('https://facebook'), + ); + }, + ); + + testWidgets( + 'uses correct twitter url', + (tester) async { + when(() => shareResource.facebookShareBaseUrl()) + .thenReturn('https://facebook'); + when(() => shareResource.twitterShareBaseUrl()) + .thenReturn('https://twitter'); + when(() => shareResource.linkedinShareBaseUrl()) + .thenReturn('https://linkedin'); + + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () => ShareWordPage.showModal( + context, + _FakeWord(), + ), + child: Text('open share score'), + ); + }, + ), + ), + shareResource: shareResource, + ); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + expect( + tester + .widget(find.byType(ShareDialog)) + .twitterShareUrl, + equals('https://twitter'), + ); + }, + ); + + testWidgets( + 'uses correct linkedin url', + (tester) async { + when(() => shareResource.facebookShareBaseUrl()) + .thenReturn('https://facebook'); + when(() => shareResource.twitterShareBaseUrl()) + .thenReturn('https://twitter'); + when(() => shareResource.linkedinShareBaseUrl()) + .thenReturn('https://linkedin'); + + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () => ShareWordPage.showModal( + context, + _FakeWord(), + ), + child: Text('open share score'), + ); + }, + ), + ), + shareResource: shareResource, + ); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + + expect( + tester + .widget(find.byType(ShareDialog)) + .linkedInShareUrl, + equals('https://linkedin'), + ); + }, + ); + }); }); } diff --git a/test/share/widgets/share_dialog_test.dart b/test/share/widgets/share_dialog_test.dart index 4f3626af9..085beab01 100644 --- a/test/share/widgets/share_dialog_test.dart +++ b/test/share/widgets/share_dialog_test.dart @@ -3,16 +3,45 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:io_crossword/share/widgets/share_dialog.dart'; +import 'package:io_crossword_ui/io_crossword_ui.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import '../../helpers/helpers.dart'; +class _MockUrlLauncherPlatform extends Mock + with MockPlatformInterfaceMixin + implements UrlLauncherPlatform {} + +class _FakeLaunchOptions extends Fake implements LaunchOptions {} + void main() { group('ShareDialog', () { + late UrlLauncherPlatform urlLauncher; + + setUpAll(() async { + registerFallbackValue(_FakeLaunchOptions()); + }); + + setUp(() { + urlLauncher = _MockUrlLauncherPlatform(); + + UrlLauncherPlatform.instance = urlLauncher; + + when(() => urlLauncher.canLaunch(any())).thenAnswer((_) async => true); + when(() => urlLauncher.launchUrl(any(), any())) + .thenAnswer((_) async => true); + }); + testWidgets('renders title', (tester) async { await tester.pumpApp( ShareDialog( title: 'title', content: const Text('test'), + twitterShareUrl: 'https://twitter', + linkedInShareUrl: 'https://linkedin', + facebookShareUrl: 'https://facebook', ), ); @@ -24,10 +53,79 @@ void main() { ShareDialog( title: 'title', content: const Text('test'), + twitterShareUrl: 'https://twitter', + linkedInShareUrl: 'https://linkedin', + facebookShareUrl: 'https://facebook', ), ); expect(find.text('test'), findsOneWidget); }); + + testWidgets('launches linkedIn url when linkedin icon is tapped', + (tester) async { + await tester.pumpApp( + ShareDialog( + title: 'title', + content: const Text('test'), + twitterShareUrl: 'https://twitter', + linkedInShareUrl: 'https://linkedin', + facebookShareUrl: 'https://facebook', + ), + ); + + await tester.tap(find.byIcon(IoIcons.linkedin)); + + verify( + () => urlLauncher.launchUrl( + 'https://linkedin', + any(), + ), + ).called(1); + }); + + testWidgets('launches twitter url when twitter icon is tapped', + (tester) async { + await tester.pumpApp( + ShareDialog( + title: 'title', + content: const Text('test'), + twitterShareUrl: 'https://twitter', + linkedInShareUrl: 'https://linkedin', + facebookShareUrl: 'https://facebook', + ), + ); + + await tester.tap(find.byIcon(IoIcons.twitter)); + + verify( + () => urlLauncher.launchUrl( + 'https://twitter', + any(), + ), + ).called(1); + }); + + testWidgets('launches facebook url when facebook icon is tapped', + (tester) async { + await tester.pumpApp( + ShareDialog( + title: 'title', + content: const Text('test'), + twitterShareUrl: 'https://twitter', + linkedInShareUrl: 'https://linkedin', + facebookShareUrl: 'https://facebook', + ), + ); + + await tester.tap(find.byIcon(IoIcons.facebook)); + + verify( + () => urlLauncher.launchUrl( + 'https://facebook', + any(), + ), + ).called(1); + }); }); }