Skip to content

Commit

Permalink
Add logic for white labeled app
Browse files Browse the repository at this point in the history
  • Loading branch information
PrimozRatej committed May 19, 2024
1 parent b2e990b commit 707419e
Show file tree
Hide file tree
Showing 14 changed files with 445 additions and 22 deletions.
21 changes: 21 additions & 0 deletions lib/apps/flavored/models/humhub.f.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:humhub/apps/flavored/models/manifest.f.dart';
import 'package:humhub/models/hum_hub.dart';

class HumHubF extends HumHub{
@override
ManifestF get manifest => ManifestF.fromEnv();
@override
String get manifestUrl => dotenv.env['MANIFEST_URL']!;

HumHubF({
bool isHideOpener = false,
String? randomHash,
String? appVersion,
String? pushToken,
}) : super(
isHideOpener: isHideOpener,
randomHash: HumHub.generateHash(32),
appVersion: appVersion,
pushToken: pushToken);
}
25 changes: 25 additions & 0 deletions lib/apps/flavored/models/manifest.f.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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']!,
);
}
}
245 changes: 245 additions & 0 deletions lib/apps/flavored/web_view.f.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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/apps/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/channel.dart';
import 'package:humhub/util/notifications/plugin.dart';
import 'package:humhub/util/providers.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';

class WebViewF extends ConsumerStatefulWidget {
const WebViewF({super.key});

@override
FlavoredWebViewState createState() => FlavoredWebViewState();
}

class FlavoredWebViewState extends ConsumerState<WebViewF> {
HumHubF instance = HumHubF();
late AuthInAppBrowser _authBrowser;
HeadlessInAppWebView? headlessWebView;

@override
void initState() {
_authBrowser = AuthInAppBrowser(
manifest: instance.manifest,
concludeAuth: (URLRequest request) {
_concludeAuth(request);
},
);
super.initState();
}

@override
Widget build(BuildContext context) {
// ignore: deprecated_member_use
return WillPopScope(
onWillPop: () => WebViewGlobalController.value!.exitApp(context, ref),
child: Scaffold(
backgroundColor: HexColor(instance.manifest.themeColor),
body: SafeArea(
bottom: false,
child: InAppWebView(
initialUrlRequest: _initialRequest,
initialOptions: _options,
pullToRefreshController: _pullToRefreshController,
shouldOverrideUrlLoading: _shouldOverrideUrlLoading,
onWebViewCreated: _onWebViewCreated,
shouldInterceptFetchRequest: _shouldInterceptFetchRequest,
onCreateWindow: _onCreateWindow,
onLoadStop: _onLoadStop,
onLoadStart: _onLoadStart,
onLoadError: _onLoadError,
onProgressChanged: _onProgressChanged,
),
),
),
);
}

URLRequest get _initialRequest {
String? url = instance.manifest.startUrl;
String? payloadFromPush = InitFromPush.usePayload();
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,
),
);

PullToRefreshController get _pullToRefreshController => PullToRefreshController(
options: PullToRefreshOptions(
color: HexColor(instance.manifest.themeColor),
),
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();
}
},
);

Future<NavigationActionPolicy?> _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<String, String> mergedMap = {...instance.customHeaders, ...?action.request.headers};
URLRequest newRequest = action.request.copyWith(headers: mergedMap);
controller.loadUrl(urlRequest: newRequest);
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
}

Future<void> _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<FetchRequest?> _shouldInterceptFetchRequest(InAppWebViewController controller, FetchRequest request) async {
request.headers!.addAll(_initialRequest.headers!);
return request;
}

Future<bool> _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');
}
LoadingProvider.of(ref).dismissAll();
return Future.value(true); // Allow creating a new window.
}

Future<void> _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);
await _pullToRefreshController.endRefreshing();
LoadingProvider.of(ref).dismissAll();
}

Future<void> _onLoadStart(InAppWebViewController controller, Uri? url) async {
await _setAjaxHeadersJQuery(controller);
LoadingProvider.of(ref).dismissAll();
}

void _onLoadError(InAppWebViewController controller, Uri? url, int code, String message) async {
if (code == -1009) ShowDialog.of(context).noInternetPopup();
await _pullToRefreshController.endRefreshing();
}

void _onProgressChanged(controller, progress) async {
if (progress == 100) {
await _pullToRefreshController.endRefreshing();
}
}

void _concludeAuth(URLRequest request) {
_authBrowser.close();
WebViewGlobalController.value!.loadUrl(urlRequest: request);
}

Future<void> _setAjaxHeadersJQuery(InAppWebViewController controller) async {
String jsCode = "\$.ajaxSetup({headers: ${jsonEncode(instance.customHeaders).toString()}});";
await controller.evaluateJavascript(source: jsCode);
}

Future<void> _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;
}
}

@override
void dispose() {
super.dispose();
if (headlessWebView != null) {
headlessWebView!.dispose();
}
}
}
42 changes: 42 additions & 0 deletions lib/apps/flavored_app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:humhub/apps/flavored/web_view.f.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/storage_service.dart';

class FlavoredApp extends ConsumerStatefulWidget {
const FlavoredApp({super.key});

@override
FlavoredAppState createState() => FlavoredAppState();
}

class FlavoredAppState extends ConsumerState<FlavoredApp> {
@override
Widget build(BuildContext context) {
SecureStorageService.clearSecureStorageOnReinstall();
return IntentPlugin(
child: NotificationPlugin(
child: PushPlugin(
child: OverrideLocale(
builder: (overrideLocale) => Builder(
builder: (context) => MaterialApp(
debugShowCheckedModeBanner: false,
builder: (context, child) => const LoadingProvider(
child: WebViewF(),
),
theme: ThemeData(
fontFamily: 'OpenSans',
),
),
),
),
),
),
);
}
}
4 changes: 2 additions & 2 deletions lib/apps/opener_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ class OpenerApp extends ConsumerStatefulWidget {
const OpenerApp({super.key});

@override
MyAppState createState() => MyAppState();
OpenerAppState createState() => OpenerAppState();
}

class MyAppState extends ConsumerState<OpenerApp> {
class OpenerAppState extends ConsumerState<OpenerApp> {
@override
Widget build(BuildContext context) {
SecureStorageService.clearSecureStorageOnReinstall();
Expand Down
16 changes: 13 additions & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:humhub/apps/flavored_app.dart';
import 'package:humhub/util/log.dart';
import 'package:humhub/util/storage_service.dart';
import 'package:loggy/loggy.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'apps/opener_app.dart';

main() async {
Expand All @@ -12,7 +15,14 @@ main() async {
);
WidgetsFlutterBinding.ensureInitialized();
await SecureStorageService.clearSecureStorageOnReinstall();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_) {
runApp(const ProviderScope(child: OpenerApp()));
});

PackageInfo packageInfo = await PackageInfo.fromPlatform();
if (packageInfo.packageName == 'com.humhub.app') {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_) {
runApp(const ProviderScope(child: OpenerApp()));
});
} else {
await dotenv.load(fileName: ".env");
runApp(const ProviderScope(child: FlavoredApp()));
}
}
Loading

0 comments on commit 707419e

Please sign in to comment.