mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-12-26 12:58:51 +00:00
Start to integrate push notifications
This commit is contained in:
parent
38c639331f
commit
612fc7b0d9
@ -1,5 +1,6 @@
|
||||
import 'package:comunic/helpers/api_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/models/api_request.dart';
|
||||
import 'package:comunic/models/authentication_details.dart';
|
||||
@ -77,6 +78,8 @@ class AccountHelper {
|
||||
|
||||
/// Sign out user
|
||||
Future<void> signOut() async {
|
||||
await PushNotificationsHelper.clearLocalStatus();
|
||||
|
||||
await APIRequest.withLogin("account/logout").exec();
|
||||
|
||||
final preferencesHelper = await PreferencesHelper.getInstance();
|
||||
|
@ -13,6 +13,7 @@ enum PreferencesKeyList {
|
||||
ENABLE_DARK_THEME,
|
||||
FORCE_MOBILE_MODE,
|
||||
SHOW_PERFORMANCE_OVERLAY,
|
||||
PUSH_NOTIFICATIONS_STATUS,
|
||||
}
|
||||
|
||||
const _PreferenceKeysName = {
|
||||
@ -62,6 +63,10 @@ class PreferencesHelper {
|
||||
}
|
||||
}
|
||||
|
||||
bool containsKey(PreferencesKeyList key) {
|
||||
return _sharedPreferences.containsKey(_PreferenceKeysName[key]);
|
||||
}
|
||||
|
||||
Future<bool> removeKey(PreferencesKeyList key) async {
|
||||
return await _sharedPreferences.remove(_PreferenceKeysName[key]);
|
||||
}
|
||||
|
88
lib/helpers/push_notifications_helper.dart
Normal file
88
lib/helpers/push_notifications_helper.dart
Normal 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();
|
||||
}
|
@ -17,6 +17,7 @@ class ServerConfigurationHelper {
|
||||
(await APIRequest.withoutLogin("server/config").execWithThrow())
|
||||
.getObject();
|
||||
|
||||
final pushNotificationsPolicy = response["push_notifications"];
|
||||
final passwordPolicy = response["password_policy"];
|
||||
final dataConservationPolicy = response["data_conservation_policy"];
|
||||
final conversationsPolicy = response["conversations_policy"];
|
||||
@ -27,6 +28,10 @@ class ServerConfigurationHelper {
|
||||
termsURL: response["terms_url"],
|
||||
playStoreURL: response["play_store_url"],
|
||||
androidDirectDownloadURL: response["android_direct_download_url"],
|
||||
notificationsPolicy: NotificationsPolicy(
|
||||
hasFirebase: pushNotificationsPolicy["has_firebase"],
|
||||
hasIndependent: pushNotificationsPolicy["has_independent"],
|
||||
),
|
||||
passwordPolicy: PasswordPolicy(
|
||||
allowMailInPassword: passwordPolicy["allow_email_in_password"],
|
||||
allowNameInPassword: passwordPolicy["allow_name_in_password"],
|
||||
|
@ -1,25 +1,32 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Application configuration model
|
||||
///
|
||||
/// @author Pierre HUBERT
|
||||
|
||||
const defaultColor = Color(0xFF1A237E);
|
||||
|
||||
/// Configuration class
|
||||
class Config {
|
||||
final String apiServerName;
|
||||
final String apiServerUri;
|
||||
final bool apiServerSecure;
|
||||
final String clientName;
|
||||
final Color splashBackgroundColor;
|
||||
|
||||
const Config({
|
||||
@required this.apiServerName,
|
||||
@required this.apiServerUri,
|
||||
@required this.apiServerSecure,
|
||||
@required this.clientName,
|
||||
this.splashBackgroundColor = defaultColor,
|
||||
}) : assert(apiServerName != null),
|
||||
assert(apiServerUri != null),
|
||||
assert(apiServerSecure != null),
|
||||
assert(clientName != null);
|
||||
assert(clientName != null),
|
||||
assert(splashBackgroundColor != null);
|
||||
|
||||
/// Get and set static configuration
|
||||
static Config _config;
|
||||
|
@ -5,6 +5,17 @@ import 'package:version/version.dart';
|
||||
///
|
||||
/// @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 {
|
||||
final bool allowMailInPassword;
|
||||
final bool allowNameInPassword;
|
||||
@ -106,6 +117,7 @@ class ServerConfig {
|
||||
final String termsURL;
|
||||
final String playStoreURL;
|
||||
final String androidDirectDownloadURL;
|
||||
final NotificationsPolicy notificationsPolicy;
|
||||
final PasswordPolicy passwordPolicy;
|
||||
final ServerDataConservationPolicy dataConservationPolicy;
|
||||
final ConversationsPolicy conversationsPolicy;
|
||||
@ -115,6 +127,7 @@ class ServerConfig {
|
||||
@required this.termsURL,
|
||||
@required this.playStoreURL,
|
||||
@required this.androidDirectDownloadURL,
|
||||
@required this.notificationsPolicy,
|
||||
@required this.passwordPolicy,
|
||||
@required this.dataConservationPolicy,
|
||||
@required this.conversationsPolicy,
|
||||
@ -122,6 +135,7 @@ class ServerConfig {
|
||||
assert(termsURL != null),
|
||||
assert(playStoreURL != null),
|
||||
assert(androidDirectDownloadURL != null),
|
||||
assert(notificationsPolicy != null),
|
||||
assert(passwordPolicy != null),
|
||||
assert(dataConservationPolicy != null),
|
||||
assert(conversationsPolicy != null);
|
||||
|
188
lib/ui/routes/push_notifications_route.dart
Normal file
188
lib/ui/routes/push_notifications_route.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
import 'package:comunic/helpers/account_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/version_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/routes/login_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/tablet_route.dart';
|
||||
import 'package:comunic/ui/routes/push_notifications_route.dart';
|
||||
import 'package:comunic/ui/widgets/safe_state.dart';
|
||||
import 'package:comunic/utils/flutter_utils.dart';
|
||||
import 'package:comunic/utils/intl_utils.dart';
|
||||
@ -74,6 +77,11 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
|
||||
return;
|
||||
}
|
||||
|
||||
print("Check push notifications configuration...");
|
||||
if (await PushNotificationsHelper.getLocalStatus() ==
|
||||
PushNotificationsStatus.UNDEFINED)
|
||||
await showInitialPushNotificationsConfiguration(context);
|
||||
|
||||
print("Attempting WebSocket connection...");
|
||||
|
||||
setState(() {
|
||||
@ -107,7 +115,7 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
|
||||
/// Build loading widget
|
||||
Widget _buildNonReadyWidget() {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.indigo.shade900,
|
||||
backgroundColor: config().splashBackgroundColor,
|
||||
body: Center(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
|
@ -1,6 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
/// Flutter utilities
|
||||
///
|
||||
/// @author Pierre Hubert
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
bool get isWeb => kIsWeb;
|
||||
|
||||
bool get isAndroid => !isWeb && Platform.isAndroid;
|
Loading…
Reference in New Issue
Block a user