Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/smooth_app/lib/data_models/login_result.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ enum LoginResultType { successful, unsuccessful, serverIssue, exception }

/// Result of a log in attempt, more subtle than a `bool`.
class LoginResult {
const LoginResult(this.type, {this.user, this.text});
const LoginResult(this.type, {this.user, this.userDetails, this.text});

final LoginResultType type;
final User? user;
final String? text;
final UserDetails? userDetails;

String getErrorMessage(final AppLocalizations appLocalizations) =>
switch (type) {
Expand Down Expand Up @@ -62,6 +63,7 @@ class LoginResult {
password: user.password,
cookie: loginStatus.cookie,
),
userDetails: loginStatus.userDetails,
);
} catch (e) {
return LoginResult(LoginResultType.exception, text: e.toString());
Expand Down
38 changes: 36 additions & 2 deletions packages/smooth_app/lib/data_models/user_management_provider.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
Expand All @@ -13,6 +14,9 @@ class UserManagementProvider with ChangeNotifier {
static const String _USER_ID = 'user_id';
static const String _PASSWORD = 'pasword';
static const String _COOKIE = 'user_cookie';
static const String _USER_DETAILS = 'user_details';

static UserDetails? globalUserDetails;

/// Checks credentials and conditionally saves them.
Future<LoginResult> login(
Expand All @@ -27,16 +31,31 @@ class UserManagementProvider with ChangeNotifier {
return loginResult;
}
await putUser(loginResult.user!);
await _saveUserDetails(loginResult.userDetails);
await credentialsInStorage();
return loginResult;
}

/// Saves user details to storage
Future<void> _saveUserDetails(UserDetails? userDetails) async {
globalUserDetails = userDetails;
if (userDetails != null) {
final String jsonString = jsonEncode(userDetails.toJson());
await DaoSecuredString.put(key: _USER_DETAILS, value: jsonString);
} else {
DaoSecuredString.remove(key: _USER_DETAILS);
}
Comment on lines +45 to +47
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DaoSecuredString.remove returns a Future, but this call isn’t awaited. That can leave user_details undeleted when subsequent logic runs (and also hides storage errors). Await the remove call (and consider propagating/handling the returned bool) before notifying listeners.

Copilot uses AI. Check for mistakes.
notifyListeners();
}

/// Deletes saved credentials from storage
Future<bool> logout() async {
OpenFoodAPIConfiguration.globalUser = null;
globalUserDetails = null;
DaoSecuredString.remove(key: _USER_ID);
DaoSecuredString.remove(key: _PASSWORD);
DaoSecuredString.remove(key: _COOKIE);
DaoSecuredString.remove(key: _USER_DETAILS);
notifyListeners();
final bool contains = await credentialsInStorage();
return !contains;
Comment on lines 55 to 61
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logout() calls DaoSecuredString.remove (async) without awaiting any of them, then immediately calls credentialsInStorage(). Because deletes may not have completed, logout can incorrectly report that credentials still exist. Await the removals (e.g. Future.wait) before checking credentialsInStorage / returning.

Copilot uses AI. Check for mistakes.
Expand All @@ -52,17 +71,20 @@ class UserManagementProvider with ChangeNotifier {
String? effectiveUserId;
String? effectivePassword;
String? effectiveCookie;
String? userDetailsJson;

try {
effectiveUserId = userId ?? await DaoSecuredString.get(_USER_ID);
effectivePassword = password ?? await DaoSecuredString.get(_PASSWORD);
effectiveCookie = await DaoSecuredString.get(_COOKIE);
userDetailsJson = await DaoSecuredString.get(_USER_DETAILS);
} on PlatformException {
/// Decrypting the values can go wrong if, for example, the app was
/// manually overwritten from an external apk.
DaoSecuredString.remove(key: _USER_ID);
DaoSecuredString.remove(key: _PASSWORD);
DaoSecuredString.remove(key: _COOKIE);
DaoSecuredString.remove(key: _USER_DETAILS);
Logs.e('Credentials query failed, you have been logged out');
Comment on lines 84 to 88
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PlatformException handler, DaoSecuredString.remove is async but not awaited. That can leave stale credentials/user_details in secure storage after a decryption failure. Await these removals (e.g. Future.wait) so the cleanup is guaranteed before continuing.

Copilot uses AI. Check for mistakes.
}

Expand All @@ -76,6 +98,17 @@ class UserManagementProvider with ChangeNotifier {
cookie: effectiveCookie,
);
OpenFoodAPIConfiguration.globalUser = user;

// Restore complete UserDetails from JSON
if (userDetailsJson != null && userDetailsJson.isNotEmpty) {
try {
final Map<String, dynamic> json = jsonDecode(userDetailsJson);
globalUserDetails = UserDetails.fromJson(json);
} catch (e) {
Logs.e('Failed to parse UserDetails: $e');
DaoSecuredString.remove(key: _USER_DETAILS);
}
Comment on lines +108 to +110
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When JSON parsing fails, DaoSecuredString.remove(_USER_DETAILS) is async but not awaited. Awaiting the removal ensures corrupted data is actually cleared before the next startup/login attempt.

Copilot uses AI. Check for mistakes.
}
}

/// Checks if any credentials exist in storage
Expand Down Expand Up @@ -117,9 +150,10 @@ class UserManagementProvider with ChangeNotifier {
return;
}

/// Save the cookie if necessary
/// Save the cookie and user details if necessary
if (user.cookie == null && loginResult.user?.cookie != null) {
putUser(loginResult.user!);
await putUser(loginResult.user!);
await _saveUserDetails(loginResult.userDetails);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/user_management_provider.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/generic_lib/widgets/app_bars/app_bar_constanst.dart';
import 'package:smooth_app/l10n/app_localizations.dart';
Expand All @@ -16,12 +18,28 @@ class LoggedInAppBarHeader extends StatelessWidget {

final String userId;

String _getGreeting(AppLocalizations appLocalizations) {
final int hour = DateTime.now().hour;
if (hour < 12) {
return appLocalizations.greet_good_morning;
} else if (hour < 17) {
return appLocalizations.greet_good_afternoon;
} else if (hour < 21) {
return appLocalizations.greet_good_evening;
} else {
return appLocalizations.greet_good_night;
}
}

@override
Widget build(BuildContext context) {
final AppLocalizations appLocalizations = AppLocalizations.of(context);
final SmoothColorsThemeExtension themeExtension = context
.extension<SmoothColorsThemeExtension>();

final UserDetails? userDetails = UserManagementProvider.globalUserDetails;
final String name = userDetails?.name ?? userId;

return ConstrainedBox(
constraints: const BoxConstraints(minHeight: PROFILE_PICTURE_SIZE),
child: Row(
Expand All @@ -33,8 +51,8 @@ class LoggedInAppBarHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
spacing: VERY_SMALL_SPACE,
children: <Widget>[
Text(
userId,
AutoSizeText(
'${_getGreeting(appLocalizations)} $name',
style: TextStyle(
Comment on lines +54 to 56
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The greeting is built by concatenating two localized fragments (greeting + name). This is not fully localizable (word order / punctuation / spacing differ by language). Prefer a single localized message with a {name} placeholder (e.g. per time-of-day greeting keys that include the name), and format it through l10n instead of string concatenation.

Copilot uses AI. Check for mistakes.
color: themeExtension.secondaryNormal,
fontSize: 18.0,
Expand Down
6 changes: 5 additions & 1 deletion packages/smooth_app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -5744,5 +5744,9 @@
"type": "String"
}
}
}
},
"greet_good_morning": "Good morning",
"greet_good_afternoon": "Good afternoon",
"greet_good_evening": "Good evening",
"greet_good_night": "Good night"
}
24 changes: 24 additions & 0 deletions packages/smooth_app/lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10785,6 +10785,30 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'{percent}%'**
String percent_value(String percent);

/// No description provided for @greet_good_morning.
///
/// In en, this message translates to:
/// **'Good morning'**
String get greet_good_morning;

/// No description provided for @greet_good_afternoon.
///
/// In en, this message translates to:
/// **'Good afternoon'**
String get greet_good_afternoon;

/// No description provided for @greet_good_evening.
///
/// In en, this message translates to:
/// **'Good evening'**
String get greet_good_evening;

/// No description provided for @greet_good_night.
///
/// In en, this message translates to:
/// **'Good night'**
String get greet_good_night;
}

class _AppLocalizationsDelegate
Expand Down