1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-11-22 12:59:21 +00:00

Start to integrate push notifications

This commit is contained in:
Pierre HUBERT 2021-04-12 19:26:05 +02:00
parent 38c639331f
commit 612fc7b0d9
9 changed files with 324 additions and 2 deletions

View File

@ -1,5 +1,6 @@
import 'package:comunic/helpers/api_helper.dart'; import 'package:comunic/helpers/api_helper.dart';
import 'package:comunic/helpers/preferences_helper.dart'; import 'package:comunic/helpers/preferences_helper.dart';
import 'package:comunic/helpers/push_notifications_helper.dart';
import 'package:comunic/helpers/websocket_helper.dart'; import 'package:comunic/helpers/websocket_helper.dart';
import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/authentication_details.dart'; import 'package:comunic/models/authentication_details.dart';
@ -77,6 +78,8 @@ class AccountHelper {
/// Sign out user /// Sign out user
Future<void> signOut() async { Future<void> signOut() async {
await PushNotificationsHelper.clearLocalStatus();
await APIRequest.withLogin("account/logout").exec(); await APIRequest.withLogin("account/logout").exec();
final preferencesHelper = await PreferencesHelper.getInstance(); final preferencesHelper = await PreferencesHelper.getInstance();

View File

@ -13,6 +13,7 @@ enum PreferencesKeyList {
ENABLE_DARK_THEME, ENABLE_DARK_THEME,
FORCE_MOBILE_MODE, FORCE_MOBILE_MODE,
SHOW_PERFORMANCE_OVERLAY, SHOW_PERFORMANCE_OVERLAY,
PUSH_NOTIFICATIONS_STATUS,
} }
const _PreferenceKeysName = { const _PreferenceKeysName = {
@ -62,6 +63,10 @@ class PreferencesHelper {
} }
} }
bool containsKey(PreferencesKeyList key) {
return _sharedPreferences.containsKey(_PreferenceKeysName[key]);
}
Future<bool> removeKey(PreferencesKeyList key) async { Future<bool> removeKey(PreferencesKeyList key) async {
return await _sharedPreferences.remove(_PreferenceKeysName[key]); return await _sharedPreferences.remove(_PreferenceKeysName[key]);
} }

View File

@ -0,0 +1,88 @@
import 'package:comunic/helpers/preferences_helper.dart';
import 'package:comunic/models/api_request.dart';
/// Push notifications helper
///
/// @author Pierre Hubert
enum PushNotificationsStatus { UNDEFINED, DISABLED, FIREBASE, INDEPENDENT }
const _PushNotificationsAPIMap = {
"undefined": PushNotificationsStatus.UNDEFINED,
"disabled": PushNotificationsStatus.DISABLED,
"firebase": PushNotificationsStatus.FIREBASE,
"independent": PushNotificationsStatus.INDEPENDENT
};
class PushNotificationsHelper {
/// Get cached status of push notifications
static Future<PushNotificationsStatus> getLocalStatus() async {
final pref = await PreferencesHelper.getInstance();
if (!pref.containsKey(PreferencesKeyList.PUSH_NOTIFICATIONS_STATUS))
return PushNotificationsStatus.UNDEFINED;
return _PushNotificationsAPIMap[
pref.getString(PreferencesKeyList.PUSH_NOTIFICATIONS_STATUS)];
}
/// Refresh local status with information from server
///
/// Throws in case of failure
static Future<void> refreshLocalStatus() async {
final pref = await PreferencesHelper.getInstance();
final response = await APIRequest.withLogin("push_notifications/status")
.execWithThrowGetObject();
switch (response["status"]) {
case "undefined":
await pref.removeKey(PreferencesKeyList.PUSH_NOTIFICATIONS_STATUS);
break;
case "disabled":
await pref.setString(
PreferencesKeyList.PUSH_NOTIFICATIONS_STATUS, "disabled");
break;
case "firebase":
await pref.setString(
PreferencesKeyList.PUSH_NOTIFICATIONS_STATUS, "firebase");
break;
case "independent":
await pref.setString(
PreferencesKeyList.PUSH_NOTIFICATIONS_STATUS, "independent");
// TODO : Invoke plugin to apply new WebSocket URL
break;
default:
print(
"Push notifications status ${response["status"]} is unknown, defaulting to disabled!");
await pref.setString(
PreferencesKeyList.PUSH_NOTIFICATIONS_STATUS, "disabled");
}
}
/// Clear local push notifications status
static Future<void> clearLocalStatus() async {
await (await PreferencesHelper.getInstance())
.removeKey(PreferencesKeyList.PUSH_NOTIFICATIONS_STATUS);
// TODO : stop local refresh notification refresh
}
/// Set new push notification status on the server
static Future<void> setNewStatus(
PushNotificationsStatus newStatus, {
String firebaseToken = "",
}) async =>
await APIRequest.withLogin("push_notifications/configure")
.addString(
"status",
_PushNotificationsAPIMap.entries
.firstWhere((e) => e.value == newStatus)
.key)
.addString("firebase_token", firebaseToken)
.execWithThrow();
}

View File

@ -17,6 +17,7 @@ class ServerConfigurationHelper {
(await APIRequest.withoutLogin("server/config").execWithThrow()) (await APIRequest.withoutLogin("server/config").execWithThrow())
.getObject(); .getObject();
final pushNotificationsPolicy = response["push_notifications"];
final passwordPolicy = response["password_policy"]; final passwordPolicy = response["password_policy"];
final dataConservationPolicy = response["data_conservation_policy"]; final dataConservationPolicy = response["data_conservation_policy"];
final conversationsPolicy = response["conversations_policy"]; final conversationsPolicy = response["conversations_policy"];
@ -27,6 +28,10 @@ class ServerConfigurationHelper {
termsURL: response["terms_url"], termsURL: response["terms_url"],
playStoreURL: response["play_store_url"], playStoreURL: response["play_store_url"],
androidDirectDownloadURL: response["android_direct_download_url"], androidDirectDownloadURL: response["android_direct_download_url"],
notificationsPolicy: NotificationsPolicy(
hasFirebase: pushNotificationsPolicy["has_firebase"],
hasIndependent: pushNotificationsPolicy["has_independent"],
),
passwordPolicy: PasswordPolicy( passwordPolicy: PasswordPolicy(
allowMailInPassword: passwordPolicy["allow_email_in_password"], allowMailInPassword: passwordPolicy["allow_email_in_password"],
allowNameInPassword: passwordPolicy["allow_name_in_password"], allowNameInPassword: passwordPolicy["allow_name_in_password"],

View File

@ -1,25 +1,32 @@
import 'dart:ui';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
/// Application configuration model /// Application configuration model
/// ///
/// @author Pierre HUBERT /// @author Pierre HUBERT
const defaultColor = Color(0xFF1A237E);
/// Configuration class /// Configuration class
class Config { class Config {
final String apiServerName; final String apiServerName;
final String apiServerUri; final String apiServerUri;
final bool apiServerSecure; final bool apiServerSecure;
final String clientName; final String clientName;
final Color splashBackgroundColor;
const Config({ const Config({
@required this.apiServerName, @required this.apiServerName,
@required this.apiServerUri, @required this.apiServerUri,
@required this.apiServerSecure, @required this.apiServerSecure,
@required this.clientName, @required this.clientName,
this.splashBackgroundColor = defaultColor,
}) : assert(apiServerName != null), }) : assert(apiServerName != null),
assert(apiServerUri != null), assert(apiServerUri != null),
assert(apiServerSecure != null), assert(apiServerSecure != null),
assert(clientName != null); assert(clientName != null),
assert(splashBackgroundColor != null);
/// Get and set static configuration /// Get and set static configuration
static Config _config; static Config _config;

View File

@ -5,6 +5,17 @@ import 'package:version/version.dart';
/// ///
/// @author Pierre Hubert /// @author Pierre Hubert
class NotificationsPolicy {
final bool hasFirebase;
final bool hasIndependent;
const NotificationsPolicy({
@required this.hasFirebase,
@required this.hasIndependent,
}) : assert(hasFirebase != null),
assert(hasIndependent != null);
}
class PasswordPolicy { class PasswordPolicy {
final bool allowMailInPassword; final bool allowMailInPassword;
final bool allowNameInPassword; final bool allowNameInPassword;
@ -106,6 +117,7 @@ class ServerConfig {
final String termsURL; final String termsURL;
final String playStoreURL; final String playStoreURL;
final String androidDirectDownloadURL; final String androidDirectDownloadURL;
final NotificationsPolicy notificationsPolicy;
final PasswordPolicy passwordPolicy; final PasswordPolicy passwordPolicy;
final ServerDataConservationPolicy dataConservationPolicy; final ServerDataConservationPolicy dataConservationPolicy;
final ConversationsPolicy conversationsPolicy; final ConversationsPolicy conversationsPolicy;
@ -115,6 +127,7 @@ class ServerConfig {
@required this.termsURL, @required this.termsURL,
@required this.playStoreURL, @required this.playStoreURL,
@required this.androidDirectDownloadURL, @required this.androidDirectDownloadURL,
@required this.notificationsPolicy,
@required this.passwordPolicy, @required this.passwordPolicy,
@required this.dataConservationPolicy, @required this.dataConservationPolicy,
@required this.conversationsPolicy, @required this.conversationsPolicy,
@ -122,6 +135,7 @@ class ServerConfig {
assert(termsURL != null), assert(termsURL != null),
assert(playStoreURL != null), assert(playStoreURL != null),
assert(androidDirectDownloadURL != null), assert(androidDirectDownloadURL != null),
assert(notificationsPolicy != null),
assert(passwordPolicy != null), assert(passwordPolicy != null),
assert(dataConservationPolicy != null), assert(dataConservationPolicy != null),
assert(conversationsPolicy != null); assert(conversationsPolicy != null);

View File

@ -0,0 +1,188 @@
import 'package:comunic/helpers/push_notifications_helper.dart';
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/config.dart';
import 'package:comunic/ui/widgets/async_screen_widget.dart';
import 'package:comunic/utils/flutter_utils.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/log_utils.dart';
import 'package:comunic/utils/ui_utils.dart';
import 'package:flutter/material.dart';
/// Push notifications configuration route
///
/// @author Pierre Hubert
Future<void> showInitialPushNotificationsConfiguration(
BuildContext context) async {
// Check if no notifications service are available
if (!srvConfig.notificationsPolicy.hasIndependent &&
(!isAndroid || !srvConfig.notificationsPolicy.hasFirebase)) return;
await Navigator.of(context).push(
MaterialPageRoute(builder: (c) => PushNotificationsConfigurationRoute()));
}
class PushNotificationsConfigurationRoute extends StatefulWidget {
@override
_PushNotificationsConfigurationRouteState createState() =>
_PushNotificationsConfigurationRouteState();
}
class _PushNotificationsConfigurationRouteState
extends State<PushNotificationsConfigurationRoute> {
PushNotificationsStatus currStatus;
bool get canSubmit =>
(currStatus ?? PushNotificationsStatus.UNDEFINED) !=
PushNotificationsStatus.UNDEFINED;
Future<void> _refresh() async {
await PushNotificationsHelper.refreshLocalStatus();
currStatus = await PushNotificationsHelper.getLocalStatus();
}
@override
Widget build(BuildContext context) {
return Material(
color: config().splashBackgroundColor,
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: AsyncScreenWidget(
onReload: _refresh,
onBuild: _buildForm,
errorMessage: tr("Failed to load push notifications settings!"),
),
),
));
}
Widget _buildForm() => ConstrainedBox(
constraints: BoxConstraints(maxWidth: 300),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(),
Icon(Icons.notifications_none),
Spacer(),
Text(
tr("Comunic can now send you notifications to your device when the application is closed if you want."),
textAlign: TextAlign.center,
),
Spacer(),
_NotificationOption(
title: tr("Use Google services"),
description: tr(
"Use the Google Play services to send you notifications. This option is less privacy-friendly, but it will work on most phones and will save your battery life."),
option: PushNotificationsStatus.FIREBASE,
current: currStatus,
available: srvConfig.notificationsPolicy.hasFirebase,
onChanged: (s) => setState(() => currStatus = s),
),
_NotificationOption(
title: tr("Use independent notifications service"),
description: tr(
"Configure Comunic to use your own self-hosted notifications service. This option is much more privacy-friendly, but it will drain your battery life."),
option: PushNotificationsStatus.INDEPENDENT,
current: currStatus,
available:
srvConfig.notificationsPolicy.hasIndependent && isAndroid,
onChanged: (s) => setState(() => currStatus = s),
),
_NotificationOption(
title: tr("Do not send notification"),
description: tr(
"Disable notifications services. You will always be able to change it from application settings."),
option: PushNotificationsStatus.DISABLED,
current: currStatus,
available: true,
onChanged: (s) => setState(() => currStatus = s),
),
Spacer(),
OutlinedButton(
onPressed: canSubmit ? _submit : null,
child: Text(tr("Configure").toUpperCase()),
style: OutlinedButton.styleFrom(primary: Colors.white)),
Spacer(),
],
),
);
Future<void> _submit() async {
try {
switch (currStatus) {
case PushNotificationsStatus.DISABLED:
break;
case PushNotificationsStatus.FIREBASE:
// TODO: Handle this case.
throw new Exception("Firebase not supported yet!");
break;
case PushNotificationsStatus.INDEPENDENT:
// TODO: Handle this case.
throw new Exception("Independent not supported yet!");
break;
default:
throw new Exception("Unknown status!");
}
await PushNotificationsHelper.clearLocalStatus();
await PushNotificationsHelper.setNewStatus(currStatus);
await PushNotificationsHelper.refreshLocalStatus();
Navigator.of(context).pop();
} catch (e, s) {
logError(e, s);
showAlert(
context: context,
message: tr("Failed to configure push notifications!"));
}
}
}
class _NotificationOption extends StatelessWidget {
final String title;
final String description;
final PushNotificationsStatus option;
final PushNotificationsStatus current;
final bool available;
final Function(PushNotificationsStatus) onChanged;
const _NotificationOption({
Key key,
@required this.title,
@required this.description,
@required this.option,
@required this.current,
@required this.available,
@required this.onChanged,
}) : assert(title != null),
assert(description != null),
assert(option != null),
assert(current != null),
assert(available != null),
assert(onChanged != null),
super(key: key);
@override
Widget build(BuildContext context) => !available
? Container()
: Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: ListTile(
leading: Radio(
value: option,
groupValue: current,
onChanged: onChanged,
),
title: Text(title),
subtitle: Text(description),
contentPadding: EdgeInsets.all(0),
onTap: () => onChanged(option),
),
);
}

View File

@ -1,13 +1,16 @@
import 'package:comunic/helpers/account_helper.dart'; import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/helpers/events_helper.dart'; import 'package:comunic/helpers/events_helper.dart';
import 'package:comunic/helpers/push_notifications_helper.dart';
import 'package:comunic/helpers/server_config_helper.dart'; import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/helpers/version_helper.dart'; import 'package:comunic/helpers/version_helper.dart';
import 'package:comunic/helpers/websocket_helper.dart'; import 'package:comunic/helpers/websocket_helper.dart';
import 'package:comunic/models/config.dart';
import 'package:comunic/ui/dialogs/deprecation_dialog.dart'; import 'package:comunic/ui/dialogs/deprecation_dialog.dart';
import 'package:comunic/ui/routes/login_route.dart'; import 'package:comunic/ui/routes/login_route.dart';
import 'package:comunic/ui/routes/main_route/main_route.dart'; import 'package:comunic/ui/routes/main_route/main_route.dart';
import 'package:comunic/ui/routes/main_route/smartphone_route.dart'; import 'package:comunic/ui/routes/main_route/smartphone_route.dart';
import 'package:comunic/ui/routes/main_route/tablet_route.dart'; import 'package:comunic/ui/routes/main_route/tablet_route.dart';
import 'package:comunic/ui/routes/push_notifications_route.dart';
import 'package:comunic/ui/widgets/safe_state.dart'; import 'package:comunic/ui/widgets/safe_state.dart';
import 'package:comunic/utils/flutter_utils.dart'; import 'package:comunic/utils/flutter_utils.dart';
import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/intl_utils.dart';
@ -74,6 +77,11 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
return; return;
} }
print("Check push notifications configuration...");
if (await PushNotificationsHelper.getLocalStatus() ==
PushNotificationsStatus.UNDEFINED)
await showInitialPushNotificationsConfiguration(context);
print("Attempting WebSocket connection..."); print("Attempting WebSocket connection...");
setState(() { setState(() {
@ -107,7 +115,7 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
/// Build loading widget /// Build loading widget
Widget _buildNonReadyWidget() { Widget _buildNonReadyWidget() {
return Scaffold( return Scaffold(
backgroundColor: Colors.indigo.shade900, backgroundColor: config().splashBackgroundColor,
body: Center( body: Center(
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,

View File

@ -1,6 +1,10 @@
import 'dart:io';
/// Flutter utilities /// Flutter utilities
/// ///
/// @author Pierre Hubert /// @author Pierre Hubert
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
bool get isWeb => kIsWeb; bool get isWeb => kIsWeb;
bool get isAndroid => !isWeb && Platform.isAndroid;