diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c39b04d955156b8ee0254d950cb7082148ceaa1c..cd9619425429b68dae7cb14d19a18aa4c1680e34 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -39,6 +39,12 @@ <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="june" /> + </intent-filter> </activity> <!-- Don't delete the meta-data below. This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> @@ -48,10 +54,10 @@ </application> <queries> - <!-- If your app opens https URLs --> <intent> <action android:name="android.intent.action.VIEW" /> <data android:scheme="https" /> </intent> </queries> + </manifest> diff --git a/lib/main.dart b/lib/main.dart index 4c4d3a4c1a40e85a060a8861fbc5b0e7e8e526f4..40026409fee7954cf875f2ecacb880a0ee8517c9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,6 +21,7 @@ import 'package:responsive_framework/responsive_wrapper.dart'; import 'package:responsive_framework/utils/scroll_behavior.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_logging/sentry_logging.dart'; +import 'package:uni_links/uni_links.dart'; import 'app_bloc_observer.dart'; import 'config/theme.dart'; @@ -37,6 +38,7 @@ import 'data/models/payment_cubit.dart'; import 'data/models/theme_cubit.dart'; import 'data/models/transaction_cubit.dart'; import 'g1/api.dart'; +import 'g1/g1_helper.dart'; import 'shared_prefs.dart'; import 'ui/contacts_cache.dart'; import 'ui/logger.dart'; @@ -44,6 +46,7 @@ import 'ui/notification_controller.dart'; import 'ui/screens/skeleton_screen.dart'; import 'ui/ui_helpers.dart'; import 'ui/widgets/connectivity_widget_wrapper_wrapper.dart'; +import 'ui/widgets/first_screen/pay_contact_search_page.dart'; void main() async { await NotificationController.initializeLocalNotifications(); @@ -79,7 +82,7 @@ void main() async { final Box<dynamic> box = await Hive.openBox('hydrated_box', path: HydratedStorage.webStorageDirectory.path); final List<dynamic> keysToDelete = - box.keys.where((dynamic key) => '$key'.startsWith('minified')).toList(); + box.keys.where((dynamic key) => '$key'.startsWith('minified')).toList(); box.deleteAll(keysToDelete); // This should we done after init // await HydratedBloc.storage.clear(); @@ -93,7 +96,7 @@ void main() async { final Directory tmpDir = await getTemporaryDirectory(); Hive.init(tmpDir.toString()); HydratedBloc.storage = - await HydratedStorage.build(storageDirectory: tmpDir); + await HydratedStorage.build(storageDirectory: tmpDir); } PWAInstall().setup(installCallback: () { @@ -102,7 +105,8 @@ void main() async { Bloc.observer = AppBlocObserver(); - void appRunner() => runApp( + void appRunner() => + runApp( EasyLocalization( path: 'assets/translations', supportedLocales: const <Locale>[ @@ -146,9 +150,7 @@ void main() async { if (kReleaseMode) { // Only use sentry in production - await SentryFlutter.init(( - SentryFlutterOptions options, - ) { + await SentryFlutter.init((SentryFlutterOptions options,) { options.tracesSampleRate = 1.0; options.reportPackages = false; // options.addInAppInclude('sentry_flutter_example'); @@ -195,7 +197,7 @@ class AppIntro extends StatefulWidget { class _AppIntro extends State<AppIntro> { final GlobalKey<IntroductionScreenState> introKey = - GlobalKey<IntroductionScreenState>(); + GlobalKey<IntroductionScreenState>(); void _onIntroEnd(BuildContext context, AppCubit cubit) { cubit.introViewed(); @@ -209,40 +211,43 @@ class _AppIntro extends State<AppIntro> { Widget build(BuildContext context) { return BlocBuilder<AppCubit, AppState>( builder: (BuildContext buildContext, AppState state) { - final AppCubit cubit = context.read<AppCubit>(); - return IntroductionScreen( - key: introKey, - pages: <PageViewModel>[ - for (int i = 1; i <= 5; i++) - createPageViewModel('intro_${i}_title', 'intro_${i}_description', - 'assets/img/undraw_intro_$i.png', context), - ], - onDone: () => _onIntroEnd(buildContext, cubit), - showSkipButton: true, - skipOrBackFlex: 0, - onSkip: () => _onIntroEnd(buildContext, cubit), - nextFlex: 0, - skip: Text(tr('skip')), - next: const Icon(Icons.arrow_forward), - done: Text(tr('start'), - style: const TextStyle(fontWeight: FontWeight.w600)), - dotsDecorator: const DotsDecorator( - size: Size(10.0, 10.0), - color: Color(0xFFBDBDBD), - activeColor: Colors.blueAccent, - activeSize: Size(22.0, 10.0), - activeShape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(25.0)), - ), - ), - ); - }); + final AppCubit cubit = context.read<AppCubit>(); + return IntroductionScreen( + key: introKey, + pages: <PageViewModel>[ + for (int i = 1; i <= 5; i++) + createPageViewModel( + 'intro_${i}_title', 'intro_${i}_description', + 'assets/img/undraw_intro_$i.png', context), + ], + onDone: () => _onIntroEnd(buildContext, cubit), + showSkipButton: true, + skipOrBackFlex: 0, + onSkip: () => _onIntroEnd(buildContext, cubit), + nextFlex: 0, + skip: Text(tr('skip')), + next: const Icon(Icons.arrow_forward), + done: Text(tr('start'), + style: const TextStyle(fontWeight: FontWeight.w600)), + dotsDecorator: const DotsDecorator( + size: Size(10.0, 10.0), + color: Color(0xFFBDBDBD), + activeColor: Colors.blueAccent, + activeSize: Size(22.0, 10.0), + activeShape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(25.0)), + ), + ), + ); + }); } } -PageViewModel createPageViewModel( - String title, String body, String imageAsset, BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; +PageViewModel createPageViewModel(String title, String body, String imageAsset, + BuildContext context) { + final ColorScheme colorScheme = Theme + .of(context) + .colorScheme; final TextStyle titleStyle = TextStyle( color: colorScheme.primary, fontWeight: FontWeight.bold, @@ -270,7 +275,7 @@ class GinkgoApp extends StatefulWidget { // The navigator key is necessary to navigate using static methods static final GlobalKey<NavigatorState> navigatorKey = - GlobalKey<NavigatorState>(); + GlobalKey<NavigatorState>(); @override State<GinkgoApp> createState() => _GinkgoAppState(); @@ -286,10 +291,16 @@ class _GinkgoAppState extends State<GinkgoApp> { } void _printNodeStatus({String prefix = 'Starting'}) { - final int nDuniterNodes = NodeManager().nodeList(NodeType.duniter).length; + final int nDuniterNodes = NodeManager() + .nodeList(NodeType.duniter) + .length; final int nCesiumPlusNodes = - NodeManager().nodeList(NodeType.cesiumPlus).length; - final int nGvaNodes = NodeManager().nodeList(NodeType.gva).length; + NodeManager() + .nodeList(NodeType.cesiumPlus) + .length; + final int nGvaNodes = NodeManager() + .nodeList(NodeType.gva) + .length; logger( '$prefix with $nDuniterNodes duniter nodes, $nCesiumPlusNodes c+ nodes, and $nGvaNodes gva nodes'); if (!kReleaseMode) { @@ -303,12 +314,13 @@ class _GinkgoAppState extends State<GinkgoApp> { @override void initState() { super.initState(); + _initDeepLinkListener(); NodeManager().loadFromCubit(context.read<NodeListCubit>()); // Only after at least the action method is set, the notification events are delivered NotificationController.startListeningNotificationEvents(); Once.runHourly('load_nodes', callback: () async { final bool isConnected = - await ConnectivityWidgetWrapperWrapper.isConnected; + await ConnectivityWidgetWrapperWrapper.isConnected; if (isConnected) { logger('Load nodes via once'); _loadNodes(); @@ -323,7 +335,10 @@ class _GinkgoAppState extends State<GinkgoApp> { Once.runDaily('clear_cache', callback: () { logger('clear cache via once'); ContactsCache().clear(); - ContactsCache().addContacts(context.read<ContactsCubit>().state.contacts); + ContactsCache().addContacts(context + .read<ContactsCubit>() + .state + .contacts); }); Once.runOnce('resize_avatars', callback: () { logger('resize avatar via once'); @@ -334,101 +349,146 @@ class _GinkgoAppState extends State<GinkgoApp> { @override void dispose() { ContactsCache().dispose(); + _disposeDeepLinkListener(); super.dispose(); } + late StreamSubscription<dynamic>? _sub; + + Future<void> _initDeepLinkListener() async { + _sub = linkStream.listen((String? link) async { + if (!mounted) { + return; + } + if (link != null) { + logger('got link: $link'); + if (parseScannedUri(link) != null) { + showDialog( + context: context, + builder: (BuildContext context) { + return + PayContactSearchPage(uri: link); + }, + ); + } + } + }, onError: (Object err) { + if (!mounted) { + return; + } + logger('got err: $err'); + }); + } + + void _disposeDeepLinkListener() { + if (_sub != null) { + _sub!.cancel(); + _sub = null; + } + } + @override Widget build(BuildContext context) { return BlocBuilder<NodeListCubit, NodeListState>( builder: (BuildContext nodeContext, NodeListState state) { - return ConnectivityAppWrapper( - app: FilesystemPickerDefaultOptions( - fileTileSelectMode: FileTileSelectMode.wholeTile, - theme: FilesystemPickerTheme( - topBar: FilesystemPickerTopBarThemeData( - backgroundColor: Theme.of(context).colorScheme.primary, - ), - ), - child: MaterialApp( - /// Localization is not available for the title. - title: 'Äž1nkgo', - theme: ThemeData( - useMaterial3: true, colorScheme: lightColorScheme), - darkTheme: + return ConnectivityAppWrapper( + app: FilesystemPickerDefaultOptions( + fileTileSelectMode: FileTileSelectMode.wholeTile, + theme: FilesystemPickerTheme( + topBar: FilesystemPickerTopBarThemeData( + backgroundColor: Theme + .of(context) + .colorScheme + .primary, + ), + ), + child: MaterialApp( + + /// Localization is not available for the title. + title: 'Äž1nkgo', + theme: ThemeData( + useMaterial3: true, colorScheme: lightColorScheme), + darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme), - navigatorKey: GinkgoApp.navigatorKey, - scaffoldMessengerKey: globalMessengerKey, - - /// Theme stuff - themeMode: context.watch<ThemeCubit>().state.themeMode, - - /// Localization stuff - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - debugShowCheckedModeBanner: false, - home: context.read<AppCubit>().isIntroViewed - ? BetterFeedback( + navigatorKey: GinkgoApp.navigatorKey, + scaffoldMessengerKey: globalMessengerKey, + + /// Theme stuff + themeMode: context + .watch<ThemeCubit>() + .state + .themeMode, + + /// Localization stuff + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + debugShowCheckedModeBanner: false, + home: context + .read<AppCubit>() + .isIntroViewed + ? BetterFeedback( localizationsDelegates: context.localizationDelegates ..add(CustomFeedbackLocalizationsDelegate()), child: const SkeletonScreen()) - : const AppIntro(), - builder: (BuildContext buildContext, Widget? widget) { - NotificationController.locale = context.locale; - return ResponsiveWrapper.builder( - BouncingScrollWrapper.builder( - context, - ConnectivityWidgetWrapperWrapper( - //message: tr('offline'), - //height: 18, - - offlineWidget: /* Container( + : const AppIntro(), + builder: (BuildContext buildContext, Widget? widget) { + NotificationController.locale = context.locale; + return ResponsiveWrapper.builder( + BouncingScrollWrapper.builder( + context, + ConnectivityWidgetWrapperWrapper( + //message: tr('offline'), + //height: 18, + + offlineWidget: /* Container( color: Colors.transparent, child: Center( child: */ Column( - mainAxisSize: MainAxisSize.min, - children: <Widget>[ - const Icon( - Icons.cloud_off, - size: 48, - color: Colors.grey, - ), - const SizedBox(height: 6), - Container( - padding: const EdgeInsets.all(5.0), - decoration: const BoxDecoration( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + const Icon( + Icons.cloud_off, + size: 48, color: Colors.grey, - borderRadius: - BorderRadius.all(Radius.circular(10.0)), ), - child: Text( - tr('offline'), - style: const TextStyle( - color: Colors.white, - decoration: TextDecoration.none, - fontSize: 14, - ), - )), - const SizedBox(height: 110), - ], - ), - - child: widget!, - )), - maxWidth: 480, - minWidth: 480, - // defaultScale: true, - breakpoints: <ResponsiveBreakpoint>[ - const ResponsiveBreakpoint.resize(200, name: MOBILE), - const ResponsiveBreakpoint.resize(480, name: TABLET), - const ResponsiveBreakpoint.resize(1000, name: DESKTOP), - ], - background: Container(color: const Color(0xFFF5F5F5)), - ); - }, - ))); - }); + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.all(5.0), + decoration: const BoxDecoration( + color: Colors.grey, + borderRadius: + BorderRadius.all(Radius.circular(10.0)), + ), + child: Text( + tr('offline'), + style: const TextStyle( + color: Colors.white, + decoration: TextDecoration.none, + fontSize: 14, + ), + )), + const SizedBox(height: 110), + ], + ), + + child: widget!, + )), + maxWidth: 480, + minWidth: 480, + // defaultScale: true, + breakpoints: <ResponsiveBreakpoint>[ + const ResponsiveBreakpoint.resize(200, name: MOBILE), + const ResponsiveBreakpoint.resize(480, name: TABLET), + const ResponsiveBreakpoint.resize( + 1000, name: DESKTOP), + ], + background: Container(color: const Color(0xFFF5F5F5)), + ); + }, + ))); + }); } } diff --git a/pubspec.lock b/pubspec.lock index 941c148acaef3a95c2ee6dbb334d73f1fa75b582..3669e3a1734f08d954e0a2f7ea523e044a0d457c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1459,6 +1459,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + uni_links: + dependency: "direct main" + description: + name: uni_links + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + uni_links_platform_interface: + dependency: transitive + description: + name: uni_links_platform_interface + sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + uni_links_web: + dependency: transitive + description: + name: uni_links_web + sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + url: "https://pub.dev" + source: hosted + version: "0.1.0" universal_html: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5630d3ef211309b65c059c7ae7d5e4a818628a71..9e25e455e2ceac0c14f9e92c1e446336030c827f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -92,6 +92,7 @@ dependencies: crypto: ^3.0.3 flutter_nfc_kit: ^3.3.1 ndef: ^0.3.1 + uni_links: ^0.5.1 dev_dependencies: flutter_test: