Skip to content

Commit

Permalink
feat: Support for deep links (#3995)
Browse files Browse the repository at this point in the history
* Support for deep links on Android

* Deep links: Better support for network errors / unknown product

* iOS configuration for deep links

* Improve documentation

* Improve doc

* Support other domains

* Add an error page

* Upgrade to go_router 7.0.2

* Add some Sentry events

* Update packages/smooth_app/lib/helpers/extension_on_text_helper.dart

Co-authored-by: monsieurtanuki <[email protected]>

* SizedBox -> EMPTY_WIDGET

* PreferencePageType: from name to tag

* Remove an unused async keyword

* Move internal app routes to an enum

* Add a clarification about `ProductLoaderPage`

* Update packages/smooth_app/lib/pages/navigator/app_navigator.dart

Co-authored-by: Pierre Slamich <[email protected]>

* Revert how we open the product addition/creation on the product not found card

---------

Co-authored-by: monsieurtanuki <[email protected]>
Co-authored-by: Pierre Slamich <[email protected]>
  • Loading branch information
3 people authored May 23, 2023
1 parent ec7113d commit 4c44b06
Show file tree
Hide file tree
Showing 25 changed files with 805 additions and 86 deletions.
15 changes: 14 additions & 1 deletion packages/smooth_app/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,27 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Enable deep links in the app -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="*.openfoodfacts.org" />
<data android:scheme="http" android:host="*.openfoodfacts.net" />
<data android:scheme="http" android:host="*.openbeautyfacts.org" />
<data android:scheme="http" android:host="*.openproductsfacts.org" />
<data android:scheme="http" android:host="*.openpetfoodfacts.org" />
<data android:scheme="https" />
</intent-filter>
</activity>

<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2"/>


<!-- Quick tile / setting (only available on Android 24+ / Nougat) -->
<service android:name="org.openfoodfacts.app.AppMainTile"
android:exported="true"
Expand Down
5 changes: 5 additions & 0 deletions packages/smooth_app/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3BA1CE0C2A18114C009FECD1 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
6505AE5B14FDEF303D722C58 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -102,6 +103,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
3BA1CE0C2A18114C009FECD1 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
Expand Down Expand Up @@ -356,6 +358,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ZC9CYWD334;
ENABLE_BITCODE = NO;
Expand Down Expand Up @@ -485,6 +488,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ZC9CYWD334;
ENABLE_BITCODE = NO;
Expand All @@ -508,6 +512,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ZC9CYWD334;
ENABLE_BITCODE = NO;
Expand Down
2 changes: 2 additions & 0 deletions packages/smooth_app/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,7 @@
<false/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>FlutterDeepLinkingEnabled</key>
<true/>
</dict>
</plist>
11 changes: 11 additions & 0 deletions packages/smooth_app/ios/Runner/Runner.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:*.openfoodfacts.org</string>
<string>applinks:*.openfoodfacts.net</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:smooth_app/generic_lib/widgets/smooth_product_image.dart';
import 'package:smooth_app/helpers/product_cards_helper.dart';
import 'package:smooth_app/helpers/product_compatibility_helper.dart';
import 'package:smooth_app/helpers/ui_helpers.dart';
import 'package:smooth_app/pages/product/new_product_page.dart';
import 'package:smooth_app/pages/navigator/app_navigator.dart';

class SmoothProductCardFound extends StatelessWidget {
const SmoothProductCardFound({
Expand Down Expand Up @@ -64,12 +64,10 @@ class SmoothProductCardFound extends StatelessWidget {
child: InkWell(
borderRadius: ROUNDED_BORDER_RADIUS,
onTap: onTap ??
() async {
await Navigator.push<void>(
context,
MaterialPageRoute<void>(
builder: (BuildContext context) => ProductPage(product),
),
() {
AppNavigator.of(context).push(
AppRoutes.PRODUCT(product.barcode!),
extra: product,
);
},
onLongPress: () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ class SmoothProductCardNotFound extends StatelessWidget {
context,
MaterialPageRoute<void>(
builder: (BuildContext context) =>
AddNewProductPage(barcode),
AddNewProductPage(barcode: barcode),
),
);

if (callback != null) {
await callback!();
}
Expand Down
12 changes: 9 additions & 3 deletions packages/smooth_app/lib/helpers/analytics_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ enum AnalyticsCategory {
share(tag: 'share'),
couldNotFindProduct(tag: 'could not find product'),
productEdit(tag: 'product edit'),
list(tag: 'list');
list(tag: 'list'),
deepLink(tag: 'deep link');

const AnalyticsCategory({required this.tag});

final String tag;
}

Expand All @@ -40,11 +42,15 @@ enum AnalyticsEvent {
tag: 'opened product edit page',
category: AnalyticsCategory.productEdit,
),

shareList(tag: 'shared a list', category: AnalyticsCategory.list),
openListWeb(tag: 'open a list in wbe', category: AnalyticsCategory.list);
openListWeb(tag: 'open a list in wbe', category: AnalyticsCategory.list),
productDeepLink(
tag: 'open a product from an URL', category: AnalyticsCategory.deepLink),
genericDeepLink(
tag: 'generic deep link', category: AnalyticsCategory.deepLink);

const AnalyticsEvent({required this.tag, required this.category});

final String tag;
final AnalyticsCategory category;
}
Expand Down
15 changes: 15 additions & 0 deletions packages/smooth_app/lib/helpers/extension_on_text_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,18 @@ extension Selectable on Text {
);
}
}

extension StringExtensions on String {
int count(String character) {
assert(character.length == 1);

int count = 0;
for (int i = 0; i < length; i++) {
if (this[i] == character) {
count++;
}
}

return count;
}
}
29 changes: 29 additions & 0 deletions packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -2040,5 +2040,34 @@
"contrast_low": "Low",
"@contrast_low": {
"description": "Low Contrast Text Color"
},
"product_loader_not_found_title": "Product not found!",
"@product_loader_not_found_title": {
"description": "When fetching a product opened via a link and it doesn't exist"
},
"product_loader_not_found_message": "A product with the following barcode doesn't exist in our database: {barcode}",
"@product_loader_not_found_message": {
"description": "When fetching a product opened via a link, it doesn't exist",
"placeholders": {
"barcode": {
"type": "String"
}
}
},
"product_loader_network_error_title": "No internet connection!",
"@product_loader_network_error_title": {
"description": "When fetching a product opened via a link and there is no connection"
},
"product_loader_network_error_message": "Please check that your smartphone is on a WiFi network or has mobile data enabled",
"@product_loader_network_error_message": {
"description": "When fetching a product opened via a link and there is no connection"
},
"page_not_found_title": "Page not found!",
"@page_not_found_title": {
"description": "Title for a page not found (when an URL is not recognized)"
},
"page_not_found_button": "Go back to the homepage",
"@page_not_found_button": {
"description": "Button to go back to the homepage"
}
}
37 changes: 6 additions & 31 deletions packages/smooth_app/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart';
import 'package:scanner_shared/scanner_shared.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:smooth_app/data_models/continuous_scan_model.dart';
import 'package:smooth_app/data_models/product_preferences.dart';
import 'package:smooth_app/data_models/user_management_provider.dart';
Expand All @@ -28,6 +27,7 @@ import 'package:smooth_app/helpers/entry_points_helper.dart';
import 'package:smooth_app/helpers/global_vars.dart';
import 'package:smooth_app/helpers/network_config.dart';
import 'package:smooth_app/helpers/permission_helper.dart';
import 'package:smooth_app/pages/navigator/app_navigator.dart';
import 'package:smooth_app/pages/onboarding/onboarding_flow_navigator.dart';
import 'package:smooth_app/query/product_query.dart';
import 'package:smooth_app/services/smooth_services.dart';
Expand Down Expand Up @@ -194,7 +194,7 @@ class _SmoothAppState extends State<SmoothApp> {
}
if (snapshot.connectionState != ConnectionState.done) {
//We don't need a loading indicator since the splash screen is still visible
return Container();
return EMPTY_WIDGET;
}

// The `create` constructor of [ChangeNotifierProvider] takes care of
Expand All @@ -220,21 +220,20 @@ class _SmoothAppState extends State<SmoothApp> {
provide<SmoothAppDataImporter>(_appDataImporter),
provide<PermissionListener>(_permissionListener),
],
builder: _buildApp,
child: AppNavigator(child: Builder(builder: _buildApp)),
);
},
);
}

Widget _buildApp(BuildContext context, Widget? child) {
Widget _buildApp(BuildContext context) {
final ThemeProvider themeProvider = context.watch<ThemeProvider>();
final ColorProvider colorProvider = context.watch<ColorProvider>();
final TextContrastProvider textContrastProvider =
context.watch<TextContrastProvider>();
final OnboardingPage lastVisitedOnboardingPage =
_userPreferences.lastVisitedOnboardingPage;
OnboardingFlowNavigator(_userPreferences);
final Widget appWidget = lastVisitedOnboardingPage.getPageWidget(context);
final bool isOnboardingComplete =
lastVisitedOnboardingPage.isOnboardingComplete();
themeProvider.setOnboardingComplete(isOnboardingComplete);
Expand All @@ -245,20 +244,17 @@ class _SmoothAppState extends State<SmoothApp> {
final String? languageCode =
context.select((UserPreferences up) => up.appLanguageCode);

return MaterialApp(
return MaterialApp.router(
locale: languageCode != null ? Locale(languageCode) : null,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
debugShowCheckedModeBanner: !(kReleaseMode || _screenshots),
navigatorObservers: <NavigatorObserver>[
SentryNavigatorObserver(),
],
theme: SmoothTheme.getThemeData(
Brightness.light, themeProvider, colorProvider, textContrastProvider),
darkTheme: SmoothTheme.getThemeData(
Brightness.dark, themeProvider, colorProvider, textContrastProvider),
themeMode: themeProvider.currentThemeMode,
home: SmoothAppGetLanguage(appWidget, _userPreferences),
routerConfig: AppNavigator.of(context).router,
);
}

Expand All @@ -274,24 +270,3 @@ class _SmoothAppState extends State<SmoothApp> {
);
}
}

/// Layer needed because we need to know the language. Language isn't available
/// in the [context] in top level widget ([SmoothApp])
class SmoothAppGetLanguage extends StatelessWidget {
const SmoothAppGetLanguage(this.appWidget, this.userPreferences);

final Widget appWidget;
final UserPreferences userPreferences;

@override
Widget build(BuildContext context) {
// TODO(monsieurtanuki): refactor removing the `SmoothAppGetLanguage` layer?
ProductQuery.setLanguage(context, userPreferences);
context.read<ProductPreferences>().refresh();

// The migration requires the language to be set in the app!
_appDataImporter.startMigrationAsync();

return appWidget;
}
}
6 changes: 6 additions & 0 deletions packages/smooth_app/lib/pages/inherited_data_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ class InheritedDataManager extends StatefulWidget {
.data;
}

static InheritedDataManagerState? find(BuildContext context) {
return context
.findAncestorWidgetOfExactType<_InheritedDataManagerProvider>()
?.data;
}

@override
State<InheritedDataManager> createState() => InheritedDataManagerState();
}
Expand Down
Loading

0 comments on commit 4c44b06

Please sign in to comment.