use mutex when updating user

This commit is contained in:
otsmr 2025-06-15 21:25:54 +02:00
parent 6946164d1e
commit 20030ccd14
23 changed files with 194 additions and 136 deletions

View file

@ -44,6 +44,10 @@ class UserData {
DateTime? signalLastSignedPreKeyUpdated; DateTime? signalLastSignedPreKeyUpdated;
@JsonKey(defaultValue: false)
bool identityBackupEnabled = false;
DateTime? identityBackupLastBackupTime;
final int userId; final int userId;
factory UserData.fromJson(Map<String, dynamic> json) => factory UserData.fromJson(Map<String, dynamic> json) =>

View file

@ -44,7 +44,12 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..signalLastSignedPreKeyUpdated = ..signalLastSignedPreKeyUpdated =
json['signalLastSignedPreKeyUpdated'] == null json['signalLastSignedPreKeyUpdated'] == null
? null ? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String); : DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..identityBackupEnabled = json['identityBackupEnabled'] as bool? ?? false
..identityBackupLastBackupTime =
json['identityBackupLastBackupTime'] == null
? null
: DateTime.parse(json['identityBackupLastBackupTime'] as String);
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username, 'username': instance.username,
@ -69,6 +74,9 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'myBestFriendContactId': instance.myBestFriendContactId, 'myBestFriendContactId': instance.myBestFriendContactId,
'signalLastSignedPreKeyUpdated': 'signalLastSignedPreKeyUpdated':
instance.signalLastSignedPreKeyUpdated?.toIso8601String(), instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
'identityBackupEnabled': instance.identityBackupEnabled,
'identityBackupLastBackupTime':
instance.identityBackupLastBackupTime?.toIso8601String(),
'userId': instance.userId, 'userId': instance.userId,
}; };

View file

@ -21,10 +21,9 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
notifyListeners(); notifyListeners();
var user = await getUser(); await updateUserdata((user) {
if (user != null) {
user.themeMode = newThemeMode; user.themeMode = newThemeMode;
await updateUser(user); return user;
} });
} }
} }

View file

@ -13,7 +13,6 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/app.dart'; import 'package:twonly/app.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pbserver.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
@ -347,11 +346,10 @@ class ApiService {
server.Response_Ok ok = result.value; server.Response_Ok ok = result.value;
if (ok.hasAuthenticated()) { if (ok.hasAuthenticated()) {
server.Response_Authenticated authenticated = ok.authenticated; server.Response_Authenticated authenticated = ok.authenticated;
UserData? user = await getUser(); updateUserdata((user) {
if (user != null) {
user.subscriptionPlan = authenticated.plan; user.subscriptionPlan = authenticated.plan;
await updateUser(user); return user;
} });
} }
Log.info("websocket is authenticated"); Log.info("websocket is authenticated");
onAuthenticated(); onAuthenticated();

View file

@ -34,20 +34,24 @@ Future<ErrorCode?> isAllowedToSend() async {
return ErrorCode.PlanNotAllowed; return ErrorCode.PlanNotAllowed;
} }
if (user.subscriptionPlan == "Free") { if (user.subscriptionPlan == "Free") {
int? todaysImageCounter = user.todaysImageCounter;
if (user.lastImageSend != null && user.todaysImageCounter != null) { if (user.lastImageSend != null && user.todaysImageCounter != null) {
if (isToday(user.lastImageSend!)) { if (isToday(user.lastImageSend!)) {
if (user.todaysImageCounter == 3) { if (user.todaysImageCounter == 3) {
return ErrorCode.PlanLimitReached; return ErrorCode.PlanLimitReached;
} }
user.todaysImageCounter = user.todaysImageCounter! + 1; todaysImageCounter = user.todaysImageCounter! + 1;
} else { } else {
user.todaysImageCounter = 1; todaysImageCounter = 1;
} }
} else { } else {
user.todaysImageCounter = 1; todaysImageCounter = 1;
} }
user.lastImageSend = DateTime.now(); await updateUserdata((user) {
await updateUser(user); user.lastImageSend = DateTime.now();
user.todaysImageCounter = todaysImageCounter;
return user;
});
} }
return null; return null;
} }

View file

@ -0,0 +1,20 @@
import 'package:twonly/src/utils/storage.dart';
Future<bool> isIdentityBackupEnabled() async {
final user = await getUser();
if (user == null) return false;
return user.identityBackupEnabled;
}
Future<DateTime?> getLastIdentityBackup() async {
final user = await getUser();
if (user == null) return null;
return user.identityBackupLastBackupTime;
}
Future enableIdentityBackup() async {
await updateUserdata((user) {
user.identityBackupEnabled = false;
return user;
});
}

View file

@ -21,8 +21,10 @@ Future syncFlameCounters() async {
contacts.firstWhere((x) => x.totalMediaCounter == maxMessageCounter); contacts.firstWhere((x) => x.totalMediaCounter == maxMessageCounter);
if (user.myBestFriendContactId != bestFriend.userId) { if (user.myBestFriendContactId != bestFriend.userId) {
user.myBestFriendContactId = bestFriend.userId; await updateUserdata((user) {
await updateUser(user); user.myBestFriendContactId = bestFriend.userId;
return user;
});
} }
for (Contact contact in contacts) { for (Contact contact in contacts) {

View file

@ -38,8 +38,10 @@ Future signalHandleNewServerConnection() async {
Log.error("could not generate a new signed pre key!"); Log.error("could not generate a new signed pre key!");
return; return;
} }
user.signalLastSignedPreKeyUpdated = DateTime.now(); await updateUserdata((user) {
await updateUser(user); user.signalLastSignedPreKeyUpdated = DateTime.now();
return user;
});
Result res = await apiService.updateSignedPreKey( Result res = await apiService.updateSignedPreKey(
signedPreKey.id, signedPreKey.id,
signedPreKey.getKeyPair().publicKey.serialize(), signedPreKey.getKeyPair().publicKey.serialize(),
@ -47,10 +49,10 @@ Future signalHandleNewServerConnection() async {
); );
if (res.isError) { if (res.isError) {
Log.error("could not update the signed pre key: ${res.error}"); Log.error("could not update the signed pre key: ${res.error}");
final UserData? user = await getUser(); await updateUserdata((user) {
if (user == null) return; user.signalLastSignedPreKeyUpdated = null;
user.signalLastSignedPreKeyUpdated = null; return user;
await updateUser(user); });
} else { } else {
Log.info("updated signed pre key"); Log.info("updated signed pre key");
} }

View file

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mutex/mutex.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
@ -33,18 +34,27 @@ Future<UserData?> getUser() async {
Future updateUsersPlan(BuildContext context, String planId) async { Future updateUsersPlan(BuildContext context, String planId) async {
context.read<CustomChangeProvider>().plan = planId; context.read<CustomChangeProvider>().plan = planId;
var user = await getUser();
if (user != null) { await updateUserdata((user) {
user.subscriptionPlan = planId; user.subscriptionPlan = planId;
await updateUser(user); return user;
} });
if (!context.mounted) return; if (!context.mounted) return;
context.read<CustomChangeProvider>().updatePlan(planId); context.read<CustomChangeProvider>().updatePlan(planId);
} }
Future updateUser(UserData userData) async { Mutex updateProtection = Mutex();
final storage = FlutterSecureStorage();
storage.write(key: "userData", value: jsonEncode(userData)); Future<UserData?> updateUserdata(Function(UserData userData) updateUser) async {
return await updateProtection.protect<UserData?>(() async {
final user = await getUser();
if (user == null) return null;
UserData updated = updateUser(user);
final storage = FlutterSecureStorage();
storage.write(key: "userData", value: jsonEncode(updated));
return user;
});
} }
Future<bool> deleteLocalUserData() async { Future<bool> deleteLocalUserData() async {

View file

@ -558,11 +558,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
onPressed: () async { onPressed: () async {
useHighQuality = !useHighQuality; useHighQuality = !useHighQuality;
setState(() {}); setState(() {});
var user = await getUser(); await updateUserdata((user) {
if (user != null) {
user.useHighQuality = useHighQuality; user.useHighQuality = useHighQuality;
updateUser(user); return user;
} });
}, },
), ),
if (!hasAudioPermission) if (!hasAudioPermission)

View file

@ -29,21 +29,21 @@ class _EmojisState extends State<Emojis> {
} }
Future selectEmojis(String emoji) async { Future selectEmojis(String emoji) async {
final user = await getUser(); await updateUserdata((user) {
if (user == null) return; if (user.lastUsedEditorEmojis == null) {
if (user.lastUsedEditorEmojis == null) { user.lastUsedEditorEmojis = [emoji];
user.lastUsedEditorEmojis = [emoji]; } else {
} else { if (user.lastUsedEditorEmojis!.contains(emoji)) {
if (user.lastUsedEditorEmojis!.contains(emoji)) { user.lastUsedEditorEmojis!.remove(emoji);
user.lastUsedEditorEmojis!.remove(emoji); }
user.lastUsedEditorEmojis!.insert(0, emoji);
if (user.lastUsedEditorEmojis!.length > 12) {
user.lastUsedEditorEmojis = user.lastUsedEditorEmojis!.sublist(0, 12);
}
user.lastUsedEditorEmojis!.toSet().toList();
} }
user.lastUsedEditorEmojis!.insert(0, emoji); return user;
if (user.lastUsedEditorEmojis!.length > 12) { });
user.lastUsedEditorEmojis = user.lastUsedEditorEmojis!.sublist(0, 12);
}
user.lastUsedEditorEmojis!.toSet().toList();
}
await updateUser(user);
if (!mounted) return; if (!mounted) return;
Navigator.pop( Navigator.pop(
context, context,

View file

@ -233,11 +233,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
maxShowTime = gMediaShowInfinite; maxShowTime = gMediaShowInfinite;
} }
setState(() {}); setState(() {});
var user = await getUser(); await updateUserdata((user) {
if (user != null) {
user.defaultShowTime = maxShowTime; user.defaultShowTime = maxShowTime;
updateUser(user); return user;
} });
}, },
), ),
), ),

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/services/backup.identitiy.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/settings/backup/twonly_identity_backup.view.dart'; import 'package:twonly/src/views/settings/backup/twonly_identity_backup.view.dart';
@ -22,10 +23,10 @@ class _BackupViewState extends State<BackupView> {
} }
Future initAsync() async { Future initAsync() async {
setState(() { _twonlyIdBackupEnabled = await isIdentityBackupEnabled();
_twonlyIdBackupEnabled = true; _twonlyIdLastBackup = await getLastIdentityBackup();
_dataBackupEnabled = false; _dataBackupEnabled = false;
}); setState(() {});
} }
@override @override
@ -37,7 +38,7 @@ class _BackupViewState extends State<BackupView> {
body: ListView( body: ListView(
children: [ children: [
BackupOption( BackupOption(
title: 'twonly-Identity Backup', title: 'twonly Safe',
description: description:
'Back up your twonly identity, as this is the only way to restore your account if you uninstall or lose your phone.', 'Back up your twonly identity, as this is the only way to restore your account if you uninstall or lose your phone.',
lastBackup: _twonlyIdLastBackup, lastBackup: _twonlyIdLastBackup,
@ -51,7 +52,7 @@ class _BackupViewState extends State<BackupView> {
}, },
), ),
BackupOption( BackupOption(
title: 'Daten-Backup', title: 'Daten-Backup (Coming Soon)',
description: description:
'This backup contains besides of your twonly-Identity also all of your media files. This backup will also be encrypted using a password chosen by the user but stored locally on the smartphone. You then have to ensure to manually copy it onto your laptop or device of your choice.', 'This backup contains besides of your twonly-Identity also all of your media files. This backup will also be encrypted using a password chosen by the user but stored locally on the smartphone. You then have to ensure to manually copy it onto your laptop or device of your choice.',
autoBackupEnabled: _dataBackupEnabled, autoBackupEnabled: _dataBackupEnabled,

View file

@ -13,12 +13,14 @@ class _TwonlyIdentityBackupViewState extends State<TwonlyIdentityBackupView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text("twonly-Identity Backup"), title: Text("twonly Safe"),
),
body: ListView(
children: [
Text(
'Backup of your twonly-Identity. As twonly does not have any second factor like your phone number or email, this backup contains your twonly-Identity. If you lose your device, the only option to recover is with the twonly-ID Backup. This backup will be protected by a password chosen by you in the next step and anonymously uploaded to the twonly servers. Read more [here](https://twonly.eu/s/backup).'),
],
), ),
body: ListView(children: [
Text(
'Backup of your twonly-Identity. As twonly does not have any second factor like your phone number or email, this backup contains your twonly-Identity. If you lose your device, the only option to recover is with the twonly-ID Backup. This backup will be protected by a password chosen by you in the next step and anonymously uploaded to the twonly servers. Read more [here](https://twonly.eu/s/backup).'),
]),
); );
} }
} }

View file

@ -36,11 +36,10 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
} else { } else {
if (selectedEmojis.length < 12) { if (selectedEmojis.length < 12) {
selectedEmojis.add(emoji); selectedEmojis.add(emoji);
var user = await getUser(); await updateUserdata((user) {
if (user != null) {
user.preSelectedEmojies = selectedEmojis; user.preSelectedEmojies = selectedEmojis;
await updateUser(user); return user;
} });
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@ -98,11 +97,10 @@ class _ChatReactionSelectionView extends State<ChatReactionSelectionView> {
selectedEmojis = selectedEmojis =
EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6); EmojiAnimation.animatedIcons.keys.toList().sublist(0, 6);
setState(() {}); setState(() {});
var user = await getUser(); await updateUserdata((user) {
if (user != null) {
user.preSelectedEmojies = selectedEmojis; user.preSelectedEmojies = selectedEmojis;
await updateUser(user); return user;
} });
}, },
child: Icon(Icons.settings_backup_restore_rounded), child: Icon(Icons.settings_backup_restore_rounded),
), ),

View file

@ -48,10 +48,10 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
} }
void toggleStoreInGallery() async { void toggleStoreInGallery() async {
final user = await getUser(); await updateUserdata((u) {
if (user == null) return; u.storeMediaFilesInGallery = !storeMediaFilesInGallery;
user.storeMediaFilesInGallery = !storeMediaFilesInGallery; return u;
await updateUser(user); });
initAsync(); initAsync();
} }
@ -179,10 +179,12 @@ class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
} }
// Call the onUpdate callback to notify the parent widget // Call the onUpdate callback to notify the parent widget
final user = await getUser();
if (user == null) return; await updateUserdata((u) {
user.autoDownloadOptions = autoDownloadOptions; u.autoDownloadOptions = autoDownloadOptions;
await updateUser(user); return u;
});
widget.onUpdate(); widget.onUpdate();
setState(() {}); setState(() {});
} }

View file

@ -39,10 +39,10 @@ class HelpView extends StatelessWidget {
title: Text(context.lang.settingsResetTutorials), title: Text(context.lang.settingsResetTutorials),
subtitle: Text(context.lang.settingsResetTutorialsDesc), subtitle: Text(context.lang.settingsResetTutorialsDesc),
onTap: () async { onTap: () async {
final user = await getUser(); updateUserdata((user) {
if (user == null) return; user.tutorialDisplayed = [];
user.tutorialDisplayed = []; return user;
await updateUser(user); });
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(

View file

@ -2,7 +2,6 @@ import 'dart:math';
import 'package:avatar_maker/avatar_maker.dart'; import 'package:avatar_maker/avatar_maker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import "package:get/get.dart"; import "package:get/get.dart";
@ -12,17 +11,16 @@ class ModifyAvatar extends StatelessWidget {
const ModifyAvatar({super.key}); const ModifyAvatar({super.key});
Future updateUserAvatar(String json, String svg) async { Future updateUserAvatar(String json, String svg) async {
UserData? user = await getUser(); await updateUserdata((user) {
if (user == null) return null; user.avatarJson = json;
user.avatarSvg = svg;
user.avatarJson = json; if (user.avatarCounter == null) {
user.avatarSvg = svg; user.avatarCounter = 1;
if (user.avatarCounter == null) { } else {
user.avatarCounter = 1; user.avatarCounter = user.avatarCounter! + 1;
} else { }
user.avatarCounter = user.avatarCounter! + 1; return user;
} });
await updateUser(user);
await notifyContactsAboutProfileChange(); await notifyContactsAboutProfileChange();
} }

View file

@ -29,16 +29,17 @@ class _ProfileViewState extends State<ProfileView> {
setState(() {}); setState(() {});
} }
Future updateUserDisplayname(String displayName) async { Future updateUserDisplayName(String displayName) async {
UserData? user = await getUser(); await updateUserdata((user) {
if (user == null) return null; user.displayName = displayName;
user.displayName = displayName; if (user.avatarCounter == null) {
if (user.avatarCounter == null) { user.avatarCounter = 1;
user.avatarCounter = 1; } else {
} else { user.avatarCounter = user.avatarCounter! + 1;
user.avatarCounter = user.avatarCounter! + 1; }
} return user;
await updateUser(user); });
await notifyContactsAboutProfileChange(); await notifyContactsAboutProfileChange();
initAsync(); initAsync();
} }
@ -83,7 +84,7 @@ class _ProfileViewState extends State<ProfileView> {
final displayName = final displayName =
await showDisplayNameChangeDialog(context, user!.displayName); await showDisplayNameChangeDialog(context, user!.displayName);
if (context.mounted && displayName != null && displayName != "") { if (context.mounted && displayName != null && displayName != "") {
updateUserDisplayname(displayName); updateUserDisplayName(displayName);
} }
}, },
), ),

View file

@ -7,6 +7,7 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/settings/account.view.dart'; import 'package:twonly/src/views/settings/account.view.dart';
import 'package:twonly/src/views/settings/appearance.view.dart'; import 'package:twonly/src/views/settings/appearance.view.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart';
import 'package:twonly/src/views/settings/chat/chat_settings.view.dart'; import 'package:twonly/src/views/settings/chat/chat_settings.view.dart';
import 'package:twonly/src/views/settings/data_and_storage.view.dart'; import 'package:twonly/src/views/settings/data_and_storage.view.dart';
import 'package:twonly/src/views/settings/notification.view.dart'; import 'package:twonly/src/views/settings/notification.view.dart';
@ -120,16 +121,16 @@ class _SettingsMainViewState extends State<SettingsMainView> {
})); }));
}, },
), ),
// BetterListTile( BetterListTile(
// icon: Icons.lock_clock_rounded, icon: Icons.lock_clock_rounded,
// text: context.lang.settingsBackup, text: context.lang.settingsBackup,
// onTap: () { onTap: () {
// Navigator.push(context, Navigator.push(context,
// MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
// return BackupView(); return BackupView();
// })); }));
// }, },
// ), ),
const Divider(), const Divider(),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.sun, icon: FontAwesomeIcons.sun,

View file

@ -14,24 +14,27 @@ import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
Future<List<Response_AddAccountsInvite>?> loadAdditionalUserInvites() async { Future<List<Response_AddAccountsInvite>?> loadAdditionalUserInvites() async {
List<Response_AddAccountsInvite>? ballance; final ballance = await apiService.getAdditionalUserInvites();
final user = await getUser();
if (user == null) return ballance;
ballance = await apiService.getAdditionalUserInvites();
if (ballance != null) { if (ballance != null) {
user.additionalUserInvites = await updateUserdata((u) {
jsonEncode(ballance.map((x) => x.writeToJson()).toList()); u.additionalUserInvites =
await updateUser(user); jsonEncode(ballance.map((x) => x.writeToJson()).toList());
} else if (user.lastPlanBallance != null) { return u;
});
return ballance;
}
final user = await getUser();
if (user != null && user.lastPlanBallance != null) {
try { try {
List<String> decoded = jsonDecode(user.additionalUserInvites!); List<String> decoded = jsonDecode(user.additionalUserInvites!);
ballance = return decoded
decoded.map((x) => Response_AddAccountsInvite.fromJson(x)).toList(); .map((x) => Response_AddAccountsInvite.fromJson(x))
.toList();
} catch (e) { } catch (e) {
Log.error("from json: $e"); Log.error("from json: $e");
} }
} }
return ballance; return null;
} }
class AdditionalUsersView extends StatefulWidget { class AdditionalUsersView extends StatefulWidget {

View file

@ -36,16 +36,18 @@ String localePrizing(BuildContext context, int cents) {
} }
Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async { Future<Response_PlanBallance?> loadPlanBalance({bool useCache = true}) async {
Response_PlanBallance? ballance; final ballance = await apiService.getPlanBallance();
final user = await getUser();
if (user == null) return ballance;
ballance = await apiService.getPlanBallance();
if (ballance != null) { if (ballance != null) {
user.lastPlanBallance = ballance.writeToJson(); updateUserdata((u) {
await updateUser(user); u.lastPlanBallance = ballance.writeToJson();
} else if (user.lastPlanBallance != null && useCache) { return u;
});
return ballance;
}
final user = await getUser();
if (user != null && user.lastPlanBallance != null && useCache) {
try { try {
ballance = Response_PlanBallance.fromJson( return Response_PlanBallance.fromJson(
user.lastPlanBallance!, user.lastPlanBallance!,
); );
} catch (e) { } catch (e) {

View file

@ -47,7 +47,12 @@ Future<bool> checkIfTutorialAlreadyShown(String tutorialId) async {
return true; return true;
} }
user.tutorialDisplayed!.add(tutorialId); user.tutorialDisplayed!.add(tutorialId);
await updateUser(user);
await updateUserdata((u) {
u.tutorialDisplayed = user.tutorialDisplayed;
return u;
});
return false; return false;
} }