1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2025-07-01 14:13:29 +00:00

25 Commits
1.1.0 ... 1.1.1

Author SHA1 Message Date
1ee9a2c5cc Update build script 2021-02-20 12:32:03 +01:00
25a25e4c70 Update makefile 2021-02-20 12:12:19 +01:00
d4a0748249 Add missing french translations 2021-02-20 10:12:01 +01:00
1ea286f3ef Add download link on deprecation dialog 2021-02-20 09:59:21 +01:00
a32e968992 Terms of use are not hard coded anymore 2021-02-20 09:35:03 +01:00
0cd9371460 Show deprecation warning 2021-02-20 09:24:51 +01:00
1b0a3fd24b Add new build configuration 2021-02-20 09:03:17 +01:00
54e37b3e69 Display current build version on startup screen 2021-02-20 08:58:03 +01:00
2519adeef4 Fix typo 2021-02-20 08:41:56 +01:00
4f9001cb2b Remove useless configuration load checks 2021-02-19 17:38:12 +01:00
613ceadfaa Load configuration at application startup 2021-02-19 17:32:38 +01:00
459757b292 Fix issues with WebSocket 2021-02-18 19:36:51 +01:00
e399f71a78 Fix issue when WebSocket is closed 2021-02-18 19:11:50 +01:00
c5d1512375 Apply password policy on all forms 2021-02-18 18:58:47 +01:00
16ec9a8e00 Rename route with a more logical name 2021-02-18 18:28:57 +01:00
c19cbaac88 Fix invalid call 2021-02-18 18:27:24 +01:00
277c08048d Implement password policy for account creation 2021-02-18 18:20:50 +01:00
482e938744 Fix bad layout 2021-02-16 19:36:14 +01:00
1d0bd45632 Finish data conservation policy screen 2021-02-16 19:35:52 +01:00
3a39387365 Start to integrate data conservation policy 2021-02-16 19:30:06 +01:00
4d885affb9 Add Makefile 2021-02-13 17:13:09 +01:00
0ad8d5c393 Fix bad sign out order 2021-02-13 16:07:23 +01:00
e5ed4fadda Upgrade login system 2021-02-13 16:03:07 +01:00
581059cb1d Deprecate old movie system 2021-02-13 11:27:13 +01:00
0c526abfe8 Start to work on next version 2021-02-13 08:45:29 +01:00
37 changed files with 894 additions and 155 deletions

16
Makefile Normal file
View File

@ -0,0 +1,16 @@
beta_online_release:
flutter build apk --flavor beta -t lib/main_online.dart
beta_online_release_split_per_abi:
flutter build apk --flavor beta -t lib/main_online.dart --target-platform android-arm,android-arm64,android-x64 --split-per-abi
stable_release_split_per_abi:
flutter build apk --flavor stable -t lib/main_online.dart --target-platform android-arm,android-arm64,android-x64 --split-per-abi
stable_release:
flutter build apk --flavor stable -t lib/main_online.dart
.PHONY: beta_offline_release beta_online_release_split_per_abi stable_release_split_per_abi

View File

@ -13,9 +13,18 @@
"1 member": "1 membre",
"1 month": "1 mois",
"1 year": "1 an",
"10 years": "10 ans",
"15 days": "15 jours",
"2 years": "2 ans",
"3 months": "3 mois",
"5 years": "5 ans",
"50 years": "50 ans",
"6 months": "6 mois",
"7 days": "7 jours",
":yourShortcut:": ":votreRaccourcis:",
"A network error occured!": "Une erreur de réseau s'est produite !",
"A registration is required to access this group page.": "Une inscription est nécessaire pour accéder à cette page de groupe !",
"ALL": "TOUS",
"About this application": "A propos de cette application",
"Accept": "Accepter",
"Accept request": "Accepter la demande",
@ -41,6 +50,7 @@
"An error occured while creating your account. Please try again.": "Une erreur s'est produite lors de la création du compte. Veuillez ré-essayer.",
"An error occurred while checking your options !": "Erreur lors de la récupération de vos options de récupération !",
"An error occurred while checking your recovery options !": "Erreur lors de la récupération de vos options de récupération !",
"An error occurred while creating your account. Please try again.": "Erreur lors de la création de votre compte. Veuillez réessayer",
"Answer %num%": "Réponse %num%",
"Answer 1": "Réponse 1",
"Answer 2": "Réponse 2",
@ -49,6 +59,12 @@
"Appearance": "Apparence",
"Application settings": "Paramètres de l'application",
"Are you sure do you want to remove this friend from your list of friends ? A friendship request will have to be sent to get this user back to your list!": "Voulez-vous vraiment supprimer cet ami de votre liste d'amis ? Il faudra une demande d'ami pour réintégrer cet utilisateur à votre liste !",
"Automatically delete unread notifications after": "Supprimer automatiquement les notifications non lues après",
"Automatically delete your account if you have been inactive for": "Supprimer votre compte si vous avez été déconnecté pendant",
"Automatically delete your comments after": "Supprimer automatiquement vos commentaires après",
"Automatically delete your conversation messages after": "Supprimer automatiqumenet vos messages de conversations après",
"Automatically delete your likes after": "Supprimer automatiquement vos \"J'aime\" après",
"Automatically delete your posts after": "Supprimer automatiquement vos posts après",
"Block the creation of new responses": "Bloquer la création de nouvelles réponses",
"Camera": "Caméra",
"Can access to all group posts": "Peut accéder à tous les posts du groupe",
@ -181,6 +197,7 @@
"Custom emojis": "Emoticons personnalisés",
"Customize your account image": "Personalisez votre image de compte",
"Danger zone": "Zone de danger",
"Data conservation policy": "Politique de conservation des données",
"Debug features": "Fonctionnalités de développement",
"Delete": "Supprimer",
"Delete account image": "Supprimer l'image de compte",
@ -190,6 +207,7 @@
"Delete group": "Supprimer le groupe",
"Delete logo": "Supprimer le logo",
"Delete your account": "Supprimer votre compte",
"Deprecated application version": "Version obsolète de l'application",
"Disconnect all your devices": "Déconnecter tous vos appareils",
"Disconnect all your devices from Comunic, including the current one. Use this option if one of the device you use for Comunic was stolen.": "Déconnecte tous vos appareils de Comunic, en incluant l'appareil actuel. Nous vous recommandons d'utiliser cette option si vous avez des raisons de penser que l'un des appareils que vous utiliser pour accéder à Comunic a été volé.",
"Do you really want to block new choices creation?": "Voulez-vous vraiment bloquer la création de nouveaux choix ?",
@ -213,6 +231,7 @@
"Do you really want to remove this membership ?": "Voulez-vous vraiment supprimer cette inscription ?",
"Do you really want to sign out from the application ?": "Voulez-vous vraiment vous déconnecter de l'application ?",
"Do you want to unselected currently selected image ?": "Voulez-vous désélectionner l'image ?",
"Download update outside Play Store": "Télécharger la mise hors du Play Store",
"Email address": "Adresse e-mail",
"Email address...": "Adresse mail...",
"Enable dark theme": "Activer le thème sombre",
@ -222,6 +241,8 @@
"Everyone": "Tout le monde",
"Everyone can choose to join the group without moderator approval": "Tout le monde peut rejoindre le groupe, sans l'approbation d'un modérateur",
"Everyone can request a membership, but a moderator review the request": "Tout le monde peut demander à rejoindre le groupe, mais un modérateur doit accepter les demandes",
"Failed to load privacy settings!": "Erreur lors du chargement des paramètres de vie privée !",
"Failed to update data conservation policy!": "Echec de la mise à jour des paramètres de vie privée !",
"First name": "Prénom",
"Follow": "Suivre",
"Follow conversation": "Suivre la conversation",
@ -229,7 +250,7 @@
"Force mobile mode": "Forcer l'utilisation du mode mobile",
"Force the smartphone mode of the application to be used, even when tablet mode could be used.": "Forcer l'utilisation du mode smartphone de l'application, même lorsque le mode tablette est disponible.",
"Form can not be submitted at this point!": "Impossible de soumettre le formulaire à ce stade !",
"Free social network that respect your privacy": "Réseau sociale libre qui respecte votre vie privée",
"Free social network that respect your privacy": "Réseau social libre qui respecte votre vie privée",
"Friends": "Amis",
"Friends of %name%": "Amis de %name%",
"Friends only": "Amis seulement",
@ -238,6 +259,7 @@
"General settings": "Paramètres généraux",
"Generate a new random logo": "Générer un logo aléatoire",
"Generate a random account image": "Générer une image de compte aléatoire",
"Go to the Play Store": "Accéder au Play Store",
"Group": "Groupe",
"Group ID": "Identifiant du gorupe",
"Group URL (optional)": "URL du groupe (optionnelle)",
@ -293,6 +315,7 @@
"My friends only": "Mes amis uniquement",
"Name of the group": "Nom du groupe",
"Name of the group to create": "Nom du groupe à créer",
"Never": "Jamais",
"New choice": "Nouveau choix",
"New choice...": "Nouveau choix...",
"New comment...": "Nouveau commentaire...",
@ -391,6 +414,7 @@
"This account is private.": "Ce compte est privé.",
"This kind of notification is not supported yet by this application.": "Ce type de notification n'est pas encore supportée par l'application.",
"This password is not the same as the other one!": "Ce mot de passe est différent de l'autre",
"This version of the Comunic application is deprecated. You might still be able to use it, but some features may not work. We recommend you to update to the latest version of the application.": "Cette version de l'application Comunic est obsolète. Vous pouvez continuer à l'utiliser, mais certaines fonctionalités pourront ne plus fonctionner. Nous vous recommandons d'installer la dernière version de l'applicatioon.",
"This virtual directory is invalid / unvailable !": "Ce répertoire virtuel est invalide / indisponible !",
"Too many accounts have been created from this IP address for now. Please try again later.": "Trop de comptes ont été créés avec cette addresse IP pour l'instant. Veuillez ré-essayer plus tard.",
"Too many unsuccessfull login attempts! Please try again later...": "Trop de tentatives de connexion ont échoué. Veuillez ré-essayer plus tard...",
@ -407,7 +431,9 @@
"Upload a new logo": "Envoyer un nouveau logo",
"Upload an account image": "Envoyer une nouvelle image de compte",
"Upload new account image": "Changer l'image de compte",
"Use the old application anyway": "Utiliser l'ancienne version",
"User ID": "Numéro d'utilisateur",
"Version %version% - Build %build%": "Version %version% - Build %build%",
"Virtual directory": "Répertoire virtuel",
"Virtual directory (optional)": "Dossier virtuel (optionnel)",
"Visitor": "Visiteur",
@ -430,6 +456,11 @@
"Your new password": "Votre nouveau mot de passe",
"Your page settings": "Paramètres de votre page",
"Your password has been successfully changed!": "Votre mot de passe a été changé avec succès !",
"Your password must be composed of at least %num% characters!": "Votre mot de passe doit être composé d'au moins %num% caractères !",
"Your password must contains characters of at least %num% of the following categories : %upper% upper case letter, %lower% lowercase letter, %digit% digit, %special% special character.": "Votre mot de passe doit contenir des caractères d'au moins %num% des catégories suivants : %upper% lettre majuscule, %lower% lettre minuscule, %digit% chiffre, %special% caractères spéciaux.",
"Your password must not contains part of your email address!": "Votre mot de passe ne doit pas contenir des parties de votre adresse mail !",
"Your password must not contains your first name!": "Votre mot de passe ne doit pas contenir votre prénom !",
"Your password must not contains your last name!": "Votre mot de passe ne doit pas contenir votre nom !",
"Your response: %response%": "Votre réponse : %response%",
"Your security questions can be used to recover an access to your account when you loose your password...": "Vos questions de sécurité peuvent être utilisées pour récupérer l'accès à votre compte lorsque vous perdez votre mot de passe...",
"accepted his invitation to join the group": "a accepté son invitation à rejoindre le groupe",

View File

@ -2,13 +2,4 @@
///
/// @author Pierre HUBERT
enum PostKind {
TEXT,
IMAGE,
WEB_LINK,
PDF,
MOVIE,
COUNTDOWN,
SURVEY,
YOUTUBE
}
enum PostKind { TEXT, IMAGE, WEB_LINK, PDF, COUNTDOWN, SURVEY, YOUTUBE }

View File

@ -3,9 +3,8 @@ import 'package:comunic/helpers/preferences_helper.dart';
import 'package:comunic/helpers/websocket_helper.dart';
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/authentication_details.dart';
import 'package:comunic/models/login_tokens.dart';
import 'package:comunic/models/new_account.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:comunic/models/res_check_password_reset_token.dart';
/// Account helper
///
@ -26,8 +25,6 @@ enum CreateAccountResult {
}
class AccountHelper {
static const _USER_ID_PREFERENCE_NAME = "user_id";
// Current user ID
static int _currentUserID = -1;
@ -36,7 +33,7 @@ class AccountHelper {
/// Warning : This method MUST BE CALLED AT LEAST ONCE AFTER APP START !!!
Future<bool> signedIn() async {
bool signedIn =
(await PreferencesHelper.getInstance()).getLoginTokens() != null;
(await PreferencesHelper.getInstance()).getLoginToken() != null;
// Load current user ID for later use
if (signedIn && _currentUserID == -1) await _loadCurrentUserID();
@ -47,8 +44,8 @@ class AccountHelper {
/// Sign in user
Future<AuthResult> signIn(AuthenticationDetails auth) async {
final request = APIRequest(uri: "account/login");
request.addString("userMail", auth.email);
request.addString("userPassword", auth.password);
request.addString("mail", auth.email);
request.addString("password", auth.password);
final response = await APIHelper().exec(request);
@ -59,10 +56,9 @@ class AccountHelper {
return AuthResult.TOO_MANY_ATTEMPTS;
else if (response.code != 200) return AuthResult.NETWORK_ERROR;
// Save login tokens
final tokensObj = response.getObject()["tokens"];
// Save login token
await (await PreferencesHelper.getInstance())
.setLoginTokens(LoginTokens(tokensObj["token1"], tokensObj["token2"]));
.setLoginToken(response.getObject()["token"]);
// Get current user ID
final userID = await _downloadCurrentUserID();
@ -72,8 +68,8 @@ class AccountHelper {
}
// Save current user ID
final preferences = await SharedPreferences.getInstance();
await preferences.setInt(_USER_ID_PREFERENCE_NAME, userID);
final preferences = await PreferencesHelper.getInstance();
await preferences.setInt(PreferencesKeyList.USER_ID, userID);
_currentUserID = userID;
return AuthResult.SUCCESS;
@ -81,7 +77,11 @@ class AccountHelper {
/// Sign out user
Future<void> signOut() async {
await (await PreferencesHelper.getInstance()).setLoginTokens(null);
await APIRequest.withLogin("account/logout").exec();
final preferencesHelper = await PreferencesHelper.getInstance();
await preferencesHelper.setLoginToken(null);
await preferencesHelper.setInt(PreferencesKeyList.USER_ID, -1);
_currentUserID = 0;
// Close current web socket
@ -125,6 +125,11 @@ class AccountHelper {
.execWithThrow())
.getObject()["exists"];
/// Get current user email address
static Future<String> getCurrentAccountEmailAddress() async =>
(await APIRequest.withLogin("account/mail")
.execWithThrowGetObject())["mail"];
/// Check out whether security questions have been set for an account or not
///
/// Throws in case of failure
@ -161,10 +166,19 @@ class AccountHelper {
/// Check a password reset token
///
/// Throws in case failure
static Future<void> validatePasswordResetToken(String token) async =>
await APIRequest.withoutLogin("account/check_password_reset_token")
.addString("token", token)
.execWithThrow();
static Future<ResCheckPasswordToken> validatePasswordResetToken(
String token) async {
final response =
await APIRequest.withoutLogin("account/check_password_reset_token")
.addString("token", token)
.execWithThrowGetObject();
return ResCheckPasswordToken(
firstName: response["first_name"],
lastName: response["last_name"],
email: response["mail"],
);
}
/// Change account password using password reset token
///
@ -178,10 +192,7 @@ class AccountHelper {
/// Get current user ID from the server
Future<int> _downloadCurrentUserID() async {
final response = await APIRequest(
uri: "user/getCurrentUserID",
needLogin: true,
).exec();
final response = await APIRequest.withLogin("account/id").exec();
if (response.code != 200) return null;
@ -190,8 +201,8 @@ class AccountHelper {
/// Get the ID of the currently signed in user
Future<void> _loadCurrentUserID() async {
final preferences = await SharedPreferences.getInstance();
_currentUserID = preferences.getInt(_USER_ID_PREFERENCE_NAME);
final preferences = await PreferencesHelper.getInstance();
_currentUserID = preferences.getInt(PreferencesKeyList.USER_ID);
}
/// Check if current user ID is loaded or not

View File

@ -19,15 +19,13 @@ class APIHelper {
Future<APIResponse> exec(APIRequest request, {bool multipart = false}) async {
try {
//Add API tokens
request.addString("serviceName", config().serviceName);
request.addString("serviceToken", config().serviceToken);
request.addString("client", config().clientName);
//Add user tokens (if required)
if (request.needLogin) {
final tokens = (await PreferencesHelper.getInstance()).getLoginTokens();
assert(tokens != null);
request.addString("userToken1", tokens.tokenOne);
request.addString("userToken2", tokens.tokenTwo);
final token = (await PreferencesHelper.getInstance()).getLoginToken();
assert(token != null);
request.addString("token", token);
}
// Determine server URL

View File

@ -16,7 +16,6 @@ const _NotificationElementTypeAPImapping = {
"post_text": NotificationElementType.POST_TEXT,
"post_img": NotificationElementType.POST_IMAGE,
"post_youtube": NotificationElementType.POST_YOUTUBE,
"post_movie": NotificationElementType.POST_MOVIE,
"post_weblink": NotificationElementType.POST_WEBLINK,
"post_pdf": NotificationElementType.POST_PDF,
"post_timer": NotificationElementType.POST_TIMER,

View File

@ -29,7 +29,6 @@ const _APIPostsKindsMap = {
"image": PostKind.IMAGE,
"weblink": PostKind.WEB_LINK,
"pdf": PostKind.PDF,
"movie": PostKind.MOVIE,
"countdown": PostKind.COUNTDOWN,
"survey": PostKind.SURVEY,
"youtube": PostKind.YOUTUBE

View File

@ -1,7 +1,4 @@
import 'dart:convert';
import 'package:comunic/models/application_preferences.dart';
import 'package:comunic/models/login_tokens.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Preferences helper
@ -11,14 +8,16 @@ import 'package:shared_preferences/shared_preferences.dart';
/// @author Pierre HUBERT
enum PreferencesKeyList {
LOGIN_TOKENS,
USER_ID,
LOGIN_TOKEN,
ENABLE_DARK_THEME,
FORCE_MOBILE_MODE,
SHOW_PERFORMANCE_OVERLAY,
}
const _PreferenceKeysName = {
PreferencesKeyList.LOGIN_TOKENS: "login_tokens",
PreferencesKeyList.USER_ID: "user_id",
PreferencesKeyList.LOGIN_TOKEN: "login_token",
PreferencesKeyList.ENABLE_DARK_THEME: "dark_theme",
PreferencesKeyList.FORCE_MOBILE_MODE: "force_mobile_mode",
PreferencesKeyList.SHOW_PERFORMANCE_OVERLAY: "perfs_overlay",
@ -45,23 +44,28 @@ class PreferencesHelper {
}
/// Set new login tokens
Future<void> setLoginTokens(LoginTokens tokens) async {
await setString(PreferencesKeyList.LOGIN_TOKENS,
tokens == null ? "null" : tokens.toString());
Future<void> setLoginToken(String token) async {
if (token != null)
await setString(PreferencesKeyList.LOGIN_TOKEN, token);
else
await removeKey(PreferencesKeyList.LOGIN_TOKEN);
}
/// Get current [LoginTokens]. Returns null if none or in case of failure
LoginTokens getLoginTokens() {
String getLoginToken() {
try {
final string = getString(PreferencesKeyList.LOGIN_TOKENS);
if (string == null || string == "null") return null;
return LoginTokens.fromJSON(jsonDecode(string));
final string = getString(PreferencesKeyList.LOGIN_TOKEN);
return string;
} on Exception catch (e) {
print(e.toString());
return null;
}
}
Future<bool> removeKey(PreferencesKeyList key) async {
return await _sharedPreferences.remove(_PreferenceKeysName[key]);
}
Future<bool> setString(PreferencesKeyList key, String value) async {
return await _sharedPreferences.setString(_PreferenceKeysName[key], value);
}
@ -74,6 +78,14 @@ class PreferencesHelper {
return await _sharedPreferences.setBool(_PreferenceKeysName[key], value);
}
Future<bool> setInt(PreferencesKeyList key, int value) async {
return await _sharedPreferences.setInt(_PreferenceKeysName[key], value);
}
int getInt(PreferencesKeyList key) {
return _sharedPreferences.getInt(_PreferenceKeysName[key]);
}
bool getBool(PreferencesKeyList key, {bool alternative = false}) {
final v = _sharedPreferences.getBool(_PreferenceKeysName[key]);
return v == null ? alternative : v;

View File

@ -0,0 +1,64 @@
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/server_config.dart';
import 'package:version/version.dart';
/// Server configuration helper
///
/// @author Pierre Hubert
class ServerConfigurationHelper {
static ServerConfig _config;
/// Make sure the configuration has been correctly loaded
static Future<void> ensureLoaded() async {
if (_config != null) return;
final response =
(await APIRequest.withoutLogin("server/config").execWithThrow())
.getObject();
final passwordPolicy = response["password_policy"];
final dataConservationPolicy = response["data_conservation_policy"];
_config = ServerConfig(
minSupportedMobileVersion:
Version.parse(response["min_supported_mobile_version"]),
termsURL: response["terms_url"],
playStoreURL: response["play_store_url"],
androidDirectDownloadURL: response["android_direct_download_url"],
passwordPolicy: PasswordPolicy(
allowMailInPassword: passwordPolicy["allow_email_in_password"],
allowNameInPassword: passwordPolicy["allow_name_in_password"],
minPasswordLength: passwordPolicy["min_password_length"],
minNumberUpperCaseLetters:
passwordPolicy["min_number_upper_case_letters"],
minNumberLowerCaseLetters:
passwordPolicy["min_number_lower_case_letters"],
minNumberDigits: passwordPolicy["min_number_digits"],
minNumberSpecialCharacters:
passwordPolicy["min_number_special_characters"],
minCategoriesPresence: passwordPolicy["min_categories_presence"],
),
dataConservationPolicy: ServerDataConservationPolicy(
minInactiveAccountLifetime:
dataConservationPolicy["min_inactive_account_lifetime"],
minNotificationLifetime:
dataConservationPolicy["min_notification_lifetime"],
minCommentsLifetime: dataConservationPolicy["min_comments_lifetime"],
minPostsLifetime: dataConservationPolicy["min_posts_lifetime"],
minConversationMessagesLifetime:
dataConservationPolicy["min_conversation_messages_lifetime"],
minLikesLifetime: dataConservationPolicy["min_likes_lifetime"],
),
);
}
/// Get current server configuration, throwing if it is not loaded yet
static ServerConfig get config {
if (_config == null)
throw Exception(
"Trying to access server configuration but it is not loaded yet!");
return _config;
}
}

View File

@ -1,6 +1,7 @@
import 'package:comunic/enums/user_page_visibility.dart';
import 'package:comunic/models/account_image_settings.dart';
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/data_conservation_policy_settings.dart';
import 'package:comunic/models/general_settings.dart';
import 'package:comunic/models/new_emoji.dart';
import 'package:comunic/models/security_settings.dart';
@ -193,4 +194,42 @@ class SettingsHelper {
.addString("security_answer_2", newSettings.securityAnswer2)
.execWithThrow();
}
/// Get account data conservation policy settings
///
/// Throws in case of failure
static Future<DataConservationPolicySettings>
getDataConservationPolicy() async {
final response =
(await APIRequest.withLogin("settings/get_data_conservation_policy")
.execWithThrow())
.getObject();
return DataConservationPolicySettings(
inactiveAccountLifeTime: response["inactive_account_lifetime"],
notificationLifetime: response["notification_lifetime"],
commentsLifetime: response["comments_lifetime"],
postsLifetime: response["posts_lifetime"],
conversationMessagesLifetime:
response["conversation_messages_lifetime"],
likesLifetime: response["likes_lifetime"]);
}
/// Apply new data conservation policy settings
///
/// Throws in case of failure
static Future<void> setDataConservationPolicy(
String password, DataConservationPolicySettings newSettings) async {
await APIRequest(uri: "settings/set_data_conservation_policy", needLogin: true)
.addString("password", password)
.addInt("inactive_account_lifetime",
newSettings.inactiveAccountLifeTime ?? 0)
.addInt("notification_lifetime", newSettings.notificationLifetime ?? 0)
.addInt("comments_lifetime", newSettings.commentsLifetime ?? 0)
.addInt("posts_lifetime", newSettings.postsLifetime ?? 0)
.addInt("conversation_messages_lifetime",
newSettings.conversationMessagesLifetime ?? 0)
.addInt("likes_lifetime", newSettings.likesLifetime ?? 0)
.execWithThrow();
}
}

View File

@ -0,0 +1,20 @@
import 'package:package_info/package_info.dart';
import 'package:version/version.dart';
/// Application version helper
///
/// @author Pierre Hubert
class VersionHelper {
static PackageInfo _info;
static Future<void> ensureLoaded() async {
_info = await PackageInfo.fromPlatform();
}
/// Get current version information
static PackageInfo get info => _info;
/// Get current application version, in parsed format
static Version get version => Version.parse(info.version);
}

View File

@ -67,6 +67,7 @@ class WebSocketHelper {
// Clear Futures queue
_requests.clear();
_ws = null;
EventsHelper.emit(WSClosedEvent());
},
);

View File

@ -1,7 +1,7 @@
import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/helpers/database/database_helper.dart';
import 'package:comunic/helpers/preferences_helper.dart';
import 'package:comunic/ui/routes/login_route.dart';
import 'package:comunic/helpers/version_helper.dart';
import 'package:comunic/ui/widgets/init_widget.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:flutter/material.dart';
@ -13,6 +13,9 @@ import 'package:flutter/material.dart';
void subMain() async {
WidgetsFlutterBinding.ensureInitialized();
// Load package information
await VersionHelper.ensureLoaded();
// Connect to database
await DatabaseHelper.open();
await DatabaseHelper.cleanUpDatabase();
@ -51,7 +54,7 @@ class ComunicApplicationState extends State<ComunicApplication> {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: AccountHelper.isUserIDLoaded ? InitializeWidget() : LoginRoute(),
home: InitializeWidget(),
theme: prefs.enableDarkMode ? ThemeData.dark() : null,
showPerformanceOverlay: prefs.showPerformancesOverlay,
);

View File

@ -23,9 +23,7 @@ void main() {
apiServerName: "192.168.1.9:3000",
apiServerUri: "/",
apiServerSecure: false,
serviceName: "ComunicFlutter",
serviceToken: "G9sZCBmb3IgVWJ1bnR1CkNvbW1lbnRbbmVdPeCkieCkrOCkq",
termsOfServicesURL: "http://devweb.local/comunic/current/about.php?cgu",
clientName: "ComunicFlutter",
));
HttpOverrides.global = new MyHttpOverride();

View File

@ -10,9 +10,7 @@ void main() {
apiServerName: "api.communiquons.org",
apiServerUri: "/",
apiServerSecure: true,
serviceName: "ComunicFlutter",
serviceToken: "9KfSwmB76U9UUwjXngDG7PeYccNfy",
termsOfServicesURL: "https://about.communiquons.org/about/terms/",
clientName: "ComunicFlutter",
));
subMain();

View File

@ -88,6 +88,10 @@ class APIRequest {
/// Execute the request, throws an exception in case of failure
Future<APIResponse> execWithThrow() async => (await exec()).assertOk();
/// Execute the request, throws an exception in case of failure
Future<Map<String, dynamic>> execWithThrowGetObject() async =>
(await execWithThrow()).getObject();
/// Execute the request with files
Future<APIResponse> execWithFiles() async => APIHelper().execWithFiles(this);

View File

@ -9,23 +9,17 @@ class Config {
final String apiServerName;
final String apiServerUri;
final bool apiServerSecure;
final String serviceName;
final String serviceToken;
final String termsOfServicesURL;
final String clientName;
const Config({
@required this.apiServerName,
@required this.apiServerUri,
@required this.apiServerSecure,
@required this.serviceName,
@required this.serviceToken,
@required this.termsOfServicesURL,
@required this.clientName,
}) : assert(apiServerName != null),
assert(apiServerUri != null),
assert(apiServerSecure != null),
assert(serviceName != null),
assert(serviceToken != null),
assert(termsOfServicesURL != null);
assert(clientName != null);
/// Get and set static configuration
static Config _config;

View File

@ -0,0 +1,21 @@
/// Data conservation policy settings
///
/// @author Pierre Hubert
class DataConservationPolicySettings {
int inactiveAccountLifeTime;
int notificationLifetime;
int commentsLifetime;
int postsLifetime;
int conversationMessagesLifetime;
int likesLifetime;
DataConservationPolicySettings({
this.inactiveAccountLifeTime,
this.notificationLifetime,
this.commentsLifetime,
this.postsLifetime,
this.conversationMessagesLifetime,
this.likesLifetime,
});
}

View File

@ -1,23 +0,0 @@
import 'dart:convert';
/// Login tokens model
///
/// @author Pierre HUBERT
class LoginTokens {
final String tokenOne;
final String tokenTwo;
const LoginTokens(this.tokenOne, this.tokenTwo)
: assert(tokenOne != null),
assert(tokenTwo != null);
LoginTokens.fromJSON(Map<String, dynamic> json)
: tokenOne = json["token_one"],
tokenTwo = json["token_two"];
@override
String toString() {
return jsonEncode({"token_one": tokenOne, "token_two": tokenTwo});
}
}

View File

@ -13,7 +13,6 @@ enum NotificationElementType {
POST_TEXT,
POST_IMAGE,
POST_YOUTUBE,
POST_MOVIE,
POST_WEBLINK,
POST_PDF,
POST_TIMER,

View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
/// Check password reset token result
///
/// @author Pierre Hubert
class ResCheckPasswordToken {
final String firstName;
final String lastName;
final String email;
const ResCheckPasswordToken({
@required this.firstName,
@required this.lastName,
@required this.email,
}) : assert(firstName != null),
assert(lastName != null),
assert(email != null);
}

View File

@ -0,0 +1,81 @@
import 'package:flutter/widgets.dart';
import 'package:version/version.dart';
/// Server static configuration
///
/// @author Pierre Hubert
class PasswordPolicy {
final bool allowMailInPassword;
final bool allowNameInPassword;
final int minPasswordLength;
final int minNumberUpperCaseLetters;
final int minNumberLowerCaseLetters;
final int minNumberDigits;
final int minNumberSpecialCharacters;
final int minCategoriesPresence;
const PasswordPolicy({
@required this.allowMailInPassword,
@required this.allowNameInPassword,
@required this.minPasswordLength,
@required this.minNumberUpperCaseLetters,
@required this.minNumberLowerCaseLetters,
@required this.minNumberDigits,
@required this.minNumberSpecialCharacters,
@required this.minCategoriesPresence,
}) : assert(allowMailInPassword != null),
assert(allowNameInPassword != null),
assert(minPasswordLength != null),
assert(minNumberUpperCaseLetters != null),
assert(minNumberLowerCaseLetters != null),
assert(minNumberDigits != null),
assert(minNumberSpecialCharacters != null),
assert(minCategoriesPresence != null);
}
class ServerDataConservationPolicy {
final int minInactiveAccountLifetime;
final int minNotificationLifetime;
final int minCommentsLifetime;
final int minPostsLifetime;
final int minConversationMessagesLifetime;
final int minLikesLifetime;
const ServerDataConservationPolicy({
@required this.minInactiveAccountLifetime,
@required this.minNotificationLifetime,
@required this.minCommentsLifetime,
@required this.minPostsLifetime,
@required this.minConversationMessagesLifetime,
@required this.minLikesLifetime,
}) : assert(minInactiveAccountLifetime != null),
assert(minNotificationLifetime != null),
assert(minCommentsLifetime != null),
assert(minPostsLifetime != null),
assert(minConversationMessagesLifetime != null),
assert(minLikesLifetime != null);
}
class ServerConfig {
final Version minSupportedMobileVersion;
final String termsURL;
final String playStoreURL;
final String androidDirectDownloadURL;
final PasswordPolicy passwordPolicy;
final ServerDataConservationPolicy dataConservationPolicy;
const ServerConfig({
@required this.minSupportedMobileVersion,
@required this.termsURL,
@required this.playStoreURL,
@required this.androidDirectDownloadURL,
@required this.passwordPolicy,
@required this.dataConservationPolicy,
}) : assert(minSupportedMobileVersion != null),
assert(termsURL != null),
assert(playStoreURL != null),
assert(androidDirectDownloadURL != null),
assert(passwordPolicy != null),
assert(dataConservationPolicy != null);
}

View File

@ -0,0 +1,40 @@
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
/// Deprecation dialog
///
/// @author Pierre Hubert
/// Show a dialog to warn the user this version of the application is deprecated
Future<void> showDeprecationDialog(BuildContext context) async {
await showDialog(context: context, builder: (c) => _DeprecationDialog());
}
class _DeprecationDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(tr("Deprecated application version")),
content: Text(tr(
"This version of the Comunic application is deprecated. You might still be able to use it, but some features may not work. We recommend you to update to the latest version of the application.")),
actions: [
MaterialButton(
onPressed: () =>
launch(ServerConfigurationHelper.config.playStoreURL),
child: Text(tr("Go to the Play Store")),
),
MaterialButton(
onPressed: () =>
launch(ServerConfigurationHelper.config.androidDirectDownloadURL),
child: Text(tr("Download update outside Play Store")),
),
MaterialButton(
onPressed: () => Navigator.pop(context),
child: Text(tr("Use the old application anyway")),
)
],
);
}
}

View File

@ -1,5 +1,5 @@
import 'package:comunic/ui/widgets/dialogs/auto_sized_dialog_content_widget.dart';
import 'package:comunic/utils/input_utils.dart';
import 'package:comunic/ui/widgets/new_password_input_widget.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:flutter/material.dart';
@ -8,27 +8,44 @@ import 'package:flutter/material.dart';
/// @author Pierre HUBERT
/// Ask the user to enter a new password
Future<String> showInputNewPassword(BuildContext context) async {
Future<String> showInputNewPassword({
@required BuildContext context,
@required UserInfoForPassword userInfo,
}) async {
assert(context != null);
assert(userInfo != null);
return await showDialog(
context: context, builder: (c) => _InputNewPasswordDialog());
context: context,
builder: (c) => _InputNewPasswordDialog(
userInfo: userInfo,
));
}
class _InputNewPasswordDialog extends StatefulWidget {
final UserInfoForPassword userInfo;
const _InputNewPasswordDialog({
Key key,
@required this.userInfo,
}) : assert(userInfo != null),
super(key: key);
@override
__InputNewPasswordDialogState createState() =>
__InputNewPasswordDialogState();
}
class __InputNewPasswordDialogState extends State<_InputNewPasswordDialog> {
final _controller1 = TextEditingController();
final _controller1 = GlobalKey<NewPasswordInputWidgetState>();
final _controller2 = TextEditingController();
final _focusScopeNode = FocusScopeNode();
String get _password => _controller1.text;
String get _password => _controller1.currentState.value;
bool get _input1Valid => validatePassword(_password);
bool get _input1Valid =>
_controller1.currentState != null && _controller1.currentState.valid;
bool get _input2Valid => _controller1.text == _controller2.text;
bool get _input2Valid => _controller1.currentState.value == _controller2.text;
bool get _isValid => _input1Valid && _input2Valid;
@ -65,12 +82,10 @@ class __InputNewPasswordDialogState extends State<_InputNewPasswordDialog> {
child: Column(
children: <Widget>[
// Input 1
_buildPasswordField(
controller: _controller1,
NewPasswordInputWidget(
key: _controller1,
user: widget.userInfo,
label: tr("Your new password"),
errorText: _controller1.text.isNotEmpty && !_input1Valid
? tr("Invalid password!")
: null,
textInputAction: TextInputAction.next,
onSubmitted: () => _focusScopeNode.nextFocus(),
),

View File

@ -30,7 +30,7 @@ class __InputUserPasswordDialogState
String get _currPass => _controller.text;
bool get _canSubmit =>
validatePassword(_controller.text) && _status != _Status.CHECKING;
legacyValidatePassword(_controller.text) && _status != _Status.CHECKING;
void _setStatus(_Status s) => setState(() => _status = s);

View File

@ -1,6 +1,7 @@
import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/models/config.dart';
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/new_account.dart';
import 'package:comunic/ui/widgets/new_password_input_widget.dart';
import 'package:comunic/utils/input_utils.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart';
@ -34,7 +35,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _passwordInputKey = GlobalKey<NewPasswordInputWidgetState>();
final _verifyPasswordController = TextEditingController();
bool _acceptedTOS = false;
@ -49,10 +50,11 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
bool get _isEmailValid => validateEmail(_emailController.text);
bool get _isPasswordValid => _passwordController.text.length > 3;
bool get _isPasswordValid => _passwordInputKey.currentState.valid;
bool get _isPasswordConfirmationValid =>
_passwordController.text == _verifyPasswordController.text;
_passwordInputKey.currentState != null &&
_passwordInputKey.currentState.value == _verifyPasswordController.text;
bool get _isFormValid =>
_isFirstNameValid &&
@ -69,7 +71,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
? tr(
"Too many accounts have been created from this IP address for now. Please try again later.")
: tr(
"An error occured while creating your account. Please try again.");
"An error occurred while creating your account. Please try again.");
@override
Widget build(BuildContext context) {
@ -124,15 +126,16 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
),
// Password
_InputEntry(
controller: _passwordController,
NewPasswordInputWidget(
key: _passwordInputKey,
label: tr("Password"),
onEdited: _updateUI,
icon: Icon(Icons.lock),
isPassword: true,
error: _showErrors && !_isPasswordValid
? tr("Invalid password!")
: null,
user: UserInfoForPassword(
firstName: _firstNameController.text,
lastName: _lastNameController.text,
email: _emailController.text,
),
),
// Verify password
@ -206,7 +209,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
firstName: _firstNameController.text,
lastName: _lastNameController.text,
email: _emailController.text,
password: _passwordController.text,
password: _passwordInputKey.currentState.value,
));
setState(() {
@ -223,9 +226,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
_accountCreated();
}
void _openTOS() {
launch(config().termsOfServicesURL);
}
void _openTOS() => launch(ServerConfigurationHelper.config.termsURL);
void _showCreateAccountError() async {
await showCupertinoDialog(

View File

@ -13,7 +13,7 @@ import 'package:flutter/rendering.dart';
///
/// @author Pierre Hubert
class ResetPasswordRoute extends StatelessWidget {
class ForgotPasswordRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
@ -133,8 +133,8 @@ class _ResetPasswordBodyState extends SafeState<_ResetPasswordBody> {
_setLoading(false);
} catch (e, s) {
print("Could not check given email! $e\n$s");
showSimpleSnack(
context, tr("An error occurred while checking your recovery options !"));
showSimpleSnack(context,
tr("An error occurred while checking your recovery options !"));
_setLoading(false);
}
}

View File

@ -1,7 +1,7 @@
import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/models/authentication_details.dart';
import 'package:comunic/ui/routes/create_account_route.dart';
import 'package:comunic/ui/routes/reset_password_route.dart';
import 'package:comunic/ui/routes/forgot_password_route.dart';
import 'package:comunic/ui/widgets/init_widget.dart';
import 'package:comunic/ui/widgets/login_scaffold.dart';
import 'package:comunic/utils/input_utils.dart';
@ -69,7 +69,7 @@ class _LoginRouteState extends State<LoginRoute> {
void _openResetPasswordPage() {
Navigator.of(context)
.push(MaterialPageRoute(builder: (c) => ResetPasswordRoute()));
.push(MaterialPageRoute(builder: (c) => ForgotPasswordRoute()));
}
/// Build error card

View File

@ -1,6 +1,5 @@
import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/ui/routes/conversation_route.dart';
import 'package:comunic/ui/routes/login_route.dart';
import 'package:comunic/ui/routes/main_route/page_info.dart';
import 'package:comunic/ui/routes/settings/account_settings_route.dart';
import 'package:comunic/ui/screens/call_screen.dart';
@ -170,10 +169,6 @@ abstract class MainController extends State<MainRoute> {
popUntilMainRoute();
await AccountHelper().signOut();
Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) {
return LoginRoute();
}));
}
/// Pop current page. Last page can not be popped

View File

@ -1,6 +1,8 @@
import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/models/res_check_password_reset_token.dart';
import 'package:comunic/ui/dialogs/input_new_password_dialog.dart';
import 'package:comunic/ui/widgets/async_screen_widget.dart';
import 'package:comunic/ui/widgets/new_password_input_widget.dart';
import 'package:comunic/ui/widgets/safe_state.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart';
@ -47,12 +49,13 @@ class __PasswordResetBodyState extends SafeState<_PasswordResetBody> {
final _key = GlobalKey<AsyncScreenWidgetState>();
var _status = _Status.BEFORE_CHANGE;
ResCheckPasswordToken _tokenInfo;
void _setStatus(_Status s) => setState(() => _status = s);
Future<void> _validateToken() async {
_status = _Status.BEFORE_CHANGE;
await AccountHelper.validatePasswordResetToken(widget.token);
_tokenInfo = await AccountHelper.validatePasswordResetToken(widget.token);
}
@override
@ -108,7 +111,14 @@ class __PasswordResetBodyState extends SafeState<_PasswordResetBody> {
void _changePassword() async {
try {
// Ask for new password
final newPass = await showInputNewPassword(context);
final newPass = await showInputNewPassword(
context: context,
userInfo: UserInfoForPassword(
firstName: _tokenInfo.firstName,
lastName: _tokenInfo.lastName,
email: _tokenInfo.email,
),
);
if (newPass == null) return;
_setStatus(_Status.WHILE_CHANGE);

View File

@ -1,6 +1,13 @@
import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/helpers/settings_helper.dart';
import 'package:comunic/models/data_conservation_policy_settings.dart';
import 'package:comunic/models/server_config.dart';
import 'package:comunic/ui/dialogs/input_user_password_dialog.dart';
import 'package:comunic/ui/dialogs/multi_choices_dialog.dart';
import 'package:comunic/ui/widgets/async_screen_widget.dart';
import 'package:comunic/ui/widgets/settings/header_spacer_section.dart';
import 'package:comunic/ui/widgets/settings/multi_choices_settings_tile.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart';
import 'package:flutter/cupertino.dart';
@ -17,19 +24,117 @@ class AccountPrivacySettings extends StatefulWidget {
}
class _AccountPrivacySettingsState extends State<AccountPrivacySettings> {
final _key = GlobalKey<AsyncScreenWidgetState>();
ServerConfig _serverConfig;
DataConservationPolicySettings _userSettings;
String _cachedPassword;
Future<void> _loadSettings() async {
_serverConfig = ServerConfigurationHelper.config;
_userSettings = await SettingsHelper.getDataConservationPolicy();
}
@override
Widget build(BuildContext context) {
return SettingsList(sections: [
HeadSpacerSection(),
SettingsSection(title: tr("Privacy settings"), tiles: [
SettingsTile(
title: tr("Delete your account"),
subtitle:
tr("Permanently delete your account and all data related to it."),
onPressed: (_) => _deleteAccount(),
)
])
]);
return AsyncScreenWidget(
key: _key,
onReload: _loadSettings,
onBuild: () => SettingsList(sections: [
HeadSpacerSection(),
SettingsSection(
title: tr("Data conservation policy"),
tiles: _dataConservationPolicyTiles,
),
HeadSpacerSection(),
HeadSpacerSection(),
SettingsSection(title: tr("Danger zone"), tiles: [
SettingsTile(
title: tr("Delete your account"),
subtitle: tr(
"Permanently delete your account and all data related to it."),
onPressed: (_) => _deleteAccount(),
)
])
]),
errorMessage: tr("Failed to load privacy settings!"),
);
}
List<SettingsTile> get _dataConservationPolicyTiles => [
DataConservationPolicyTile(
value: _userSettings.notificationLifetime,
title: tr("Automatically delete unread notifications after"),
onChange: (val) {
_userSettings.notificationLifetime = val;
_updateDataConservationPolicy();
},
minValue:
_serverConfig.dataConservationPolicy.minNotificationLifetime,
),
DataConservationPolicyTile(
value: _userSettings.commentsLifetime,
title: tr("Automatically delete your comments after"),
onChange: (val) {
_userSettings.commentsLifetime = val;
_updateDataConservationPolicy();
},
minValue: _serverConfig.dataConservationPolicy.minCommentsLifetime,
),
DataConservationPolicyTile(
value: _userSettings.postsLifetime,
title: tr("Automatically delete your posts after"),
onChange: (val) {
_userSettings.postsLifetime = val;
_updateDataConservationPolicy();
},
minValue: _serverConfig.dataConservationPolicy.minPostsLifetime,
),
DataConservationPolicyTile(
value: _userSettings.conversationMessagesLifetime,
title: tr("Automatically delete your conversation messages after"),
onChange: (val) {
_userSettings.conversationMessagesLifetime = val;
_updateDataConservationPolicy();
},
minValue: _serverConfig
.dataConservationPolicy.minConversationMessagesLifetime,
),
DataConservationPolicyTile(
value: _userSettings.likesLifetime,
title: tr("Automatically delete your likes after"),
onChange: (val) {
_userSettings.likesLifetime = val;
_updateDataConservationPolicy();
},
minValue: _serverConfig.dataConservationPolicy.minLikesLifetime,
),
DataConservationPolicyTile(
value: _userSettings.inactiveAccountLifeTime,
title: tr(
"Automatically delete your account if you have been inactive for"),
onChange: (val) {
_userSettings.inactiveAccountLifeTime = val;
_updateDataConservationPolicy();
},
minValue:
_serverConfig.dataConservationPolicy.minInactiveAccountLifetime,
),
];
void _updateDataConservationPolicy() async {
try {
if (_cachedPassword == null)
_cachedPassword = await showUserPasswordDialog(context);
await SettingsHelper.setDataConservationPolicy(
_cachedPassword, _userSettings);
_key.currentState.refresh();
} catch (e, s) {
print("Could not update data conservation policy! $e\n$s");
showSimpleSnack(
context, tr("Failed to update data conservation policy!"));
}
}
/// Permanently delete user account
@ -61,6 +166,98 @@ class _AccountPrivacySettingsState extends State<AccountPrivacySettings> {
}
}
class DataConservationPolicyTile extends SettingsTile {
final int value;
final String title;
final Function(int) onChange;
final int minValue;
const DataConservationPolicyTile({
@required this.value,
@required this.title,
@required this.onChange,
@required this.minValue,
}) : assert(title != null),
assert(onChange != null),
assert(minValue != null);
@override
Widget build(BuildContext context) {
return MultiChoicesSettingsTile(
title: title,
choices: _choices,
currentValue: _roundValue,
onChanged: onChange,
);
}
int get _day => 60 * 60 * 24;
int get _month => _day * 30;
int get _year => _day * 365;
int get _roundValue {
if (this.value == null) return 0;
return _choices.firstWhere((element) => element.id >= this.value).id;
}
List<MultiChoiceEntry<int>> get _choices => [
MultiChoiceEntry(id: 0, title: tr("Never"), hidden: false),
MultiChoiceEntry(
id: _day * 7,
title: tr("7 days"),
hidden: _day * 7 < minValue,
),
MultiChoiceEntry(
id: _day * 15,
title: tr("15 days"),
hidden: _day * 15 < minValue,
),
MultiChoiceEntry(
id: _month,
title: tr("1 month"),
hidden: _month < minValue,
),
MultiChoiceEntry(
id: _month * 3,
title: tr("3 months"),
hidden: _month * 3 < minValue,
),
MultiChoiceEntry(
id: _month * 6,
title: tr("6 months"),
hidden: _month * 6 < minValue,
),
MultiChoiceEntry(
id: _year,
title: tr("1 year"),
hidden: _year < minValue,
),
MultiChoiceEntry(
id: _year * 2,
title: tr("2 years"),
hidden: _year * 5 < minValue,
),
MultiChoiceEntry(
id: _year * 5,
title: tr("5 years"),
hidden: _year * 5 < minValue,
),
MultiChoiceEntry(
id: _year * 10,
title: tr("10 years"),
hidden: _year * 10 < minValue,
),
MultiChoiceEntry(
id: _year * 50,
title: tr("50 years"),
hidden: _year * 50 < minValue,
),
];
}
class _LastChanceDeleteAccountDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {

View File

@ -1,10 +1,13 @@
import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/helpers/settings_helper.dart';
import 'package:comunic/helpers/users_helper.dart';
import 'package:comunic/models/security_settings.dart';
import 'package:comunic/ui/dialogs/input_new_password_dialog.dart';
import 'package:comunic/ui/dialogs/input_user_password_dialog.dart';
import 'package:comunic/ui/widgets/dialogs/auto_sized_dialog_content_widget.dart';
import 'package:comunic/ui/widgets/new_password_input_widget.dart';
import 'package:comunic/ui/widgets/settings/header_spacer_section.dart';
import 'package:comunic/utils/account_utils.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart';
import 'package:flutter/material.dart';
@ -55,11 +58,21 @@ class _AccountSecuritySettingsScreenState
/// Change current user password
void _changePassword() async {
try {
final currEmail = await AccountHelper.getCurrentAccountEmailAddress();
final currUser = await UsersHelper().getSingleWithThrow(userID());
final currPassword = await showUserPasswordDialog(context);
if (currPassword == null) return;
final newPassword = await showInputNewPassword(context);
final newPassword = await showInputNewPassword(
context: context,
userInfo: UserInfoForPassword(
firstName: currUser.firstName,
lastName: currUser.lastName,
email: currEmail,
),
);
if (newPassword == null) return;

View File

@ -1,5 +1,9 @@
import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/helpers/events_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/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';
@ -36,7 +40,10 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
super.listen<InvalidLoginTokensEvent>((ev) => _openLoginPage());
// Listen to WebSocket close event
super.listen<WSClosedEvent>((e) => _tryConnect());
super.listen<WSClosedEvent>((e) {
_popToMainRoute();
_tryConnect();
});
}
@override
@ -54,6 +61,19 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
/// Try to connect to server
void _tryConnect() async {
try {
await ServerConfigurationHelper.ensureLoaded();
if (ServerConfigurationHelper.config.minSupportedMobileVersion > VersionHelper.version)
await showDeprecationDialog(context);
if (!AccountHelper.isUserIDLoaded) {
_popToMainRoute();
_openLoginPage();
return;
}
print("Attempting WebSocket connection...");
setState(() {
_error = false;
});
@ -65,9 +85,7 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
print("Could not connect to server! $e");
print(stack);
// Pop until we reach main route
Navigator.of(context).popUntil((settings) =>
ModalRoute.of(context).isCurrent || !ModalRoute.of(context).isActive);
_popToMainRoute();
setState(() {
_error = true;
@ -77,7 +95,7 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
@override
Widget build(BuildContext context) {
return !_error && WebSocketHelper.isConnected()
return (!_error && WebSocketHelper.isConnected())
? (isTablet(context)
? TabletRoute(key: mainControllerKey)
: SmartphoneMainRoute(key: mainControllerKey))
@ -110,6 +128,11 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
Spacer(
flex: 2,
),
Text(tr("Version %version% - Build %build%", args: {
"version": VersionHelper.info.version.toString(),
"build": VersionHelper.info.buildNumber.toString()
})),
Spacer(flex: 1),
],
),
),
@ -138,4 +161,10 @@ class _InitializeWidgetState extends SafeState<InitializeWidget> {
],
);
}
void _popToMainRoute() {
// Pop until we reach main route
Navigator.of(context).popUntil((settings) =>
ModalRoute.of(context).isCurrent || !ModalRoute.of(context).isActive);
}
}

View File

@ -0,0 +1,151 @@
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/server_config.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:flutter/material.dart';
/// New password input widget
///
/// @author Pierre Hubert
class UserInfoForPassword {
final String firstName;
final String lastName;
final String email;
const UserInfoForPassword({
@required this.firstName,
@required this.lastName,
@required this.email,
});
}
class NewPasswordInputWidget extends StatefulWidget {
final Widget icon;
final VoidCallback onEdited;
final VoidCallback onSubmitted;
final TextInputAction textInputAction;
final String label;
final UserInfoForPassword user;
const NewPasswordInputWidget({
Key key,
this.icon,
this.onEdited,
this.onSubmitted,
this.textInputAction,
@required this.label,
@required this.user,
}) : assert(label != null),
assert(user != null),
super(key: key);
@override
NewPasswordInputWidgetState createState() => NewPasswordInputWidgetState();
}
class NewPasswordInputWidgetState extends State<NewPasswordInputWidget> {
final TextEditingController _controller = TextEditingController();
String get value => _controller.text;
bool get valid => value.isNotEmpty && (_errorMessage ?? "").isEmpty;
PasswordPolicy get _policy => ServerConfigurationHelper.config.passwordPolicy;
@override
void didUpdateWidget(covariant NewPasswordInputWidget oldWidget) {
super.didUpdateWidget(oldWidget);
setState(() {});
}
@override
Widget build(BuildContext context) => TextField(
controller: _controller,
obscureText: true,
onChanged: (s) => _onChanged(),
onSubmitted:
widget.onSubmitted == null ? null : (s) => widget.onSubmitted(),
textInputAction: widget.textInputAction,
decoration: InputDecoration(
errorText: _errorMessage,
errorMaxLines: 3,
icon: widget.icon,
labelText: widget.label,
),
);
void _onChanged() {
setState(() {});
if (widget.onEdited != null) widget.onEdited();
}
/// Generate an error message associated with current password
String get _errorMessage {
if (value.isEmpty) return null;
// Mandatory checks
if (!_policy.allowMailInPassword &&
(widget.user.email ?? "").isNotEmpty &&
(widget.user.email.toLowerCase().contains(value.toLowerCase()) ||
value.toLowerCase().contains(widget.user.email.toLowerCase()))) {
return tr("Your password must not contains part of your email address!");
}
if (!_policy.allowNameInPassword &&
(widget.user.firstName ?? "").isNotEmpty &&
value.toLowerCase().contains(widget.user.firstName.toLowerCase())) {
return tr("Your password must not contains your first name!");
}
if (!_policy.allowNameInPassword &&
(widget.user.lastName ?? "").isNotEmpty &&
value.toLowerCase().contains(widget.user.lastName.toLowerCase())) {
return tr("Your password must not contains your last name!");
}
if (_policy.minPasswordLength > value.length) {
return tr(
"Your password must be composed of at least %num% characters!",
args: {
"num": _policy.minPasswordLength.toString(),
},
);
}
// Characteristics check
var count = 0;
if (_hasCharacteristic(RegExp(r'[A-Z]'), _policy.minNumberUpperCaseLetters))
count++;
if (_hasCharacteristic(RegExp(r'[a-z]'), _policy.minNumberLowerCaseLetters))
count++;
if (_hasCharacteristic(RegExp(r'[0-9]'), _policy.minNumberDigits)) count++;
if (_hasCharacteristic(
RegExp(r'[^A-Za-z0-9]'), _policy.minNumberSpecialCharacters)) count++;
if (count >= _policy.minCategoriesPresence) return null;
return tr(
"Your password must contains characters of at least %num% of the following categories : %upper% upper case letter, %lower% lowercase letter, %digit% digit, %special% special character.",
args: {
"num": (_policy.minCategoriesPresence == 4
? tr("ALL")
: _policy.minCategoriesPresence.toString()),
"upper": _policy.minNumberUpperCaseLetters.toString(),
"lower": _policy.minNumberLowerCaseLetters.toString(),
"digit": _policy.minNumberDigits.toString(),
"special": _policy.minNumberSpecialCharacters.toString(),
},
);
}
bool _hasCharacteristic(RegExp exp, int requiredCount) {
if (requiredCount < 1) return true;
return exp.allMatches(value).length >= requiredCount;
}
}

View File

@ -3,7 +3,7 @@
/// @author Pierre HUBERT
/// Check out whether a password is valid or not
bool validatePassword(String s) => s.length > 3;
bool legacyValidatePassword(String s) => s.length > 3;
/// Check out whether a given email address is valid or not

View File

@ -332,7 +332,7 @@ packages:
source: hosted
version: "0.3.0"
package_info:
dependency: transitive
dependency: "direct main"
description:
name: package_info
url: "https://pub.dartlang.org"
@ -630,6 +630,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
version:
dependency: "direct main"
description:
name: version
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
wakelock:
dependency: "direct main"
description:

View File

@ -11,7 +11,7 @@ description: Comunic client
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.1.0+4
version: 1.1.1+5
environment:
sdk: ">=2.1.0 <3.0.0"
@ -85,6 +85,12 @@ dependencies:
# Pick any kind of file
file_picker_cross: ^4.2.8
# Get information about current version
package_info: ^0.4.3+4
# Version manager
version: ^1.2.0
dev_dependencies:
flutter_test:
sdk: flutter