Files
monsieurtanuki 1b70203d0b fix: 6784 - readyForPriceTagValidation true only for priceTag and bulk (#6794)
* fix: 6784 - readyForPriceTagValidation true only for priceTag and bulk

Impacted files:
* `background_task_add_price.dart`: added field and parameter `bool bulkProofUpload`; set `readyForPriceTagValidation` as "priceTag and bulk"
* `price_bulk_proof_card.dart`: added warning about AI and image validation
* `price_model.dart`: added field `bool bulkProofUpload`
* `proof_bulk_add_page.dart`: added parameter `bulkProofUpload: true`

* Replaced parameter bulkProofUpload with readyForPriceTagValidation

Impacted files:
* `background_task_add_price.dart`: replaced parameter bulkProofUpload with readyForPriceTagValidation
* `price_bulk_proof_card.dart`: added a switch to change the value of readyForPriceTagValidation
* `price_model.dart`: readyForPriceTagValidation with getter and setter
* `proof_bulk_add_page.dart`: start with the default user value for readyForPriceTagValidation
* `user_preferences.dart`: storing the default user value for readyForPriceTagValidation

* minor fix

* Localization
2025-07-30 15:35:38 +02:00

601 lines
19 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:smooth_app/data_models/product_preferences.dart';
import 'package:smooth_app/pages/onboarding/onboarding_flow_navigator.dart';
import 'package:smooth_app/pages/preferences/user_preferences_dev_mode.dart';
import 'package:smooth_app/pages/product/product_page/footer/new_product_footer.dart';
import 'package:smooth_app/themes/color_schemes.dart';
import 'package:smooth_app/themes/theme_provider.dart';
part 'package:smooth_app/data_models/preferences/migration/user_preferences_migration.dart';
/// User choice regarding the picture source.
enum UserPictureSource {
/// Always select between Gallery and Camera
SELECT('S'),
/// Always use Gallery
GALLERY('G'),
/// Always use Camera
CAMERA('C');
const UserPictureSource(this.tag);
final String tag;
static UserPictureSource get defaultValue => UserPictureSource.SELECT;
static UserPictureSource fromString(final String tag) => UserPictureSource
.values
.firstWhere((final UserPictureSource source) => source.tag == tag);
}
class UserPreferences extends ChangeNotifier {
UserPreferences._shared(final SharedPreferences sharedPreferences)
: _sharedPreferences = sharedPreferences {
onCrashReportingChanged = ValueNotifier<bool>(crashReports);
onAnalyticsChanged = ValueNotifier<bool>(userTracking);
_incrementAppLaunches();
}
/// Singleton
static UserPreferences? _instance;
final SharedPreferences _sharedPreferences;
static Future<UserPreferences> getUserPreferences() async {
if (_instance == null) {
final SharedPreferences preferences =
await SharedPreferences.getInstance();
_instance = UserPreferences._shared(preferences);
}
return _instance!;
}
/// Once we initialized with main.dart, we don't need the "async".
static UserPreferences getUserPreferencesSync() => _instance!;
late ValueNotifier<bool> onCrashReportingChanged;
late ValueNotifier<bool> onAnalyticsChanged;
/// Whether the preferences are empty or not
static const String _TAG_INIT = 'init';
/// The current version of preferences
static const String _TAG_VERSION = 'prefs_version';
static const int _PREFS_CURRENT_VERSION = 3;
static const String _TAG_APP_LAUNCHES = 'appLaunches';
static const String _TAG_PREFIX_IMPORTANCE = 'IMPORTANCE_AS_STRING';
static const String _TAG_CURRENT_THEME_MODE = 'currentThemeMode';
static const String _TAG_CURRENT_COLOR_SCHEME = 'currentColorScheme';
static const String _TAG_CURRENT_CONTRAST_MODE = 'contrastMode';
static const String _TAG_USER_COUNTRY_CODE = 'userCountry';
static const String _TAG_USER_COUNTRY_CODE_LAST_UPDATE =
'userCountryLastUpdate';
static const String _TAG_USER_CURRENCY_CODE = 'userCurrency';
static const String _TAG_LAST_VISITED_ONBOARDING_PAGE =
'lastVisitedOnboardingPage';
static const String _TAG_PREFIX_FLAG = 'FLAG_PREFIX_';
static const String _TAG_DEV_MODE = 'devMode';
static const String _TAG_USER_TRACKING = 'user_tracking';
static const String _TAG_CRASH_REPORTS = 'crash_reports';
static const String _TAG_PRICES_FEEDBACK_FORM = 'prices_feedback_form';
static const String _TAG_EXCLUDED_ATTRIBUTE_IDS = 'excluded_attributes';
static const String _TAG_UNIQUE_RANDOM = '_unique_random';
static const String _TAG_LAZY_COUNT_PREFIX = '_lazy_count_prefix';
static const String _TAG_LATEST_PRODUCT_TYPE = '_latest_product_type';
static const String _TAG_SEARCH_SHOW_PRODUCT_TYPE_FILTER =
'_search_show_product_type_filter';
static const String _TAG_PRODUCT_PAGE_ACTIONS = '_product_page_actions';
static const String _TAG_LANGUAGES_USAGE = '_languages_usage';
static const String _TAG_PRODUCT_PAGE_TABS = '_product_page_tabs';
static const String _TAG_READY_FOR_PRICE_TAG_VALIDATION =
'ready_for_price_tag_validation';
/// Camera preferences
// Use the flash/torch with the camera
static const String _TAG_USE_FLASH_WITH_CAMERA = 'enable_flash_with_camera';
// Play sound when decoding a barcode
static const String _TAG_PLAY_CAMERA_SCAN_SOUND = 'camera_scan_sound';
/// Vibrations / haptic feedback
static const String _TAG_HAPTIC_FEEDBACK_IN_APP = 'haptic_feedback_enabled';
/// Price privacy warning
static const String TAG_PRICE_PRIVACY_WARNING = 'price_privacy_warning';
/// Attribute group that is not collapsed
static const String _TAG_ACTIVE_ATTRIBUTE_GROUP = 'activeAttributeGroup';
/// User picture source
static const String _TAG_USER_PICTURE_SOURCE = 'userPictureSource';
/// If the in-app review was asked at least one time (false by default)
static const String _TAG_IN_APP_REVIEW_ALREADY_DISPLAYED =
'inAppReviewAlreadyAsked';
static const String _TAG_NUMBER_OF_SCANS = 'numberOfScans';
/// User knowledge panel order
static const String _TAG_USER_KNOWLEDGE_PANEL_ORDER =
'userKnowledgePanelOrder';
/// Tagline feed (news displayed / clicked)
static const String _TAG_TAGLINE_FEED_NEWS_DISPLAYED =
'taglineFeedNewsDisplayed';
static const String _TAG_TAGLINE_FEED_NEWS_CLICKED = 'taglineFeedNewsClicked';
/// Info messages
static const String _TAG_SHOW_BANNER_INPUT_PRODUCT_NAME =
'bannerInputProductName';
Future<void> init(final ProductPreferences productPreferences) async {
await _onMigrate();
if (_sharedPreferences.getBool(_TAG_INIT) != null) {
return;
}
await productPreferences.resetImportances();
await _sharedPreferences.setBool(_TAG_INIT, true);
}
/// Allow to migrate between versions
Future<void> _onMigrate() async {
await UserPreferencesMigrationTool.onUpgrade(
this,
_sharedPreferences.getInt(_TAG_VERSION),
_PREFS_CURRENT_VERSION,
);
await _sharedPreferences.setInt(
_TAG_VERSION,
UserPreferences._PREFS_CURRENT_VERSION,
);
}
int get appLaunches => _sharedPreferences.getInt(_TAG_APP_LAUNCHES) ?? 0;
Future<void> _incrementAppLaunches() async {
await _sharedPreferences.setInt(_TAG_APP_LAUNCHES, appLaunches + 1);
// No need to call notifyListeners here
}
String _getImportanceTag(final String variable) =>
_TAG_PREFIX_IMPORTANCE + variable;
Future<void> setImportance(
final String attributeId,
final String importanceId,
) async {
await _sharedPreferences.setString(
_getImportanceTag(attributeId),
importanceId,
);
notifyListeners();
}
String getImportance(final String attributeId) =>
_sharedPreferences.getString(_getImportanceTag(attributeId)) ??
PreferenceImportance.ID_NOT_IMPORTANT;
Future<void> setTheme(final String theme) async {
await _sharedPreferences.setString(_TAG_CURRENT_THEME_MODE, theme);
notifyListeners();
}
Future<void> setColorScheme(final String color) async {
await _sharedPreferences.setString(_TAG_CURRENT_COLOR_SCHEME, color);
notifyListeners();
}
Future<void> setContrastScheme(final String contrastLevel) async {
await _sharedPreferences.setString(
_TAG_CURRENT_CONTRAST_MODE,
contrastLevel,
);
notifyListeners();
}
String _getLazyCountTag(final String tag) => '$_TAG_LAZY_COUNT_PREFIX$tag';
Future<void> setLazyCount(
final int value,
final String suffixTag, {
required final bool notify,
}) async {
final int? oldValue = getLazyCount(suffixTag);
if (value == oldValue) {
return;
}
await _sharedPreferences.setInt(_getLazyCountTag(suffixTag), value);
if (notify) {
notifyListeners();
}
}
int? getLazyCount(final String suffixTag) =>
_sharedPreferences.getInt(_getLazyCountTag(suffixTag));
Future<void> setUserTracking(final bool state) async {
await _sharedPreferences.setBool(_TAG_USER_TRACKING, state);
onAnalyticsChanged.value = state;
notifyListeners();
}
bool get userTracking =>
_sharedPreferences.getBool(_TAG_USER_TRACKING) ?? false;
/// Returns a huge random value that will be computed just once.
Future<int> getUniqueRandom() async {
const String tag = _TAG_UNIQUE_RANDOM;
int? result = _sharedPreferences.getInt(tag);
if (result != null) {
return result;
}
result = math.Random().nextInt(1 << 32);
await _sharedPreferences.setInt(tag, result);
return result;
}
Future<void> setCrashReports(final bool state) async {
await _sharedPreferences.setBool(_TAG_CRASH_REPORTS, state);
onCrashReportingChanged.value = state;
notifyListeners();
}
bool get crashReports =>
_sharedPreferences.getBool(_TAG_CRASH_REPORTS) ?? false;
Future<void> markPricesFeedbackFormAsCompleted() async {
await _sharedPreferences.setBool(_TAG_PRICES_FEEDBACK_FORM, false);
notifyListeners();
}
bool get shouldShowPricesFeedbackForm =>
_sharedPreferences.getBool(_TAG_PRICES_FEEDBACK_FORM) ?? true;
Future<void> setReadyForPriceTagValidation(final bool state) async =>
_sharedPreferences.setBool(_TAG_READY_FOR_PRICE_TAG_VALIDATION, state);
bool get readyForPriceTagValidation =>
_sharedPreferences.getBool(_TAG_READY_FOR_PRICE_TAG_VALIDATION) ?? false;
String get currentTheme =>
_sharedPreferences.getString(_TAG_CURRENT_THEME_MODE) ??
THEME_SYSTEM_DEFAULT;
String get currentColor =>
_sharedPreferences.getString(_TAG_CURRENT_COLOR_SCHEME) ??
COLOR_DEFAULT_NAME;
String get currentContrastLevel =>
_sharedPreferences.getString(_TAG_CURRENT_CONTRAST_MODE) ??
CONTRAST_MEDIUM;
/// Please use [ProductQuery.setCountry] as interface
Future<void> setUserCountryCode(final String countryCode) async {
await _sharedPreferences.setString(_TAG_USER_COUNTRY_CODE, countryCode);
await _sharedPreferences.setInt(
_TAG_USER_COUNTRY_CODE_LAST_UPDATE,
DateTime.now().millisecondsSinceEpoch,
);
notifyListeners();
}
String? get userCountryCode =>
_sharedPreferences.getString(_TAG_USER_COUNTRY_CODE);
Future<void> setUserCurrencyCode(final String code) async {
await _sharedPreferences.setString(_TAG_USER_CURRENCY_CODE, code);
notifyListeners();
}
String? get userCurrencyCode =>
_sharedPreferences.getString(_TAG_USER_CURRENCY_CODE);
Future<void> setLastVisitedOnboardingPage(final OnboardingPage page) async {
await _sharedPreferences.setInt(
_TAG_LAST_VISITED_ONBOARDING_PAGE,
page.index,
);
notifyListeners();
}
Future<void> resetOnboarding() async {
await setLastVisitedOnboardingPage(OnboardingPage.NOT_STARTED);
// for tests with a fresh null country
await _sharedPreferences.remove(_TAG_USER_COUNTRY_CODE);
await _sharedPreferences.remove(_TAG_USER_CURRENCY_CODE);
notifyListeners();
}
OnboardingPage get lastVisitedOnboardingPage {
final int? pageIndex = _sharedPreferences.getInt(
_TAG_LAST_VISITED_ONBOARDING_PAGE,
);
return pageIndex == null
? OnboardingPage.NOT_STARTED
: OnboardingPage.values[math.min(
pageIndex,
OnboardingPage.values.length - 1,
)];
}
Future<void> incrementScanCount() async {
await _sharedPreferences.setInt(_TAG_NUMBER_OF_SCANS, numberOfScans + 1);
notifyListeners();
}
int get numberOfScans => _sharedPreferences.getInt(_TAG_NUMBER_OF_SCANS) ?? 0;
Future<void> markInAppReviewAsShown() async {
await _sharedPreferences.setBool(
_TAG_IN_APP_REVIEW_ALREADY_DISPLAYED,
true,
);
notifyListeners();
}
bool get inAppReviewAlreadyAsked =>
_sharedPreferences.getBool(_TAG_IN_APP_REVIEW_ALREADY_DISPLAYED) ?? false;
/// Please use [ProductQuery.setLanguage] as interface
Future<void> setAppLanguageCode(String? languageCode) async {
if (languageCode == null) {
await _sharedPreferences.remove(
UserPreferencesDevMode.userPreferencesAppLanguageCode,
);
} else {
await setDevModeString(
UserPreferencesDevMode.userPreferencesAppLanguageCode,
languageCode,
);
}
notifyListeners();
}
/// Please use [ProductQuery.getLanguage] as interface
String? get appLanguageCode =>
getDevModeString(UserPreferencesDevMode.userPreferencesAppLanguageCode);
String _getFlagTag(final String key) => _TAG_PREFIX_FLAG + key;
Future<void> setFlag(final String key, final bool? value) async {
value == null
? await _sharedPreferences.remove(_getFlagTag(key))
: await _sharedPreferences.setBool(_getFlagTag(key), value);
notifyListeners();
}
bool? getFlag(final String key) =>
_sharedPreferences.getBool(_getFlagTag(key));
List<String> getExcludedAttributeIds() =>
_sharedPreferences.getStringList(_TAG_EXCLUDED_ATTRIBUTE_IDS) ??
<String>[];
Future<void> setExcludedAttributeIds(final List<String> value) async {
await _sharedPreferences.setStringList(_TAG_EXCLUDED_ATTRIBUTE_IDS, value);
notifyListeners();
}
bool get useFlashWithCamera =>
_sharedPreferences.getBool(_TAG_USE_FLASH_WITH_CAMERA) ?? false;
Future<void> setUseFlashWithCamera(final bool useFlash) async {
await _sharedPreferences.setBool(_TAG_USE_FLASH_WITH_CAMERA, useFlash);
notifyListeners();
}
Future<void> setPlayCameraSound(bool playSound) async {
await _sharedPreferences.setBool(_TAG_PLAY_CAMERA_SCAN_SOUND, playSound);
notifyListeners();
}
bool get playCameraSound =>
_sharedPreferences.getBool(_TAG_PLAY_CAMERA_SCAN_SOUND) ?? false;
Future<void> setHapticFeedbackEnabled(bool enabled) async {
await _sharedPreferences.setBool(_TAG_HAPTIC_FEEDBACK_IN_APP, enabled);
notifyListeners();
}
bool get hapticFeedbackEnabled =>
_sharedPreferences.getBool(_TAG_HAPTIC_FEEDBACK_IN_APP) ?? true;
Future<void> setDevMode(final int value) async {
await _sharedPreferences.setInt(_TAG_DEV_MODE, value);
notifyListeners();
}
int get devMode => _sharedPreferences.getInt(_TAG_DEV_MODE) ?? 0;
Future<void> setDevModeString(final String tag, final String value) async {
await _sharedPreferences.setString(tag, value);
notifyListeners();
}
String? getDevModeString(final String tag) =>
_sharedPreferences.getString(tag);
Future<void> setActiveAttributeGroup(final String value) async {
await _sharedPreferences.setString(_TAG_ACTIVE_ATTRIBUTE_GROUP, value);
notifyListeners();
}
String get activeAttributeGroup =>
_sharedPreferences.getString(_TAG_ACTIVE_ATTRIBUTE_GROUP) ??
AttributeGroup.ATTRIBUTE_GROUP_NUTRITIONAL_QUALITY;
UserPictureSource get userPictureSource => UserPictureSource.fromString(
_sharedPreferences.getString(_TAG_USER_PICTURE_SOURCE) ??
UserPictureSource.defaultValue.tag,
);
Future<void> setUserPictureSource(final UserPictureSource source) async {
await _sharedPreferences.setString(_TAG_USER_PICTURE_SOURCE, source.tag);
notifyListeners();
}
List<String> get userKnowledgePanelOrder =>
_sharedPreferences.getStringList(_TAG_USER_KNOWLEDGE_PANEL_ORDER) ??
<String>[];
Future<void> setUserKnowledgePanelOrder(final List<String> source) async {
await _sharedPreferences.setStringList(
_TAG_USER_KNOWLEDGE_PANEL_ORDER,
source,
);
notifyListeners();
}
List<String> get taglineFeedDisplayedNews =>
_sharedPreferences.getStringList(_TAG_TAGLINE_FEED_NEWS_DISPLAYED) ??
<String>[];
List<String> get taglineFeedClickedNews =>
_sharedPreferences.getStringList(_TAG_TAGLINE_FEED_NEWS_CLICKED) ??
<String>[];
// This method voluntarily does not notify listeners (not needed)
Future<void> taglineFeedMarkNewsAsDisplayed(final String ids) async {
final List<String> displayedNews = taglineFeedDisplayedNews;
final List<String> clickedNews = taglineFeedClickedNews;
if (!displayedNews.contains(ids)) {
displayedNews.add(ids);
_sharedPreferences.setStringList(
_TAG_TAGLINE_FEED_NEWS_DISPLAYED,
displayedNews,
);
}
if (clickedNews.contains(ids)) {
clickedNews.remove(ids);
_sharedPreferences.setStringList(
_TAG_TAGLINE_FEED_NEWS_CLICKED,
clickedNews,
);
}
}
// This method voluntarily does not notify listeners (not needed)
Future<void> taglineFeedMarkNewsAsClicked(final String ids) async {
final List<String> displayedNews = taglineFeedDisplayedNews;
final List<String> clickedNews = taglineFeedClickedNews;
if (displayedNews.contains(ids)) {
displayedNews.remove(ids);
_sharedPreferences.setStringList(
_TAG_TAGLINE_FEED_NEWS_DISPLAYED,
displayedNews,
);
}
if (!clickedNews.contains(ids)) {
clickedNews.add(ids);
_sharedPreferences.setStringList(
_TAG_TAGLINE_FEED_NEWS_CLICKED,
clickedNews,
);
}
}
bool showInputProductNameBanner() =>
_sharedPreferences.getBool(_TAG_SHOW_BANNER_INPUT_PRODUCT_NAME) ?? true;
Future<void> hideInputProductNameBanner() async {
await _sharedPreferences.setBool(
_TAG_SHOW_BANNER_INPUT_PRODUCT_NAME,
false,
);
notifyListeners();
}
ProductType get latestProductType =>
ProductType.fromOffTag(
_sharedPreferences.getString(_TAG_LATEST_PRODUCT_TYPE),
) ??
ProductType.food;
set latestProductType(final ProductType value) => unawaited(
_sharedPreferences.setString(_TAG_LATEST_PRODUCT_TYPE, value.offTag),
);
Future<void> setSearchProductTypeFilter(final bool visible) async {
await _sharedPreferences.setBool(
_TAG_SEARCH_SHOW_PRODUCT_TYPE_FILTER,
visible,
);
notifyListeners();
}
bool get searchProductTypeFilterVisible =>
_sharedPreferences.getBool(_TAG_SEARCH_SHOW_PRODUCT_TYPE_FILTER) ?? false;
List<ProductFooterActionBar> get productPageActions {
final List<String>? actions = _sharedPreferences.getStringList(
_TAG_PRODUCT_PAGE_ACTIONS,
);
if (actions == null) {
return ProductFooterActionBar.defaultOrder();
}
return actions
.map((String action) => ProductFooterActionBar.fromKey(action))
.toList(growable: false);
}
Future<void> setProductPageActions(
final Iterable<ProductFooterActionBar> value,
) async {
assert(!value.contains(ProductFooterActionBar.settings));
await _sharedPreferences.setStringList(
_TAG_PRODUCT_PAGE_ACTIONS,
value
.map((ProductFooterActionBar action) => action.key)
.toList(growable: false),
);
notifyListeners();
}
void increaseLanguageUsage(final OpenFoodFactsLanguage language) {
final String? usage = _sharedPreferences.getString(_TAG_LANGUAGES_USAGE);
final Map<String, int> languages;
if (usage == null || usage.isEmpty) {
languages = <String, int>{};
} else {
languages = Map<String, int>.from(jsonDecode(usage));
}
languages[language.code] = (languages[language.code] ?? 0) + 1;
unawaited(
_sharedPreferences.setString(_TAG_LANGUAGES_USAGE, jsonEncode(languages)),
);
}
Map<String, int> get languagesUsage {
final String? usage = _sharedPreferences.getString(_TAG_LANGUAGES_USAGE);
if (usage == null || usage.isEmpty) {
return <String, int>{};
}
return Map<String, int>.from(jsonDecode(usage));
}
List<String> get productPageTabs =>
_sharedPreferences.getStringList(_TAG_PRODUCT_PAGE_TABS) ?? <String>[];
Future<void> setProductPageTabs(final List<String> value) async {
await _sharedPreferences.setStringList(_TAG_PRODUCT_PAGE_TABS, value);
notifyListeners();
}
}