diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d9d4ab5..968087e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -34,6 +34,16 @@ + + + + + + + + diff --git a/assets/images/user.png b/assets/images/user.png index 50a8ec0..22dc0b0 100644 Binary files a/assets/images/user.png and b/assets/images/user.png differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d29fcc9..72baf42 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 22230FEC84FAAF7E8AA781A7 /* Pods-Runner.release-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-development.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-development.xcconfig"; sourceTree = ""; }; 230C707E8D6A9E6A1DC18028 /* Pods-Runner.release-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-staging.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-staging.xcconfig"; sourceTree = ""; }; 2D785F1A266C2C4E00E43AE3 /* config */ = {isa = PBXFileReference; lastKnownFileType = folder; path = config; sourceTree = ""; }; + 2DBBAF542734E8E30043162B /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 4019077285505CC6EADC6A13 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 451976F66DC4F31C5F96421B /* Pods-Runner.profile-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-development.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-development.xcconfig"; sourceTree = ""; }; @@ -103,6 +104,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 2DBBAF542734E8E30043162B /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -408,6 +410,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -545,6 +548,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -574,6 +578,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -658,6 +663,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -740,6 +746,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -819,6 +826,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -903,6 +911,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -985,6 +994,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; @@ -1064,6 +1074,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..0d677e6 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:spotvideo.app + + + diff --git a/ios/fastlane/metadata/en-US/release_notes.txt b/ios/fastlane/metadata/en-US/release_notes.txt index a2de267..81f8952 100644 --- a/ios/fastlane/metadata/en-US/release_notes.txt +++ b/ios/fastlane/metadata/en-US/release_notes.txt @@ -1,2 +1,2 @@ -- Nearby videos will be clustered together -- Sharing videos will now share spot web page \ No newline at end of file +- Fixed a bug where clicking on shared video link does not open the video page within the app +- Updated the defualt user icon \ No newline at end of file diff --git a/lib/app/app.dart b/lib/app/app.dart index 69ea87f..402fc8d 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:spot/cubits/notification/notification_cubit.dart'; +import 'package:spot/data_profiders/app_link_provider.dart'; import 'package:spot/data_profiders/location_provider.dart'; import 'package:spot/pages/tab_page.dart'; import 'package:spot/repositories/repository.dart'; @@ -86,7 +87,9 @@ class App extends StatelessWidget { navigatorObservers: [ FirebaseAnalyticsObserver(analytics: _analytics), ], - home: TabPage(), + home: TabPage( + appLinkProvider: AppLinkProvider(), + ), ), ), ); diff --git a/lib/data_profiders/app_link_provider.dart b/lib/data_profiders/app_link_provider.dart new file mode 100644 index 0000000..2f39978 --- /dev/null +++ b/lib/data_profiders/app_link_provider.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:spot/pages/view_video_page.dart'; +import 'package:spot/utils/constants.dart'; +import 'package:uni_links/uni_links.dart'; + +/// Takes care of app links +class AppLinkProvider { + /// Sets up the receiver of app links + Future setupAppLinks(BuildContext context) async { + /// Quick and dirty way of + /// waiting until TabPage gets added to the widget tree. + await Future.delayed(const Duration(milliseconds: 300)); + + try { + final initialUri = await getInitialUri(); + _handleAppLink(uri: initialUri, context: context); + uriLinkStream.listen((uri) => _handleAppLink(uri: uri, context: context)); + } catch (_) { + context.showErrorSnackbar('Error opening the video.'); + } + } + + void _handleAppLink({required Uri? uri, required BuildContext context}) { + if (uri != null) { + final path = uri.path; + if (path.split('/').first == 'post') { + final videoId = path.split('/').last; + Navigator.of(context).push(ViewVideoPage.route(videoId: videoId)); + } + } + } +} diff --git a/lib/pages/tab_page.dart b/lib/pages/tab_page.dart index 41166be..1b310e5 100644 --- a/lib/pages/tab_page.dart +++ b/lib/pages/tab_page.dart @@ -7,6 +7,7 @@ import 'package:spot/components/gradient_border.dart'; import 'package:spot/components/gradient_button.dart'; import 'package:spot/components/notification_dot.dart'; import 'package:spot/cubits/notification/notification_cubit.dart'; +import 'package:spot/data_profiders/app_link_provider.dart'; import 'package:spot/pages/record_page.dart'; import 'package:spot/pages/tabs/map_tab.dart'; import 'package:spot/pages/tabs/notifications_tab.dart'; @@ -22,16 +23,13 @@ import 'record_page.dart'; /// Page that holds tab navigation at the bottom. /// This is the first page presented to the user. class TabPage extends StatefulWidget { - /// Name of this page within `RouteSettinngs` - static const name = 'TabPage'; + /// Page that holds tab navigation at the bottom. + /// This is the first page presented to the user. + const TabPage({Key? key, required AppLinkProvider appLinkProvider}) + : _appLinkProvider = appLinkProvider, + super(key: key); - /// Method ot create this page with necessary `BlocProvider` - static Route route() { - return MaterialPageRoute( - settings: const RouteSettings(name: name), - builder: (_) => TabPage(), - ); - } + final AppLinkProvider _appLinkProvider; @override TabPageState createState() => TabPageState(); @@ -196,6 +194,7 @@ class TabPageState extends State { @override void initState() { super.initState(); + widget._appLinkProvider.setupAppLinks(context); WidgetsBinding.instance ?.addObserver(LifecycleEventHandler(resumeCallBack: onResumed)); } diff --git a/lib/pages/view_video_page.dart b/lib/pages/view_video_page.dart index d3f4e6c..55dfcb2 100644 --- a/lib/pages/view_video_page.dart +++ b/lib/pages/view_video_page.dart @@ -21,7 +21,6 @@ import 'package:spot/utils/functions.dart'; import 'package:video_player/video_player.dart'; import '../utils/constants.dart'; -import 'tab_page.dart'; @visibleForTesting @@ -412,9 +411,8 @@ class __DeletingDialogContentState extends State<_DeletingDialogContent> { _loading = true; }); await widget._videoCubit.delete(); - Navigator.of(context).popUntil( - (route) => route.settings.name == TabPage.name, - ); + Navigator.of(context) + .popUntil((route) => route.isFirst); } catch (err) { setState(() { _loading = false; @@ -482,9 +480,8 @@ class __BlockingDialogContentState extends State<_BlockingDialogContent> { _loading = true; }); await widget._videoCubit.block(widget._blockedUserId); - Navigator.of(context).popUntil( - (route) => route.settings.name == TabPage.name, - ); + Navigator.of(context) + .popUntil((route) => route.isFirst); } catch (err) { setState(() { _loading = false; diff --git a/pubspec.lock b/pubspec.lock index bea221c..e012681 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -838,6 +838,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + uni_links: + dependency: "direct main" + description: + name: uni_links + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.1" + uni_links_platform_interface: + dependency: transitive + description: + name: uni_links_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + uni_links_web: + dependency: transitive + description: + name: uni_links_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" universal_io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7c30409..13199f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: spot description: Find local video posts -version: 1.4.2+64 +version: 1.4.3+65 publish_to: none environment: @@ -32,6 +32,7 @@ dependencies: flutter_video_info: ^1.2.0 shared_preferences: ^2.0.6 google_maps_cluster_manager: ^3.0.0+1 + uni_links: ^0.5.1 dev_dependencies: flutter_test: diff --git a/test/pages/tab_page_test.dart b/test/pages/tab_page_test.dart index ca6d768..9b671c8 100644 --- a/test/pages/tab_page_test.dart +++ b/test/pages/tab_page_test.dart @@ -9,6 +9,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:spot/components/frosted_dialog.dart'; import 'package:spot/components/notification_dot.dart'; import 'package:spot/cubits/notification/notification_cubit.dart'; +import 'package:spot/data_profiders/app_link_provider.dart'; import 'package:spot/models/notification.dart'; import 'package:spot/pages/edit_profile_page.dart'; import 'package:spot/pages/login_page.dart'; @@ -25,9 +26,21 @@ import '../test_resources/constants.dart'; class MockNavigatorObserver extends Mock implements NavigatorObserver {} +class MockAppLinkProvider extends Mock implements AppLinkProvider {} + +class BuildContextFake extends Fake implements BuildContext {} + void main() { + late final AppLinkProvider _appLinkProvider; + /// This will allow http request to be sent within test code - setUpAll(() => HttpOverrides.global = null); + setUpAll(() { + HttpOverrides.global = null; + _appLinkProvider = MockAppLinkProvider(); + registerFallbackValue(BuildContextFake()); + when(() => _appLinkProvider.setupAppLinks(any())) + .thenAnswer((_) async => null); + }); group('TabPage', () { group('signed in', () { late final Repository repository; @@ -40,7 +53,7 @@ void main() { .thenReturn(Completer()..complete()); }); testWidgets('Every tab gets rendered', (tester) async { - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -59,7 +72,7 @@ void main() { }); testWidgets('Initial index is 0', (tester) async { - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -75,7 +88,7 @@ void main() { }); testWidgets('Tapping Home goes to tab index 0', (tester) async { - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -92,7 +105,7 @@ void main() { expect(tabPage.createState().currentIndex, 0); }); testWidgets('Tapping Search goes to tab index 1', (tester) async { - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -111,7 +124,7 @@ void main() { }); testWidgets('Tapping Notifications goes to tab index 2 when signed in', (tester) async { - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -132,7 +145,7 @@ void main() { }); testWidgets('Tapping Profile goes to tab index 3 when signed in', (tester) async { - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -169,7 +182,7 @@ void main() { create: (context) => NotificationCubit( repository: RepositoryProvider.of(context))), ], - child: TabPage(), + child: TabPage(appLinkProvider: _appLinkProvider), ), repository: repository, ); @@ -191,7 +204,7 @@ void main() { create: (context) => NotificationCubit( repository: RepositoryProvider.of(context))), ], - child: TabPage(), + child: TabPage(appLinkProvider: _appLinkProvider), ), repository: repository, ); @@ -214,7 +227,7 @@ void main() { create: (context) => NotificationCubit( repository: RepositoryProvider.of(context))), ], - child: TabPage(), + child: TabPage(appLinkProvider: _appLinkProvider), ), repository: repository, ); @@ -249,7 +262,7 @@ void main() { create: (context) => NotificationCubit( repository: RepositoryProvider.of(context))), ], - child: TabPage(), + child: TabPage(appLinkProvider: _appLinkProvider), ), repository: repository, ); @@ -338,7 +351,7 @@ void main() { .thenAnswer( (invocation) async => 'something random @Tyler yay @Sam'); - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -400,7 +413,7 @@ void main() { when(() => repository.getVideosFromUid('aaa')) .thenAnswer((_) => Future.value([])); - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -460,7 +473,7 @@ void main() { when(() => repository.getVideosFromUid('aaa')) .thenAnswer((_) => Future.value([])); - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -524,7 +537,7 @@ void main() { when(() => repository.getVideosFromUid('aaa')) .thenAnswer((_) => Future.value([])); - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [ @@ -581,7 +594,7 @@ void main() { when(() => repository.getVideosFromUid('aaa')) .thenAnswer((_) => Future.value([])); - final tabPage = TabPage(); + final tabPage = TabPage(appLinkProvider: _appLinkProvider); await tester.pumpApp( widget: MultiBlocProvider( providers: [