diff --git a/.github/sync.yml b/.github/sync.yml new file mode 100644 index 0000000..cd8991d --- /dev/null +++ b/.github/sync.yml @@ -0,0 +1,30 @@ +humhub/app@master: + - source: assets/ + deleteOrphaned: true + exclude: | + .env + - source: lib/ + deleteOrphaned: true + - source: test/ + deleteOrphaned: true + - .gitignore + - .metadata + - analysis_options.yaml + - l10n.yaml + - pubspec.lock + - pubspec.yaml +humhub/app-flavored@master: + - source: assets/ + deleteOrphaned: true + exclude: | + .env + - source: lib/ + deleteOrphaned: true + - source: test/ + deleteOrphaned: true + - .gitignore + - .metadata + - analysis_options.yaml + - l10n.yaml + - pubspec.lock + - pubspec.yaml \ No newline at end of file diff --git a/.github/workflows/sync-to-flavored-repos.yml b/.github/workflows/sync-to-flavored-repos.yml new file mode 100644 index 0000000..2047c2c --- /dev/null +++ b/.github/workflows/sync-to-flavored-repos.yml @@ -0,0 +1,17 @@ +name: Sync HumHub app with app-flavored + +on: + push: + branches: + - 'master' + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + - name: Run GitHub File Sync + uses: BetaHuhn/repo-file-sync-action@v1 + with: + GH_PAT: ${{ secrets.GH_PAT }} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 09fba76..69ae072 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ + Bool { + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate + } GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/lib/app_flavored.dart b/lib/app_flavored.dart new file mode 100644 index 0000000..759f132 --- /dev/null +++ b/lib/app_flavored.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:humhub/flavored/models/humhub.f.dart'; +import 'package:humhub/flavored/util/intent_plugin.f.dart'; +import 'package:humhub/flavored/util/router.f.dart'; +import 'package:humhub/util/loading_provider.dart'; +import 'package:humhub/util/notifications/plugin.dart'; +import 'package:humhub/util/override_locale.dart'; +import 'package:humhub/util/push/push_plugin.dart'; +import 'package:humhub/util/storage_service.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class FlavoredApp extends ConsumerStatefulWidget { + const FlavoredApp({super.key}); + + @override + FlavoredAppState createState() => FlavoredAppState(); +} + +class FlavoredAppState extends ConsumerState { + @override + Widget build(BuildContext context) { + SecureStorageService.clearSecureStorageOnReinstall(); + return IntentPluginF( + child: NotificationPlugin( + child: PushPlugin( + child: OverrideLocale( + builder: (overrideLocale) => Builder( + builder: (context) => MaterialApp( + debugShowCheckedModeBanner: false, + initialRoute: RouterF.initRoute, + routes: RouterF.routes, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + navigatorKey: navigatorKeyF, + builder: (context, child) => LoadingProvider( + child: child!, + ), + theme: ThemeData( + fontFamily: 'OpenSans', + ), + ), + ), + ), + ), + ), + ); + } +} + +final humHubFProvider = FutureProvider((ref) { + return HumHubF.initialize(); +}); diff --git a/lib/app_opener.dart b/lib/app_opener.dart new file mode 100644 index 0000000..71e7462 --- /dev/null +++ b/lib/app_opener.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:humhub/util/intent/intent_plugin.dart'; +import 'package:humhub/util/loading_provider.dart'; +import 'package:humhub/util/notifications/plugin.dart'; +import 'package:humhub/util/override_locale.dart'; +import 'package:humhub/util/push/push_plugin.dart'; +import 'package:humhub/util/router.dart'; +import 'package:humhub/util/storage_service.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class OpenerApp extends ConsumerStatefulWidget { + const OpenerApp({super.key}); + + @override + OpenerAppState createState() => OpenerAppState(); +} + +class OpenerAppState extends ConsumerState { + @override + Widget build(BuildContext context) { + SecureStorageService.clearSecureStorageOnReinstall(); + return IntentPlugin( + child: NotificationPlugin( + child: PushPlugin( + child: OverrideLocale( + builder: (overrideLocale) => Builder( + builder: (context) => FutureBuilder( + future: MyRouter.getInitialRoute(ref), + builder: (context, snap) { + if (snap.hasData) { + return MaterialApp( + debugShowCheckedModeBanner: false, + initialRoute: snap.data, + routes: MyRouter.routes, + navigatorKey: navigatorKey, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: overrideLocale, + builder: (context, child) => LoadingProvider( + child: child!, + ), + theme: ThemeData( + fontFamily: 'OpenSans', + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/components/animated_padding_component.dart b/lib/components/animated_padding_component.dart index e055ee9..44d4e6d 100644 --- a/lib/components/animated_padding_component.dart +++ b/lib/components/animated_padding_component.dart @@ -11,13 +11,8 @@ class AnimatedPaddingComponent extends StatefulWidget { } class AnimatedPaddingComponentState extends State { - @override Widget build(BuildContext context) { - return AnimatedPadding( - duration: const Duration(milliseconds: 500), - padding: widget.padding, - child: widget.child - ); + return AnimatedPadding(duration: const Duration(milliseconds: 500), padding: widget.padding, child: widget.child); } } diff --git a/lib/components/bottom_navigation_bar.dart b/lib/components/bottom_navigation_bar.dart index f489a3d..925dea2 100644 --- a/lib/components/bottom_navigation_bar.dart +++ b/lib/components/bottom_navigation_bar.dart @@ -84,7 +84,9 @@ class BottomNavigationState extends State with TickerProviderS child: TextButton( onPressed: () => navigateForth(), child: Text( - selectedIndex != widget.pageCount - 1 ? AppLocalizations.of(context)!.next : AppLocalizations.of(context)!.connect_now, + selectedIndex != widget.pageCount - 1 + ? AppLocalizations.of(context)!.next + : AppLocalizations.of(context)!.connect_now, style: const TextStyle(color: Colors.grey), ), ), diff --git a/lib/components/language_switcher.dart b/lib/components/language_switcher.dart index c07ffe2..8eaf647 100644 --- a/lib/components/language_switcher.dart +++ b/lib/components/language_switcher.dart @@ -35,7 +35,7 @@ class _LanguageSwitcherState extends State { const SizedBox(width: 20), Text( locale.toUpperCase(), - style: TextStyle(color: primaryColor, fontSize: 16), + style: TextStyle(color: HumhubTheme.primaryColor, fontSize: 16), ), ], ), @@ -56,7 +56,7 @@ class _LanguageSwitcherState extends State { focusedBorder: InputBorder.none, ), value: _value(context), - icon: Icon(Icons.arrow_drop_down, color: primaryColor), + icon: Icon(Icons.arrow_drop_down, color: HumhubTheme.primaryColor), items: _items .mapIndexed( (localeString, index) => DropdownMenuItem( @@ -67,7 +67,7 @@ class _LanguageSwitcherState extends State { const SizedBox(width: 20), Text( localeString.toUpperCase(), - style: TextStyle(color: primaryColor, fontSize: 16), + style: TextStyle(color: HumhubTheme.primaryColor, fontSize: 16), ), ], ), diff --git a/lib/components/rotating_globe.dart b/lib/components/rotating_globe.dart index bdc5eed..da77b25 100644 --- a/lib/components/rotating_globe.dart +++ b/lib/components/rotating_globe.dart @@ -29,7 +29,7 @@ class _RotatingGlobeState extends State with TickerProviderStateM @override Widget build(BuildContext context) { - if(_controller.isCompleted){ + if (_controller.isCompleted) { _animation = widget.rotationDirection == Direction.left ? _animationSec : _animationFir; _controller.reset(); } diff --git a/lib/flavored/models/humhub.f.dart b/lib/flavored/models/humhub.f.dart new file mode 100644 index 0000000..1250a32 --- /dev/null +++ b/lib/flavored/models/humhub.f.dart @@ -0,0 +1,37 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:humhub/flavored/models/manifest.f.dart'; +import 'package:humhub/models/hum_hub.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class HumHubF extends HumHub { + @override + ManifestF get manifest => ManifestF.fromEnv(); + @override + String get manifestUrl => dotenv.env['MANIFEST_URL']!; + final String bundleId; + + HumHubF({ + bool isHideOpener = false, + String? randomHash, + String? appVersion, + String? pushToken, + required this.bundleId, + }) : super( + isHideOpener: isHideOpener, + randomHash: HumHub.generateHash(32), + appVersion: appVersion, + pushToken: pushToken); + + @override + Map get customHeaders => { + 'x-humhub-app-token': randomHash!, + 'x-humhub-app': appVersion ?? '1.0.0', + 'x-humhub-app-bundle-id': bundleId, + 'x-humhub-app-ostate': isHideOpener ? '1' : '0' + }; + + static Future initialize() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + return HumHubF(bundleId: packageInfo.packageName); + } +} diff --git a/lib/flavored/models/manifest.f.dart b/lib/flavored/models/manifest.f.dart new file mode 100644 index 0000000..677dbf5 --- /dev/null +++ b/lib/flavored/models/manifest.f.dart @@ -0,0 +1,30 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:humhub/models/manifest.dart'; + +class ManifestF extends Manifest { + ManifestF( + {required String display, + required String startUrl, + required String shortName, + required String name, + required String backgroundColor, + required String themeColor}) + : super( + display: display, + startUrl: startUrl, + shortName: shortName, + name: name, + backgroundColor: backgroundColor, + themeColor: themeColor); + + factory ManifestF.fromEnv() { + return ManifestF( + display: dotenv.env['DISPLAY']!, + startUrl: dotenv.env['START_URL']!, + shortName: dotenv.env['SHORT_NAME']!, + name: dotenv.env['NAME']!, + backgroundColor: dotenv.env['BACKGROUND_COLOR']!, + themeColor: dotenv.env['THEME_COLOR']!, + ); + } +} diff --git a/lib/flavored/util/intent_plugin.f.dart b/lib/flavored/util/intent_plugin.f.dart new file mode 100644 index 0000000..240b2a9 --- /dev/null +++ b/lib/flavored/util/intent_plugin.f.dart @@ -0,0 +1,137 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:humhub/flavored/util/router.f.dart'; +import 'package:humhub/flavored/web_view.f.dart'; +import 'package:humhub/util/loading_provider.dart'; +import 'package:loggy/loggy.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:uni_links/uni_links.dart'; + +bool _initialUriIsHandled = false; + +class IntentPluginF extends ConsumerStatefulWidget { + final Widget child; + + const IntentPluginF({ + Key? key, + required this.child, + }) : super(key: key); + + @override + IntentPluginFState createState() => IntentPluginFState(); +} + +class IntentPluginFState extends ConsumerState { + StreamSubscription? intentDataStreamSubscription; + List? sharedFiles; + Object? _err; + Uri? _initialUri; + Uri? _latestUri; + StreamSubscription? _sub; + + @override + void initState() { + logInfo([_err, _initialUri, _latestUri, _sub]); + super.initState(); + intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List value) { + setState(() { + sharedFiles = value; + }); + }); + + // For sharing images coming from outside the app while the app is closed + ReceiveSharingIntent.getInitialMedia().then((List value) { + setState(() { + sharedFiles = value; + }); + }); + _handleInitialUri(); + _handleIncomingLinks(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } + + /// Handle incoming links - the ones that the app will recieve from the OS + /// while already started. + Future _handleIncomingLinks() async { + if (!kIsWeb) { + // It will handle app links while the app is already started - be it in + // the foreground or in the background. + _sub = uriLinkStream.listen((Uri? uri) async { + if (!mounted) return; + _latestUri = uri; + String? redirectUrl = uri?.toString(); + if (redirectUrl != null && navigatorKeyF.currentState != null) { + tryNavigateWithOpener(redirectUrl); + } + _err = null; + }, onError: (err) { + if (kDebugMode) { + print(err); + } + }); + } + } + + /// Handle the initial Uri - the one the app was started with + /// + /// **ATTENTION**: `getInitialLink`/`getInitialUri` should be handled + /// ONLY ONCE in your app's lifetime, since it is not meant to change + /// throughout your app's life. + /// + /// We handle all exceptions, since it is called from initState. + Future _handleInitialUri() async { + // In this example app this is an almost useless guard, but it is here to + // show we are not going to call getInitialUri multiple times, even if this + // was a widget that will be disposed of (ex. a navigation route change). + + if (!_initialUriIsHandled) { + _initialUriIsHandled = true; + try { + final uri = await getInitialUri(); + if (uri == null || !mounted) return; + setState(() => _initialUri = uri); + if (!mounted) { + return; + } + _latestUri = uri; + String? redirectUrl = uri.queryParameters['url']; + if (redirectUrl != null && navigatorKeyF.currentState != null) { + tryNavigateWithOpener(redirectUrl); + } else { + if (redirectUrl != null) { + navigatorKeyF.currentState!.pushNamed(WebViewF.path, arguments: redirectUrl); + return; + } + } + } on PlatformException { + // Platform messages may fail but we ignore the exception + logError('Failed to get initial uri'); + } on FormatException catch (err) { + if (!mounted) return; + logError('Malformed initial uri'); + setState(() => _err = err); + } + } + } + + Future tryNavigateWithOpener(String redirectUrl) async { + LoadingProvider.of(ref).showLoading(); + bool isNewRouteSameAsCurrent = false; + navigatorKeyF.currentState!.popUntil((route) { + if (route.settings.name == WebViewF.path) { + isNewRouteSameAsCurrent = true; + } + return true; + }); + navigatorKeyF.currentState!.pushNamed(WebViewF.path, arguments: redirectUrl); + return isNewRouteSameAsCurrent; + } +} diff --git a/lib/flavored/util/notifications/channel.dart b/lib/flavored/util/notifications/channel.dart new file mode 100644 index 0000000..196d769 --- /dev/null +++ b/lib/flavored/util/notifications/channel.dart @@ -0,0 +1,36 @@ +import 'package:humhub/flavored/util/router.f.dart'; +import 'package:humhub/flavored/web_view.f.dart'; +import 'package:humhub/util/notifications/channel.dart'; +import 'package:humhub/util/notifications/init_from_push.dart'; + +class NotificationChannelF extends NotificationChannel { + const NotificationChannelF( + {super.id = 'redirect', + super.name = 'Redirect flavored app notifications', + super.description = 'These notifications redirect the user to specific url in a payload.'}); + + /// If the WebView is not opened yet or the app is not running the onTap will wake up the app or redirect to the WebView. + /// If app is already running in WebView mode then the state of [WebViewApp] will be updated with new url. + /// + @override + Future onTap(String? payload) async { + if (payload != null && navigatorKeyF.currentState != null) { + bool isNewRouteSameAsCurrent = false; + navigatorKeyF.currentState!.popUntil((route) { + if (route.settings.name == WebViewF.path) { + isNewRouteSameAsCurrent = true; + } + return true; + }); + if (isNewRouteSameAsCurrent) { + navigatorKeyF.currentState!.pushNamed(WebViewF.path, arguments: payload); + return; + } + navigatorKeyF.currentState!.pushNamed(WebViewF.path, arguments: payload); + } else { + if (payload != null) { + InitFromPush.setPayload(payload); + } + } + } +} diff --git a/lib/flavored/util/router.f.dart b/lib/flavored/util/router.f.dart new file mode 100644 index 0000000..ca1eeb1 --- /dev/null +++ b/lib/flavored/util/router.f.dart @@ -0,0 +1,15 @@ +import 'package:flutter/cupertino.dart'; +import 'package:humhub/flavored/web_view.f.dart'; + +final GlobalKey navigatorKeyF = GlobalKey(); + +NavigatorState? get navigator => navigatorKeyF.currentState; + +class RouterF { + static String? initRoute = WebViewF.path; + static dynamic initParams; + + static var routes = { + WebViewF.path: (context) => const WebViewF(), + }; +} diff --git a/lib/flavored/web_view.f.dart b/lib/flavored/web_view.f.dart new file mode 100644 index 0000000..38a4cdb --- /dev/null +++ b/lib/flavored/web_view.f.dart @@ -0,0 +1,287 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_app_badger/flutter_app_badger.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:humhub/app_flavored.dart'; +import 'package:humhub/flavored/models/humhub.f.dart'; +import 'package:humhub/util/auth_in_app_browser.dart'; +import 'package:humhub/models/channel_message.dart'; +import 'package:humhub/util/extensions.dart'; +import 'package:humhub/util/loading_provider.dart'; +import 'package:humhub/util/notifications/init_from_push.dart'; +import 'package:humhub/util/notifications/plugin.dart'; +import 'package:humhub/util/push/provider.dart'; +import 'package:humhub/util/show_dialog.dart'; +import 'package:humhub/util/web_view_global_controller.dart'; +import 'package:loggy/loggy.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class WebViewF extends ConsumerStatefulWidget { + static const String path = '/web_view_f'; + const WebViewF({super.key}); + + @override + FlavoredWebViewState createState() => FlavoredWebViewState(); +} + +class FlavoredWebViewState extends ConsumerState { + late AuthInAppBrowser _authBrowser; + HeadlessInAppWebView? headlessWebView; + late HumHubF instance; + + late PullToRefreshController pullToRefreshController; + + @override + void initState() { + instance = ref.read(humHubFProvider).value!; + _authBrowser = AuthInAppBrowser( + manifest: ref.read(humHubFProvider).value!.manifest, + concludeAuth: (URLRequest request) { + _concludeAuth(request); + }, + ); + super.initState(); + + pullToRefreshController = PullToRefreshController( + options: PullToRefreshOptions( + color: HexColor(instance.manifest.themeColor), + ), + onRefresh: () async { + if (Platform.isAndroid) { + WebViewGlobalController.value?.reload(); + } else if (Platform.isIOS) { + WebViewGlobalController.value + ?.loadUrl(urlRequest: URLRequest(url: await WebViewGlobalController.value?.getUrl())); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + // ignore: deprecated_member_use + return WillPopScope( + onWillPop: () => exitApp(context, ref), + child: Scaffold( + backgroundColor: HexColor(instance.manifest.themeColor), + body: SafeArea( + bottom: false, + child: InAppWebView( + initialUrlRequest: _initialRequest, + initialOptions: _options, + pullToRefreshController: pullToRefreshController, + shouldOverrideUrlLoading: _shouldOverrideUrlLoading, + shouldInterceptFetchRequest: _shouldInterceptFetchRequest, + onWebViewCreated: _onWebViewCreated, + onCreateWindow: _onCreateWindow, + onLoadStop: _onLoadStop, + onLoadStart: _onLoadStart, + onLoadError: _onLoadError, + onProgressChanged: _onProgressChanged, + ), + ), + ), + ); + } + + URLRequest get _initialRequest { + var payload = ModalRoute.of(context)!.settings.arguments; + String? url = instance.manifest.startUrl; + String? payloadForInitFromPush = InitFromPush.usePayload(); + String? payloadFromPush; + if (payload is String) payloadFromPush = payload; + if (payloadForInitFromPush != null) url = payloadForInitFromPush; + if (payloadFromPush != null) url = payloadFromPush; + return URLRequest(url: Uri.parse(url), headers: instance.customHeaders); + } + + InAppWebViewGroupOptions get _options => InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + useShouldOverrideUrlLoading: true, + useShouldInterceptFetchRequest: true, + javaScriptEnabled: true, + supportZoom: false, + javaScriptCanOpenWindowsAutomatically: true, + ), + android: AndroidInAppWebViewOptions( + supportMultipleWindows: true, + useHybridComposition: true, + ), + ios: IOSInAppWebViewOptions( + allowsInlineMediaPlayback: true, + ), + ); + + Future _shouldOverrideUrlLoading( + InAppWebViewController controller, NavigationAction action) async { + // 1st check if url is not def. app url and open it in a browser or inApp. + _setAjaxHeadersJQuery(controller); + final url = action.request.url!.origin; + if (!url.startsWith(instance.manifest.baseUrl) && action.isForMainFrame) { + _authBrowser.launchUrl(action.request); + return NavigationActionPolicy.CANCEL; + } + // 2nd Append customHeader if url is in app redirect and CANCEL the requests without custom headers + if (Platform.isAndroid || + action.iosWKNavigationType == IOSWKNavigationType.LINK_ACTIVATED || + action.iosWKNavigationType == IOSWKNavigationType.FORM_SUBMITTED) { + Map mergedMap = {...instance.customHeaders, ...?action.request.headers}; + URLRequest newRequest = action.request.copyWith(headers: mergedMap); + controller.loadUrl(urlRequest: newRequest); + return NavigationActionPolicy.CANCEL; + } + return NavigationActionPolicy.ALLOW; + } + + Future _onWebViewCreated(InAppWebViewController controller) async { + LoadingProvider.of(ref).showLoading(); + headlessWebView = HeadlessInAppWebView(); + headlessWebView!.run(); + await controller.addWebMessageListener( + WebMessageListener( + jsObjectName: "flutterChannel", + onPostMessage: (inMessage, sourceOrigin, isMainFrame, replyProxy) async { + ChannelMessage message = ChannelMessage.fromJson(inMessage!); + await _handleJSMessage(message, headlessWebView!); + }, + ), + ); + WebViewGlobalController.setValue(controller); + } + + Future _shouldInterceptFetchRequest(InAppWebViewController controller, FetchRequest request) async { + request.headers!.addAll(_initialRequest.headers!); + return request; + } + + Future _onCreateWindow(inAppWebViewController, createWindowAction) async { + logDebug("onCreateWindow"); + final urlToOpen = createWindowAction.request.url; + if (urlToOpen == null) return Future.value(false); + if (await canLaunchUrl(urlToOpen)) { + await launchUrl(urlToOpen, mode: LaunchMode.externalApplication); + } else { + logError('Could not launch $urlToOpen'); + } + return Future.value(true); // Allow creating a new window. + } + + Future _onLoadStop(InAppWebViewController controller, Uri? url) async { + // Disable remember me checkbox on login and set def. value to true: check if the page is actually login page, if it is inject JS that hides element + if (url!.path.contains('/user/auth/login')) { + WebViewGlobalController.value! + .evaluateJavascript(source: "document.querySelector('#login-rememberme').checked=true"); + WebViewGlobalController.value!.evaluateJavascript( + source: + "document.querySelector('#account-login-form > div.form-group.field-login-rememberme').style.display='none';"); + } + _setAjaxHeadersJQuery(controller); + pullToRefreshController.endRefreshing(); + LoadingProvider.of(ref).dismissAll(); + setState(() {}); + } + + void _onLoadStart(InAppWebViewController controller, Uri? url) async { + _setAjaxHeadersJQuery(controller); + } + + void _onLoadError(InAppWebViewController controller, Uri? url, int code, String message) async { + if (code == -1009) ShowDialog.of(context).noInternetPopup(); + pullToRefreshController.endRefreshing(); + setState(() {}); + } + + void _onProgressChanged(controller, progress) async { + if (progress == 100) { + pullToRefreshController.endRefreshing(); + setState(() {}); + } + } + + void _concludeAuth(URLRequest request) { + _authBrowser.close(); + WebViewGlobalController.value!.loadUrl(urlRequest: request); + } + + Future _setAjaxHeadersJQuery(InAppWebViewController controller) async { + String jsCode = "\$.ajaxSetup({headers: ${jsonEncode(instance.customHeaders).toString()}});"; + dynamic jsResponse = await controller.evaluateJavascript(source: jsCode); + logInfo(jsResponse != null ? jsResponse.toString() : "Script returned null value"); + } + + Future _handleJSMessage(ChannelMessage message, HeadlessInAppWebView headlessWebView) async { + switch (message.action) { + case ChannelAction.registerFcmDevice: + String? token = ref.read(pushTokenProvider).value; + if (token != null) { + var postData = Uint8List.fromList(utf8.encode("token=$token")); + await headlessWebView.webViewController.postUrl(url: Uri.parse(message.url!), postData: postData); + } + var status = await Permission.notification.status; + // status.isDenied: The user has previously denied the notification permission + // !status.isGranted: The user has never been asked for the notification permission + bool wasAskedBefore = await NotificationPlugin.hasAskedPermissionBefore(); + // ignore: use_build_context_synchronously + if (status != PermissionStatus.granted && !wasAskedBefore) ShowDialog.of(context).notificationPermission(); + break; + case ChannelAction.updateNotificationCount: + if (message.count != null) FlutterAppBadger.updateBadgeCount(message.count!); + break; + case ChannelAction.unregisterFcmDevice: + String? token = ref.read(pushTokenProvider).value; + if (token != null) { + var postData = Uint8List.fromList(utf8.encode("token=$token")); + URLRequest request = URLRequest(url: Uri.parse(message.url!), method: "POST", body: postData); + // Works but for admin to see the changes it need to reload a page because a request is called on separate instance. + await headlessWebView.webViewController.loadUrl(urlRequest: request); + } + break; + default: + break; + } + } + + Future exitApp(context, ref) async { + bool canGoBack = await WebViewGlobalController.value!.canGoBack(); + if (canGoBack) { + WebViewGlobalController.value!.goBack(); + return Future.value(false); + } else { + final exitConfirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0))), + title: Text(AppLocalizations.of(context)!.web_view_exit_popup_title), + content: Text(AppLocalizations.of(context)!.web_view_exit_popup_content), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(AppLocalizations.of(context)!.no), + ), + TextButton( + onPressed: () { + SystemNavigator.pop(); + }, + child: Text(AppLocalizations.of(context)!.yes), + ), + ], + ), + ); + return exitConfirmed ?? false; + } + } + + @override + void dispose() { + super.dispose(); + if (headlessWebView != null) { + headlessWebView!.dispose(); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index b6557da..2ad0a1c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,83 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:humhub/util/intent/intent_plugin.dart'; -import 'package:humhub/util/loading_provider.dart'; +import 'package:humhub/models/hum_hub.dart'; import 'package:humhub/util/log.dart'; -import 'package:humhub/util/notifications/plugin.dart'; -import 'package:humhub/util/override_locale.dart'; -import 'package:humhub/util/push/push_plugin.dart'; -import 'package:humhub/util/router.dart'; +import 'package:humhub/util/storage_service.dart'; import 'package:loggy/loggy.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:package_info_plus/package_info_plus.dart'; main() async { Loggy.initLoggy( logPrinter: const GlobalLog(), ); WidgetsFlutterBinding.ensureInitialized(); - await clearSecureStorageOnReinstall(); - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_) { - runApp(const ProviderScope(child: MyApp())); + await SecureStorageService.clearSecureStorageOnReinstall(); + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final app = await HumHub.app(packageInfo.packageName); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_) async { + runApp(ProviderScope(child: app)); }); } - -class MyApp extends ConsumerStatefulWidget { - const MyApp({super.key}); - - @override - MyAppState createState() => MyAppState(); -} - -class MyAppState extends ConsumerState { - @override - Widget build(BuildContext context) { - clearSecureStorageOnReinstall(); - return IntentPlugin( - child: NotificationPlugin( - child: PushPlugin( - child: OverrideLocale( - builder: (overrideLocale) => Builder( - builder: (context) => FutureBuilder( - future: MyRouter.getInitialRoute(ref), - builder: (context, snap) { - if (snap.hasData) { - return MaterialApp( - debugShowCheckedModeBanner: false, - initialRoute: snap.data, - routes: MyRouter.routes, - navigatorKey: navigatorKey, - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - locale: overrideLocale, - builder: (context, child) => LoadingProvider( - child: child!, - ), - theme: ThemeData( - fontFamily: 'OpenSans', - ), - ); - } - return const SizedBox.shrink(); - }, - ), - ), - ), - ), - ), - ); - } -} - -clearSecureStorageOnReinstall() async { - String key = 'hasRunBefore'; - SharedPreferences prefs = await SharedPreferences.getInstance(); - bool hasRunBefore = prefs.getBool(key) ?? false; - if (!hasRunBefore) { - FlutterSecureStorage storage = const FlutterSecureStorage(); - await storage.deleteAll(); - prefs.setBool(key, true); - } -} diff --git a/lib/models/channel_message.g.dart b/lib/models/channel_message.g.dart index 9c5290d..dff8270 100644 --- a/lib/models/channel_message.g.dart +++ b/lib/models/channel_message.g.dart @@ -1,30 +1,24 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'channel_message.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ChannelMessage _$ChannelMessageFromJson(String json) { - if (json == "humhub.mobile.hideOpener") { - return ChannelMessage( - "hideOpener", - null, - null, - ); - } else if (json == "humhub.mobile.showOpener") { - return ChannelMessage( - "showOpener", - null, - null, - ); - } else { - var jsonMap = jsonDecode(json) as Map; - return ChannelMessage( - jsonMap['type'] as String, - jsonMap['url'] as String?, - jsonMap['count'] as int?, - ); - } -} +part of 'channel_message.dart'; + +ChannelMessage _$ChannelMessageFromJson(String json) { + if (json == "humhub.mobile.hideOpener") { + return ChannelMessage( + "hideOpener", + null, + null, + ); + } else if (json == "humhub.mobile.showOpener") { + return ChannelMessage( + "showOpener", + null, + null, + ); + } else { + var jsonMap = jsonDecode(json) as Map; + return ChannelMessage( + jsonMap['type'] as String, + jsonMap['url'] as String?, + jsonMap['count'] as int?, + ); + } +} diff --git a/lib/models/event.dart b/lib/models/event.dart index 2f1935d..1b425f0 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -7,20 +7,20 @@ class PushEvent extends RemoteMessage { PushEvent(RemoteMessage message) : parsedData = PushEventData.fromJson(message.data), super( - senderId: message.senderId, - category: message.category, - collapseKey: message.collapseKey, - contentAvailable: message.contentAvailable, - data: message.data, - from: message.from, - messageId: message.messageId, - messageType: message.messageType, - mutableContent: message.mutableContent, - notification: message.notification, - sentTime: message.sentTime, - threadId: message.threadId, - ttl: message.ttl, - ); + senderId: message.senderId, + category: message.category, + collapseKey: message.collapseKey, + contentAvailable: message.contentAvailable, + data: message.data, + from: message.from, + messageId: message.messageId, + messageType: message.messageType, + mutableContent: message.mutableContent, + notification: message.notification, + sentTime: message.sentTime, + threadId: message.threadId, + ttl: message.ttl, + ); } class PushEventData { @@ -32,13 +32,13 @@ class PushEventData { final String? notificationCount; PushEventData( - this.notificationTitle, - this.notificationBody, - this.channel, - this.channelPayload, - this.redirectUrl, - this.notificationCount, - ); + this.notificationTitle, + this.notificationBody, + this.channel, + this.channelPayload, + this.redirectUrl, + this.notificationCount, + ); factory PushEventData.fromJson(Map json) => _$PushEventDataFromJson(json); } diff --git a/lib/models/event.g.dart b/lib/models/event.g.dart index 374d64b..becfc11 100644 --- a/lib/models/event.g.dart +++ b/lib/models/event.g.dart @@ -1,14 +1,13 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - part of 'event.dart'; -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - PushEventData _$PushEventDataFromJson(Map json) { - return PushEventData(json['notification_title'] as String?, json['notification_body'] as String?, json['channel'] as String?, - json['channel_payload'] as String?, json['url'] as String?, json['notification_count'] as String?); + return PushEventData( + json['notification_title'] as String?, + json['notification_body'] as String?, + json['channel'] as String?, + json['channel_payload'] as String?, + json['url'] as String?, + json['notification_count'] as String?); } SimpleNotification _$SimpleNotificationFromJson(Map json) { diff --git a/lib/models/hum_hub.dart b/lib/models/hum_hub.dart index 19873a0..cafb177 100644 --- a/lib/models/hum_hub.dart +++ b/lib/models/hum_hub.dart @@ -1,6 +1,10 @@ import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:humhub/app_flavored.dart'; +import 'package:humhub/app_opener.dart'; import 'package:humhub/models/manifest.dart'; -import 'package:humhub/util/universal_opener_controller.dart'; +import 'package:humhub/util/openers/universal_opener_controller.dart'; enum RedirectAction { opener, webView } @@ -63,4 +67,14 @@ class HumHub { 'x-humhub-app': appVersion ?? '1.0.0', 'x-humhub-app-ostate': isHideOpener ? '1' : '0' }; + + static Future app(String bundleId) async { + switch (bundleId) { + case 'com.humhub.app': + return const OpenerApp(); + default: + await dotenv.load(fileName: "assets/.env"); + return const FlavoredApp(); + } + } } diff --git a/lib/models/manifest.dart b/lib/models/manifest.dart index f71c3d0..2ce792f 100644 --- a/lib/models/manifest.dart +++ b/lib/models/manifest.dart @@ -8,7 +8,7 @@ class Manifest { final String backgroundColor; final String themeColor; - Manifest(this.display, this.startUrl, this.shortName, this.name, this.backgroundColor, this.themeColor); + Manifest({required this.display, required this.startUrl, required this.shortName, required this.name, required this.backgroundColor, required this.themeColor}); String get baseUrl { Uri url = Uri.parse(startUrl); @@ -17,12 +17,12 @@ class Manifest { factory Manifest.fromJson(Map json) { return Manifest( - json['display'] as String, - json['start_url'] as String, - json['short_name'] as String, - json['name'] as String, - json['background_color'] as String, - json['theme_color'] as String, + display: json['display'] as String, + startUrl: json['start_url'] as String, + shortName: json['short_name'] as String, + name: json['name'] as String, + backgroundColor: json['background_color'] as String, + themeColor: json['theme_color'] as String, ); } diff --git a/lib/pages/help/components/first_page.dart b/lib/pages/help/components/first_page.dart index cb3177a..13eb670 100644 --- a/lib/pages/help/components/first_page.dart +++ b/lib/pages/help/components/first_page.dart @@ -3,10 +3,12 @@ import 'package:humhub/components/rotating_globe.dart'; import 'package:humhub/util/const.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - class FirstPage extends StatelessWidget { final bool fadeIn; - const FirstPage({Key? key, required this.fadeIn,}) : super(key: key); + const FirstPage({ + Key? key, + required this.fadeIn, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -22,7 +24,7 @@ class FirstPage extends StatelessWidget { alignment: Alignment.centerLeft, child: Text( AppLocalizations.of(context)!.help_title, - style: getHeaderStyle(context), + style: HumhubTheme.getHeaderStyle(context), ), ), ), @@ -30,12 +32,12 @@ class FirstPage extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10), child: Text( AppLocalizations.of(context)!.help_first_par, - style: paragraphStyle, + style: HumhubTheme.paragraphStyle, ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: Text(AppLocalizations.of(context)!.help_second_par, style: paragraphStyle), + child: Text(AppLocalizations.of(context)!.help_second_par, style: HumhubTheme.paragraphStyle), ), Center( child: RotatingGlobe( diff --git a/lib/pages/help/components/second_page.dart b/lib/pages/help/components/second_page.dart index 94b5a9e..2d9deb4 100644 --- a/lib/pages/help/components/second_page.dart +++ b/lib/pages/help/components/second_page.dart @@ -21,13 +21,13 @@ class SecondPage extends StatelessWidget { alignment: Alignment.centerLeft, child: Text( AppLocalizations.of(context)!.how_to_connect_title, - style: getHeaderStyle(context), + style: HumhubTheme.getHeaderStyle(context), ), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: Text(AppLocalizations.of(context)!.how_to_connect_first_par, style: paragraphStyle), + child: Text(AppLocalizations.of(context)!.how_to_connect_first_par, style: HumhubTheme.paragraphStyle), ), EaseOutContainer( fadeIn: fadeIn, @@ -79,7 +79,7 @@ class SecondPage extends StatelessWidget { ), Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: Text(AppLocalizations.of(context)!.how_to_connect_sec_par, style: paragraphStyle), + child: Text(AppLocalizations.of(context)!.how_to_connect_sec_par, style: HumhubTheme.paragraphStyle), ), EaseOutContainer( fadeIn: fadeIn, @@ -94,7 +94,7 @@ class SecondPage extends StatelessWidget { style: ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith( (Set states) { - return primaryColor; + return HumhubTheme.primaryColor; }, ), ), @@ -114,7 +114,7 @@ class SecondPage extends StatelessWidget { ), Padding( padding: const EdgeInsets.symmetric(vertical: 20), - child: Text(AppLocalizations.of(context)!.how_to_connect_third_par, style: paragraphStyle), + child: Text(AppLocalizations.of(context)!.how_to_connect_third_par, style: HumhubTheme.paragraphStyle), ) ], ), diff --git a/lib/pages/help/components/third_page.dart b/lib/pages/help/components/third_page.dart index 6781c1f..465d551 100644 --- a/lib/pages/help/components/third_page.dart +++ b/lib/pages/help/components/third_page.dart @@ -22,21 +22,21 @@ class ThirdPage extends StatelessWidget { alignment: Alignment.centerLeft, child: Text( AppLocalizations.of(context)!.more_info_title, - style: getHeaderStyle(context), + style: HumhubTheme.getHeaderStyle(context), ), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: Text(AppLocalizations.of(context)!.more_info_first_par, style: paragraphStyle), + child: Text(AppLocalizations.of(context)!.more_info_first_par, style: HumhubTheme.paragraphStyle), ), Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: Text(AppLocalizations.of(context)!.more_info_second_par, style: paragraphStyle), + child: Text(AppLocalizations.of(context)!.more_info_second_par, style: HumhubTheme.paragraphStyle), ), Padding( padding: const EdgeInsets.symmetric(vertical: 10), - child: Text(AppLocalizations.of(context)!.more_info_third_par, style: paragraphStyle), + child: Text(AppLocalizations.of(context)!.more_info_third_par, style: HumhubTheme.paragraphStyle), ), const SizedBox( height: 40, @@ -54,12 +54,13 @@ class ThirdPage extends StatelessWidget { style: ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith( (Set states) { - return primaryColor; + return HumhubTheme.primaryColor; }, ), ), onPressed: () { - launchUrl(Uri.parse(AppLocalizations.of(context)!.more_info_pro_edition_url), mode: LaunchMode.platformDefault); + launchUrl(Uri.parse(AppLocalizations.of(context)!.more_info_pro_edition_url), + mode: LaunchMode.platformDefault); }, child: Center( child: Text( diff --git a/lib/pages/opener.dart b/lib/pages/opener.dart index ddbcf7c..028283c 100644 --- a/lib/pages/opener.dart +++ b/lib/pages/opener.dart @@ -8,7 +8,7 @@ import 'package:humhub/util/const.dart'; import 'package:humhub/util/form_helper.dart'; import 'package:humhub/util/intent/intent_plugin.dart'; import 'package:humhub/util/notifications/channel.dart'; -import 'package:humhub/util/opener_controller.dart'; +import 'package:humhub/util/openers/opener_controller.dart'; import 'package:humhub/util/providers.dart'; import 'package:rive/rive.dart'; import 'help/help_android.dart'; @@ -53,7 +53,7 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi String? urlIntent = InitFromIntent.usePayloadForInit(); if (urlIntent != null) { - await RedirectNotificationChannel().onTap(urlIntent); + await ref.read(notificationChannelProvider).value!.onTap(urlIntent); } }); } @@ -171,7 +171,7 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi onPressed: _connectInstance, child: Text( AppLocalizations.of(context)!.connect, - style: TextStyle(color: primaryColor, fontSize: 20), + style: TextStyle(color: HumhubTheme.primaryColor, fontSize: 20), ), ), ), @@ -246,7 +246,7 @@ class OpenerState extends ConsumerState with SingleTickerProviderStateMi await controlLer.initHumHub(); if (controlLer.allOk) { ref.read(humHubProvider).getInstance().then((value) { - Navigator.pushNamed(ref.context, WebViewApp.path, arguments: value.manifest); + Navigator.pushNamed(ref.context, WebView.path, arguments: value.manifest); }); } } diff --git a/lib/pages/web_view.dart b/lib/pages/web_view.dart index bba9395..087bde4 100644 --- a/lib/pages/web_view.dart +++ b/lib/pages/web_view.dart @@ -2,21 +2,23 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:app_settings/app_settings.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:humhub/components/auth_in_app_browser.dart'; +import 'package:humhub/util/auth_in_app_browser.dart'; import 'package:humhub/models/channel_message.dart'; import 'package:humhub/models/hum_hub.dart'; import 'package:humhub/models/manifest.dart'; import 'package:humhub/pages/opener.dart'; import 'package:humhub/util/connectivity_plugin.dart'; import 'package:humhub/util/extensions.dart'; -import 'package:humhub/util/notifications/channel.dart'; +import 'package:humhub/util/notifications/init_from_push.dart'; import 'package:humhub/util/providers.dart'; -import 'package:humhub/util/universal_opener_controller.dart'; +import 'package:humhub/util/openers/universal_opener_controller.dart'; +import 'package:humhub/util/push/provider.dart'; import 'package:humhub/util/router.dart'; import 'package:loggy/loggy.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -24,25 +26,17 @@ import 'package:humhub/util/router.dart' as m; import 'package:url_launcher/url_launcher.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -class WebViewGlobalController { - static InAppWebViewController? _value; +import '../util/web_view_global_controller.dart'; - static InAppWebViewController? get value => _value; - - static void setValue(InAppWebViewController newValue) { - _value = newValue; - } -} - -class WebViewApp extends ConsumerStatefulWidget { - const WebViewApp({super.key}); +class WebView extends ConsumerStatefulWidget { + const WebView({super.key}); static const String path = '/web_view'; @override WebViewAppState createState() => WebViewAppState(); } -class WebViewAppState extends ConsumerState { +class WebViewAppState extends ConsumerState { late AuthInAppBrowser authBrowser; late Manifest manifest; late URLRequest _initialRequest; @@ -59,28 +53,43 @@ class WebViewAppState extends ConsumerState { ), ); - PullToRefreshController? _pullToRefreshController; - late PullToRefreshOptions _pullToRefreshOptions; + late PullToRefreshController _pullToRefreshController; + HeadlessInAppWebView? headlessWebView; @override void initState() { super.initState(); + SchedulerBinding.instance.addPostFrameCallback((_) { + _initialRequest = _initRequest; + _pullToRefreshController = PullToRefreshController( + options: PullToRefreshOptions( + color: HexColor(manifest.themeColor), + ), + onRefresh: () async { + if (Platform.isAndroid) { + WebViewGlobalController.value?.reload(); + } else if (Platform.isIOS) { + WebViewGlobalController.value + ?.loadUrl(urlRequest: URLRequest(url: await WebViewGlobalController.value?.getUrl())); + } + }, + ); + authBrowser = AuthInAppBrowser( + manifest: manifest, + concludeAuth: (URLRequest request) { + _concludeAuth(request); + }, + ); + setState(() {}); + }); } @override Widget build(BuildContext context) { - _initialRequest = _initRequest; - _pullToRefreshController = initPullToRefreshController; - authBrowser = AuthInAppBrowser( - manifest: manifest, - concludeAuth: (URLRequest request) { - _concludeAuth(request); - }, - ); // ignore: deprecated_member_use return WillPopScope( - onWillPop: () => WebViewGlobalController.value!.exitApp(context, ref), + onWillPop: () => exitApp(context, ref), child: Scaffold( backgroundColor: HexColor(manifest.themeColor), body: SafeArea( @@ -108,6 +117,7 @@ class WebViewAppState extends ConsumerState { }, onLoadStop: _onLoadStop, onLoadStart: (controller, uri) async { + logDebug("onLoadStart"); _setAjaxHeadersJQuery(controller); }, onProgressChanged: _onProgressChanged, @@ -156,6 +166,7 @@ class WebViewAppState extends ConsumerState { logInfo(inMessage); ChannelMessage message = ChannelMessage.fromJson(inMessage!); await _handleJSMessage(message, headlessWebView!); + logDebug('flutterChannel triggered: ${message.type}'); }, ), ); @@ -163,6 +174,7 @@ class WebViewAppState extends ConsumerState { } Future _shouldInterceptFetchRequest(InAppWebViewController controller, FetchRequest request) async { + logDebug("_shouldInterceptFetchRequest"); request.headers!.addAll(_initialRequest.headers!); return request; } @@ -202,38 +214,15 @@ class WebViewAppState extends ConsumerState { "document.querySelector('#account-login-form > div.form-group.field-login-rememberme').style.display='none';"); } _setAjaxHeadersJQuery(controller); - _pullToRefreshController?.endRefreshing(); + _pullToRefreshController.endRefreshing(); } _onProgressChanged(InAppWebViewController controller, int progress) { if (progress == 100) { - _pullToRefreshController?.endRefreshing(); + _pullToRefreshController.endRefreshing(); } } - PullToRefreshController? get initPullToRefreshController { - _pullToRefreshOptions = PullToRefreshOptions( - color: HexColor(manifest.themeColor), - ); - return kIsWeb - ? null - : PullToRefreshController( - options: _pullToRefreshOptions, - onRefresh: () async { - Uri? url = await WebViewGlobalController.value!.getUrl(); - if (url != null) { - WebViewGlobalController.value!.loadUrl( - urlRequest: URLRequest( - url: await WebViewGlobalController.value!.getUrl(), - headers: ref.read(humHubProvider).customHeaders), - ); - } else { - WebViewGlobalController.value!.reload(); - } - }, - ); - } - askForNotificationPermissions() { showDialog( context: context, @@ -306,6 +295,39 @@ class WebViewAppState extends ConsumerState { } } + Future exitApp(context, ref) async { + bool canGoBack = await WebViewGlobalController.value!.canGoBack(); + if (canGoBack) { + WebViewGlobalController.value!.goBack(); + return Future.value(false); + } else { + final exitConfirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0))), + title: Text(AppLocalizations.of(context)!.web_view_exit_popup_title), + content: Text(AppLocalizations.of(context)!.web_view_exit_popup_content), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(AppLocalizations.of(context)!.no), + ), + TextButton( + onPressed: () { + var isHide = ref.read(humHubProvider).isHideDialog; + isHide + ? SystemNavigator.pop() + : Navigator.of(context).pushNamedAndRemoveUntil(Opener.path, (Route route) => false); + }, + child: Text(AppLocalizations.of(context)!.yes), + ), + ], + ), + ); + return exitConfirmed ?? false; + } + } + @override void dispose() { super.dispose(); diff --git a/lib/components/auth_in_app_browser.dart b/lib/util/auth_in_app_browser.dart similarity index 100% rename from lib/components/auth_in_app_browser.dart rename to lib/util/auth_in_app_browser.dart diff --git a/lib/util/connectivity_plugin.dart b/lib/util/connectivity_plugin.dart index 6052083..1059957 100644 --- a/lib/util/connectivity_plugin.dart +++ b/lib/util/connectivity_plugin.dart @@ -15,11 +15,11 @@ class NoConnectionDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(AppLocalizations.of(context)!.connectivity_popup_title), - content: Text(AppLocalizations.of(context)!.connectivity_popup_content), + title: Text(AppLocalizations.of(context)!.connectivity_popup_title), + content: Text(AppLocalizations.of(context)!.connectivity_popup_content), actions: [ TextButton( - child: Text(AppLocalizations.of(context)!.ok.toUpperCase()), + child: Text(AppLocalizations.of(context)!.ok.toUpperCase()), onPressed: () { Navigator.of(context).pop(); // Close the dialog }, @@ -32,7 +32,9 @@ class NoConnectionDialog extends StatelessWidget { showDialog( context: context, builder: (BuildContext context) { - return const NoConnectionDialog(); + return NoConnectionDialog( + key: context.widget.key, + ); }, ); } diff --git a/lib/util/const.dart b/lib/util/const.dart index dfb7957..8689c0e 100644 --- a/lib/util/const.dart +++ b/lib/util/const.dart @@ -3,21 +3,22 @@ import 'package:flutter/material.dart'; class StorageKeys { static String humhubInstance = "humHubInstance"; static String lastInstanceUrl = "humHubLastUrl"; - } class Assets { static String logo = "assets/images/logo.png"; static String helpImg = "assets/images/help.png"; - static String openerAnimationForward = "assets/opener_animation.riv"; - static String openerAnimationReverse = "assets/opener_animation_reverse.riv"; + static String openerAnimationForward = "assets/animations/opener_animation.riv"; + static String openerAnimationReverse = "assets/animations/opener_animation_reverse.riv"; } -Color primaryColor = const Color(0xFF21a1b3); +class HumhubTheme { + static Color primaryColor = const Color(0xFF21a1b3); -TextStyle? getHeaderStyle(context) { - return Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600); -} + static TextStyle? getHeaderStyle(context) { + return Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600); + } -TextStyle paragraphStyle = - const TextStyle(letterSpacing: 0.5, fontWeight: FontWeight.normal, color: Colors.black, fontSize: 15); + static TextStyle paragraphStyle = + const TextStyle(letterSpacing: 0.5, fontWeight: FontWeight.normal, color: Colors.black, fontSize: 15); +} diff --git a/lib/util/extensions.dart b/lib/util/extensions.dart index f752787..524afc8 100644 --- a/lib/util/extensions.dart +++ b/lib/util/extensions.dart @@ -1,52 +1,8 @@ -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:humhub/pages/opener.dart'; import 'package:humhub/util/const.dart'; -import 'package:humhub/util/providers.dart'; import 'package:loggy/loggy.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -extension MyWebViewController on InAppWebViewController { - Future exitApp(context, ref) async { - bool canGoBack = await this.canGoBack(); - if (canGoBack) { - goBack(); - return Future.value(false); - } else { - final exitConfirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10.0))), - title: Text(AppLocalizations.of(context)!.web_view_exit_popup_title), - content: Text(AppLocalizations.of(context)!.web_view_exit_popup_content), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(AppLocalizations.of(context)!.no), - ), - TextButton( - onPressed: () { - closeOrOpenDialog(context, ref); - }, - child: Text(AppLocalizations.of(context)!.yes), - ), - ], - ), - ); - return exitConfirmed ?? false; - } - } - - closeOrOpenDialog(BuildContext context, WidgetRef ref) { - var isHide = ref.read(humHubProvider).isHideDialog; - isHide - ? SystemNavigator.pop() - : Navigator.of(context).pushNamedAndRemoveUntil(Opener.path, (Route route) => false); - } -} class HexColor extends Color { static int _getColorFromHex(String hexColor) { @@ -58,7 +14,7 @@ class HexColor extends Color { return int.parse(hexColor, radix: 16); } catch (e) { logError("Color from manifest is not valid use primary color"); - return primaryColor.value; + return HumhubTheme.primaryColor.value; } } diff --git a/lib/util/form_helper.dart b/lib/util/form_helper.dart index bfd0b2b..fd93f41 100644 --- a/lib/util/form_helper.dart +++ b/lib/util/form_helper.dart @@ -11,4 +11,4 @@ class FormHelper { bool validate() => key.currentState?.validate() ?? false; void save() => key.currentState?.save(); -} \ No newline at end of file +} diff --git a/lib/util/intent/intent_plugin.dart b/lib/util/intent/intent_plugin.dart index 245a852..84668ca 100644 --- a/lib/util/intent/intent_plugin.dart +++ b/lib/util/intent/intent_plugin.dart @@ -7,7 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:humhub/pages/web_view.dart'; import 'package:humhub/util/loading_provider.dart'; import 'package:humhub/util/router.dart'; -import 'package:humhub/util/universal_opener_controller.dart'; +import 'package:humhub/util/openers/universal_opener_controller.dart'; import 'package:loggy/loggy.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:uni_links/uni_links.dart'; @@ -110,7 +110,7 @@ class IntentPluginState extends ConsumerState { if (redirectUrl != null) { UniversalOpenerController opener = UniversalOpenerController(url: redirectUrl); await opener.initHumHub(); - navigatorKey.currentState!.pushNamed(WebViewApp.path, arguments: opener); + navigatorKey.currentState!.pushNamed(WebView.path, arguments: opener); return; } } @@ -129,7 +129,7 @@ class IntentPluginState extends ConsumerState { LoadingProvider.of(ref).showLoading(); bool isNewRouteSameAsCurrent = false; navigatorKey.currentState!.popUntil((route) { - if (route.settings.name == WebViewApp.path) { + if (route.settings.name == WebView.path) { isNewRouteSameAsCurrent = true; } return true; @@ -138,7 +138,7 @@ class IntentPluginState extends ConsumerState { await opener.initHumHub(); // Always pop the current instance and init the new one. LoadingProvider.of(ref).dismissAll(); - navigatorKey.currentState!.pushNamed(WebViewApp.path, arguments: opener); + navigatorKey.currentState!.pushNamed(WebView.path, arguments: opener); return isNewRouteSameAsCurrent; } } diff --git a/lib/util/log.dart b/lib/util/log.dart index 0bd8661..b8d6956 100644 --- a/lib/util/log.dart +++ b/lib/util/log.dart @@ -11,8 +11,7 @@ class GlobalLog extends LoggyPrinter { bool get _colorize => showColors ?? false; static final _levelColors = { - LogLevel.debug: - AnsiColor(foregroundColor: AnsiColor.grey(0.5), italic: true), + LogLevel.debug: AnsiColor(foregroundColor: AnsiColor.grey(0.5), italic: true), LogLevel.info: AnsiColor(foregroundColor: 35), LogLevel.warning: AnsiColor(foregroundColor: 214), LogLevel.error: AnsiColor(foregroundColor: 196), @@ -30,21 +29,14 @@ class GlobalLog extends LoggyPrinter { @override void onLog(LogRecord record) { final time = record.time.toIso8601String().split('T')[1]; - final callerFrame = - record.callerFrame == null ? '-' : '(${record.callerFrame?.location})'; - final logLevel = record.level - .toString() - .replaceAll('Level.', '') - .toUpperCase() - .padRight(8); + final callerFrame = record.callerFrame == null ? '-' : '(${record.callerFrame?.location})'; + final logLevel = record.level.toString().replaceAll('Level.', '').toUpperCase().padRight(8); - final color = - _colorize ? levelColor(record.level) ?? AnsiColor() : AnsiColor(); + final color = _colorize ? levelColor(record.level) ?? AnsiColor() : AnsiColor(); final prefix = levelPrefix(record.level) ?? _defaultPrefix; if (kDebugMode) { - print(color( - '$prefix$time $logLevel GLOBAL $callerFrame ${record.message}')); + print(color('$prefix$time $logLevel GLOBAL $callerFrame ${record.message}')); } if (record.stackTrace != null) { @@ -61,4 +53,4 @@ class GlobalLog extends LoggyPrinter { AnsiColor? levelColor(LogLevel level) { return _levelColors[level]; } -} \ No newline at end of file +} diff --git a/lib/util/notifications/channel.dart b/lib/util/notifications/channel.dart index b38843f..a9d3626 100644 --- a/lib/util/notifications/channel.dart +++ b/lib/util/notifications/channel.dart @@ -1,51 +1,29 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:humhub/flavored/util/notifications/channel.dart'; import 'package:humhub/pages/web_view.dart'; -import 'package:humhub/util/universal_opener_controller.dart'; +import 'package:humhub/util/notifications/init_from_push.dart'; +import 'package:humhub/util/openers/universal_opener_controller.dart'; import 'package:humhub/util/router.dart'; -import 'package:loggy/loggy.dart'; +import 'package:package_info_plus/package_info_plus.dart'; -abstract class NotificationChannel { +class NotificationChannel { final String id; final String name; final String description; - NotificationChannel(this.id, this.name, this.description); - - Future onTap(String? payload); - - @protected - Future navigate(String route, {Object? arguments}) async { - logInfo('NotificationChannel navigate: $route'); - if (navigatorKey.currentState?.mounted ?? false) { - await navigatorKey.currentState?.pushNamed( - route, - arguments: arguments, - ); - } else { - queueRoute( - route, - arguments: arguments, - ); - } - } -} - -class RedirectNotificationChannel extends NotificationChannel { - RedirectNotificationChannel() - : super( - 'redirect', - 'Redirect app notifications', - 'These notifications are redirect the user to specific url in a payload.', - ); + const NotificationChannel( + {this.id = 'redirect', + this.name = 'Redirect app notifications', + this.description = 'These notifications are redirect the user to specific url in a payload.'}); /// If the WebView is not opened yet or the app is not running the onTap will wake up the app or redirect to the WebView. - /// If app is already running in WebView mode then the state of [WebViewApp] will be updated with new url. - @override + /// If app is already running in WebView mode then the state of [WebView] will be updated with new url. + /// Future onTap(String? payload) async { if (payload != null && navigatorKey.currentState != null) { bool isNewRouteSameAsCurrent = false; navigatorKey.currentState!.popUntil((route) { - if (route.settings.name == WebViewApp.path) { + if (route.settings.name == WebView.path) { isNewRouteSameAsCurrent = true; } return true; @@ -53,28 +31,29 @@ class RedirectNotificationChannel extends NotificationChannel { UniversalOpenerController opener = UniversalOpenerController(url: payload); await opener.initHumHub(); if (isNewRouteSameAsCurrent) { - navigatorKey.currentState!.pushNamed(WebViewApp.path, arguments: opener); + navigatorKey.currentState!.pushNamed(WebView.path, arguments: opener); return; } - navigatorKey.currentState!.pushNamed(WebViewApp.path, arguments: opener); + navigatorKey.currentState!.pushNamed(WebView.path, arguments: opener); } else { if (payload != null) { InitFromPush.setPayload(payload); } } } -} - -class InitFromPush { - static String? _redirectUrlFromInit; - - static setPayload(String payload) { - _redirectUrlFromInit = payload; - } - static String? usePayload() { - String? payload = _redirectUrlFromInit; - _redirectUrlFromInit = null; - return payload; + static Future getChannel() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); // Replace this with the actual condition logic + switch (packageInfo.packageName) { + case 'com.humhub.app': + return const NotificationChannel(); + default: + return const NotificationChannelF(); + } } } + +// Providers for NotificationChannel and NotificationChannelF +final notificationChannelProvider = FutureProvider((ref) { + return NotificationChannel.getChannel(); +}); diff --git a/lib/util/notifications/init_from_push.dart b/lib/util/notifications/init_from_push.dart new file mode 100644 index 0000000..7f42313 --- /dev/null +++ b/lib/util/notifications/init_from_push.dart @@ -0,0 +1,13 @@ +class InitFromPush { + static String? _redirectUrlFromInit; + + static setPayload(String payload) { + _redirectUrlFromInit = payload; + } + + static String? usePayload() { + String? payload = _redirectUrlFromInit; + _redirectUrlFromInit = null; + return payload; + } +} \ No newline at end of file diff --git a/lib/util/notifications/plugin.dart b/lib/util/notifications/plugin.dart index f79ed02..257a1f9 100644 --- a/lib/util/notifications/plugin.dart +++ b/lib/util/notifications/plugin.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:humhub/util/notifications/service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class NotificationPlugin extends StatefulWidget { final Widget child; @@ -17,6 +18,14 @@ class NotificationPlugin extends StatefulWidget { return plugin!; } + static Future hasAskedPermissionBefore() async { + String key = 'was_asked_before'; + SharedPreferences prefs = await SharedPreferences.getInstance(); + var data = prefs.getBool(key) ?? false; + prefs.setBool(key, true); + return data; + } + @override NotificationPluginState createState() => NotificationPluginState(); } diff --git a/lib/util/notifications/service.dart b/lib/util/notifications/service.dart index 11cdba9..aaf8908 100644 --- a/lib/util/notifications/service.dart +++ b/lib/util/notifications/service.dart @@ -48,7 +48,8 @@ class NotificationService { static void handleNotification(NotificationResponse response) async { final parsed = response.payload != null ? json.decode(response.payload!) : {}; if (parsed["redirectUrl"] != null) { - await RedirectNotificationChannel().onTap(parsed['redirectUrl']); + var channel = await NotificationChannel.getChannel(); + channel.onTap(parsed['redirectUrl']); return; } } @@ -85,6 +86,13 @@ class NotificationService { largeIcon: const DrawableResourceAndroidBitmap('@mipmap/ic_launcher'), color: color, ), + iOS: DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + threadIdentifier: channel.id, + interruptionLevel: InterruptionLevel.timeSensitive, + ), ); } diff --git a/lib/util/opener_controller.dart b/lib/util/openers/opener_controller.dart similarity index 95% rename from lib/util/opener_controller.dart rename to lib/util/openers/opener_controller.dart index a5f9647..0ac8eca 100644 --- a/lib/util/opener_controller.dart +++ b/lib/util/openers/opener_controller.dart @@ -6,9 +6,9 @@ import 'package:humhub/models/manifest.dart'; import 'package:humhub/util/providers.dart'; import 'package:http/http.dart' as http; import 'package:loggy/loggy.dart'; -import 'api_provider.dart'; -import 'connectivity_plugin.dart'; -import 'form_helper.dart'; +import '../api_provider.dart'; +import '../connectivity_plugin.dart'; +import '../form_helper.dart'; class OpenerController { late AsyncValue? asyncData; @@ -93,7 +93,9 @@ class OpenerController { String currentUrl = urlTextController.text; String hash = HumHub.generateHash(32); if (lastUrl == currentUrl) hash = ref.read(humHubProvider).randomHash ?? hash; - await ref.read(humHubProvider).setInstance(HumHub(manifest: manifest, randomHash: hash, manifestUrl: manifestUrl)); + await ref + .read(humHubProvider) + .setInstance(HumHub(manifest: manifest, randomHash: hash, manifestUrl: manifestUrl)); } } diff --git a/lib/util/universal_opener_controller.dart b/lib/util/openers/universal_opener_controller.dart similarity index 97% rename from lib/util/universal_opener_controller.dart rename to lib/util/openers/universal_opener_controller.dart index 8a9f96d..1177193 100644 --- a/lib/util/universal_opener_controller.dart +++ b/lib/util/openers/universal_opener_controller.dart @@ -3,8 +3,8 @@ import 'package:http/http.dart'; import 'package:humhub/models/hum_hub.dart'; import 'package:humhub/models/manifest.dart'; import 'package:http/http.dart' as http; -import 'api_provider.dart'; -import 'connectivity_plugin.dart'; +import '../api_provider.dart'; +import '../connectivity_plugin.dart'; class UniversalOpenerController { late AsyncValue? asyncData; diff --git a/lib/util/override_locale.dart b/lib/util/override_locale.dart index 075035b..b73b0d0 100644 --- a/lib/util/override_locale.dart +++ b/lib/util/override_locale.dart @@ -15,9 +15,9 @@ class OverrideLocale extends StatefulWidget { static OverrideLocaleModel of(BuildContext context) { final result = context.dependOnInheritedWidgetOfExactType(); assert( - result != null, - 'No OverrideLocale found in context' - 'Place OverrideLocale widget as high in widget tree as possible.', + result != null, + 'No OverrideLocale found in context' + 'Place OverrideLocale widget as high in widget tree as possible.', ); return result!; } diff --git a/lib/util/providers.dart b/lib/util/providers.dart index 8704437..3bffdea 100644 --- a/lib/util/providers.dart +++ b/lib/util/providers.dart @@ -1,11 +1,9 @@ import 'dart:convert'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:humhub/models/hum_hub.dart'; import 'package:humhub/models/manifest.dart'; -import 'package:humhub/util/extensions.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'const.dart'; @@ -42,7 +40,7 @@ class HumHubNotifier extends ChangeNotifier { _humHubInstance.isHideOpener = instance.isHideOpener; _humHubInstance.randomHash = instance.randomHash; _humHubInstance.appVersion = packageInfo.version; - _humHubInstance.manifestUrl = instance.manifestUrl; + _humHubInstance.manifestUrl = instance.manifestUrl; _updateSafeStorage(); notifyListeners(); } @@ -86,33 +84,3 @@ class HumHubNotifier extends ChangeNotifier { final humHubProvider = ChangeNotifierProvider((ref) { return HumHubNotifier(HumHub()); }); - -/// Remembers whether current FirebaseApp is initialized. -final firebaseInitialized = StateProvider>( - (ref) => const AsyncValue.loading(), -); - -final _pushTokenProvider = FutureProvider>( - (ref) async { - var initialized = ref.watch(firebaseInitialized.notifier).state; - if (initialized.isLoaded) { - return AsyncValue.guard(FirebaseMessaging.instance.getToken); - } - return const AsyncValue.loading(); - }, -); - -/// Provides current push token. Will wait until Firebase is initialized. -/// -/// See also: -/// * [_PushPluginState._init] -final pushTokenProvider = Provider>( - (ref) { - var provider = ref.watch(_pushTokenProvider); - return provider.when( - data: (value) => value, - error: (e, s) => AsyncValue.error(e, s), - loading: () => const AsyncValue.loading(), - ); - }, -); diff --git a/lib/util/push/provider.dart b/lib/util/push/provider.dart new file mode 100644 index 0000000..76ce8b4 --- /dev/null +++ b/lib/util/push/provider.dart @@ -0,0 +1,33 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:humhub/util/extensions.dart'; + +/// Remembers whether current FirebaseApp is initialized. +final firebaseInitialized = StateProvider>( + (ref) => const AsyncValue.loading(), +); + +final _pushTokenProvider = FutureProvider>( + (ref) async { + var initialized = ref.watch(firebaseInitialized.notifier).state; + if (initialized.isLoaded) { + return AsyncValue.guard(FirebaseMessaging.instance.getToken); + } + return const AsyncValue.loading(); + }, +); + +/// Provides current push token. Will wait until Firebase is initialized. +/// +/// See also: +/// * [_PushPluginState._init] +final pushTokenProvider = Provider>( + (ref) { + var provider = ref.watch(_pushTokenProvider); + return provider.when( + data: (value) => value, + error: (e, s) => AsyncValue.error(e, s), + loading: () => const AsyncValue.loading(), + ); + }, +); diff --git a/lib/util/push/push_plugin.dart b/lib/util/push/push_plugin.dart index a01195a..8d773fe 100644 --- a/lib/util/push/push_plugin.dart +++ b/lib/util/push/push_plugin.dart @@ -7,17 +7,17 @@ import 'package:humhub/models/event.dart'; import 'package:humhub/util/notifications/channel.dart'; import 'package:humhub/util/notifications/plugin.dart'; import 'package:humhub/util/notifications/service.dart'; +import 'package:humhub/util/push/provider.dart'; import 'package:humhub/util/push/register_token_plugin.dart'; -import 'package:humhub/util/providers.dart'; import 'package:loggy/loggy.dart'; class PushPlugin extends ConsumerStatefulWidget { final Widget child; - const PushPlugin({ + const PushPlugin({ Key? key, required this.child, - }) : super(key: key); + }) : super(key: key, ); @override PushPluginState createState() => PushPluginState(); @@ -42,7 +42,7 @@ class PushPluginState extends ConsumerState { FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { logInfo("Firebase messaging onMessageOpenedApp"); final data = PushEvent(message).parsedData; - RedirectNotificationChannel().onTap(data.redirectUrl); + ref.read(notificationChannelProvider).value!.onTap(data.redirectUrl); }); //When the app is terminated, i.e., app is neither in foreground or background. @@ -60,10 +60,34 @@ class PushPluginState extends ConsumerState { @override void initState() { + ref.read(notificationChannelProvider); _init(); super.initState(); } + _handleInitialMsg(RemoteMessage message) { + final data = PushEvent(message).parsedData; + if (data.redirectUrl != null) { + ref.read(notificationChannelProvider).value!.onTap(data.redirectUrl); + } + } + + Future _handleNotification(RemoteMessage message, NotificationService notificationService) async { + // Here we handle the notification that we get form an push notification. + final data = PushEvent(message).parsedData; + if (message.notification == null) return; + final title = message.notification?.title; + final body = message.notification?.body; + if (title == null || body == null) return; + await notificationService.showNotification( + ref.read(notificationChannelProvider).value!, + title, + body, + payload: data.channelPayload, + redirectUrl: data.redirectUrl, + ); + } + @override Widget build(BuildContext context) { return RegisterToken( @@ -72,29 +96,6 @@ class PushPluginState extends ConsumerState { } } -_handleInitialMsg(RemoteMessage message) { - final data = PushEvent(message).parsedData; - if (data.redirectUrl != null) { - RedirectNotificationChannel().onTap(data.redirectUrl); - } -} - -Future _handleNotification(RemoteMessage message, NotificationService notificationService) async { - // Here we handle the notification that we get form an push notification. - final data = PushEvent(message).parsedData; - if (message.notification == null) return; - final title = message.notification?.title; - final body = message.notification?.body; - if (title == null || body == null) return; - await notificationService.showNotification( - RedirectNotificationChannel(), - title, - body, - payload: data.channelPayload, - redirectUrl: data.redirectUrl, - ); -} - Future _handleData(RemoteMessage message, BuildContext context, WidgetRef ref) async { // Here we handle the data that we get form an push notification. PushEventData data = PushEvent(message).parsedData; diff --git a/lib/util/push/register_token_plugin.dart b/lib/util/push/register_token_plugin.dart index 329c27f..f01d169 100644 --- a/lib/util/push/register_token_plugin.dart +++ b/lib/util/push/register_token_plugin.dart @@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:humhub/util/api_provider.dart'; import 'package:humhub/util/extensions.dart'; import 'package:humhub/util/providers.dart'; +import 'package:humhub/util/push/provider.dart'; import 'package:loggy/loggy.dart'; class RegisterToken extends ConsumerWidget { @@ -42,7 +43,7 @@ class _RegisterToken extends ConsumerStatefulWidget { _RegisterTokenState createState() => _RegisterTokenState(); } -class _RegisterTokenState extends ConsumerState<_RegisterToken>{ +class _RegisterTokenState extends ConsumerState<_RegisterToken> { Future _maybeRegisterToken() async { final token = await FirebaseMessaging.instance.getToken(); if (token == null) { @@ -66,13 +67,13 @@ class _RegisterTokenState extends ConsumerState<_RegisterToken>{ } Future Function(Dio dio) _registerToken(String? token) => (dio) async { - await dio.post( - '/fcm-push/token/update', - data: { - 'token': token, - }, - ); - }; + await dio.post( + '/fcm-push/token/update', + data: { + 'token': token, + }, + ); + }; @override void didUpdateWidget(oldWidget) { diff --git a/lib/util/router.dart b/lib/util/router.dart index 8fc58ce..0d69566 100644 --- a/lib/util/router.dart +++ b/lib/util/router.dart @@ -34,7 +34,7 @@ class MyRouter { static var routes = { Opener.path: (context) => const Opener(), - WebViewApp.path: (context) => const WebViewApp(), + WebView.path: (context) => const WebView(), '/help': (context) => Platform.isAndroid ? const HelpAndroid() : const HelpIos(), }; @@ -46,9 +46,9 @@ class MyRouter { initRoute = Opener.path; return Opener.path; case RedirectAction.webView: - initRoute = WebViewApp.path; + initRoute = WebView.path; initParams = humhub.manifest; - return WebViewApp.path; + return WebView.path; } } } diff --git a/lib/util/show_dialog.dart b/lib/util/show_dialog.dart new file mode 100644 index 0000000..780f758 --- /dev/null +++ b/lib/util/show_dialog.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:app_settings/app_settings.dart'; + +class ShowDialog { + final BuildContext context; + + ShowDialog(this.context); + + static ShowDialog of(BuildContext context) { + return ShowDialog(context); + } + + void notificationPermission() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(AppLocalizations.of(context)!.notification_permission_popup_title), + content: Text(AppLocalizations.of(context)!.notification_permission_popup_content), + actions: [ + TextButton( + child: Text(AppLocalizations.of(context)!.enable), + onPressed: () { + AppSettings.openAppSettings(); + Navigator.pop(context); + }, + ), + TextButton( + child: Text(AppLocalizations.of(context)!.skip), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), + ); + } + + noInternetPopup(){ + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(AppLocalizations.of(context)!.connectivity_popup_title), + content: Text(AppLocalizations.of(context)!.connectivity_popup_content), + actions: [ + TextButton( + child: Text(AppLocalizations.of(context)!.ok.toUpperCase()), + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/util/storage_service.dart b/lib/util/storage_service.dart new file mode 100644 index 0000000..3963f2b --- /dev/null +++ b/lib/util/storage_service.dart @@ -0,0 +1,29 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SecureStorageService { + // Private constructor + static const FlutterSecureStorage _instance = FlutterSecureStorage(); + + // Factory constructor that returns the single instance + factory SecureStorageService() { + return SecureStorageService._internal(); + } + + // Private named constructor + SecureStorageService._internal(); + + // Method to access the single instance of FlutterSecureStorage + static FlutterSecureStorage get instance => _instance; + + static clearSecureStorageOnReinstall() async { + String key = 'hasRunBefore'; + SharedPreferences prefs = await SharedPreferences.getInstance(); + bool hasRunBefore = prefs.getBool(key) ?? false; + if (!hasRunBefore) { + FlutterSecureStorage storage = const FlutterSecureStorage(); + await storage.deleteAll(); + prefs.setBool(key, true); + } + } +} diff --git a/lib/util/web_view_global_controller.dart b/lib/util/web_view_global_controller.dart new file mode 100644 index 0000000..cab3d6e --- /dev/null +++ b/lib/util/web_view_global_controller.dart @@ -0,0 +1,11 @@ +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +class WebViewGlobalController { + static InAppWebViewController? _value; + + static InAppWebViewController? get value => _value; + + static void setValue(InAppWebViewController newValue) { + _value = newValue; + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 2242002..5c38660 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -278,6 +278,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_inappwebview: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index cff522b..d57756f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: swipe_to: ^1.0.2 shared_preferences: ^2.2.2 intl: any + flutter_dotenv: ^5.1.0 dev_dependencies: flutter_test: @@ -90,10 +91,10 @@ flutter: - asset: assets/fonts/OpenSans-SemiBold.ttf assets: + - assets/ - assets/images/ - assets/images/locale/ - - assets/opener_animation.riv - - assets/opener_animation_reverse.riv + - assets/animations/ # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg diff --git a/test/opener_test.dart b/test/opener_test.dart index 6acfdf1..a8a7758 100644 --- a/test/opener_test.dart +++ b/test/opener_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:humhub/util/opener_controller.dart'; +import 'package:humhub/util/openers/opener_controller.dart'; void main() { void testGroupOfURIs(Map uriMap) {