This commit is contained in:
otsmr 2025-06-03 15:19:35 +02:00
parent 036c4cab77
commit 588a790554
24 changed files with 171 additions and 97 deletions

View file

@ -3,6 +3,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api/media_send.dart';
import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/onboarding.view.dart'; import 'package:twonly/src/views/onboarding.view.dart';
@ -71,7 +72,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
Future initAsync() async { Future initAsync() async {
setUserPlan(); setUserPlan();
apiService.connect(); await apiService.connect();
// call this function so invalid media files are get purged
retryMediaUpload(true);
} }
@override @override

View file

@ -42,6 +42,7 @@ void main() async {
apiService = ApiService(); apiService = ApiService();
twonlyDB = TwonlyDatabase(); twonlyDB = TwonlyDatabase();
await twonlyDB.messagesDao.resetPendingDownloadState(); await twonlyDB.messagesDao.resetPendingDownloadState();
await twonlyDB.messagesDao.handleMediaFilesOlderThan7Days();
// purge media files in the background // purge media files in the background
purgeReceivedMediaFiles(); purgeReceivedMediaFiles();

View file

@ -69,6 +69,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.write(MessagesCompanion(contentJson: Value(null))); .write(MessagesCompanion(contentJson: Value(null)));
} }
Future handleMediaFilesOlderThan7Days() {
/// media files will be deleted by the server after 7 days, so delete them here also
return (update(messages)
..where(
(t) => (t.kind.equals(MessageKind.media.name) &
t.openedAt.isNull() &
t.messageOtherId.isNull() &
(t.sendAt.isSmallerThanValue(
DateTime.now().subtract(
Duration(days: 8),
),
))),
))
.write(MessagesCompanion(errorWhileSending: Value(true)));
}
Future<List<Message>> getAllMessagesPendingDownloading() { Future<List<Message>> getAllMessagesPendingDownloading() {
return (select(messages) return (select(messages)
..where( ..where(

View file

@ -13,7 +13,8 @@ enum MessageKind {
flameSync, flameSync,
opened, opened,
ack, ack,
pushKey pushKey,
receiveMediaError,
} }
enum DownloadState { enum DownloadState {

View file

@ -228,7 +228,7 @@
"checkoutOptions": "Optionen", "checkoutOptions": "Optionen",
"checkoutPayYearly": "Jährlich bezahlen", "checkoutPayYearly": "Jährlich bezahlen",
"checkoutTotal": "Gesamt", "checkoutTotal": "Gesamt",
"selectPaymentMethode": "Zahlungsmethode auswählen", "selectPaymentMethod": "Zahlungsmethode auswählen",
"twonlyCredit": "twonly-Guthaben", "twonlyCredit": "twonly-Guthaben",
"notEnoughCredit": "Du hast nicht genügend Guthaben!", "notEnoughCredit": "Du hast nicht genügend Guthaben!",
"chargeCredit": "Guthaben aufladen", "chargeCredit": "Guthaben aufladen",

View file

@ -386,7 +386,7 @@
"refund": "Refund", "refund": "Refund",
"checkoutPayYearly": "Pay yearly", "checkoutPayYearly": "Pay yearly",
"checkoutTotal": "Total", "checkoutTotal": "Total",
"selectPaymentMethode": "Select Payment Method", "selectPaymentMethod": "Select Payment Method",
"twonlyCredit": "twonly-Credit", "twonlyCredit": "twonly-Credit",
"notEnoughCredit": "You do not have enough credit!", "notEnoughCredit": "You do not have enough credit!",
"chargeCredit": "Charge credit", "chargeCredit": "Charge credit",

View file

@ -1382,11 +1382,11 @@ abstract class AppLocalizations {
/// **'Total'** /// **'Total'**
String get checkoutTotal; String get checkoutTotal;
/// No description provided for @selectPaymentMethode. /// No description provided for @selectPaymentMethod.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Select Payment Method'** /// **'Select Payment Method'**
String get selectPaymentMethode; String get selectPaymentMethod;
/// No description provided for @twonlyCredit. /// No description provided for @twonlyCredit.
/// ///

View file

@ -725,7 +725,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get checkoutTotal => 'Gesamt'; String get checkoutTotal => 'Gesamt';
@override @override
String get selectPaymentMethode => 'Zahlungsmethode auswählen'; String get selectPaymentMethod => 'Zahlungsmethode auswählen';
@override @override
String get twonlyCredit => 'twonly-Guthaben'; String get twonlyCredit => 'twonly-Guthaben';

View file

@ -720,7 +720,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get checkoutTotal => 'Total'; String get checkoutTotal => 'Total';
@override @override
String get selectPaymentMethode => 'Select Payment Method'; String get selectPaymentMethod => 'Select Payment Method';
@override @override
String get twonlyCredit => 'twonly-Credit'; String get twonlyCredit => 'twonly-Credit';

View file

@ -39,11 +39,12 @@ class MessageJson {
final int? messageId; final int? messageId;
DateTime timestamp; DateTime timestamp;
MessageJson( MessageJson({
{required this.kind, required this.kind,
this.messageId, this.messageId,
required this.content, required this.content,
required this.timestamp}); required this.timestamp,
});
@override @override
String toString() { String toString() {

View file

@ -51,7 +51,7 @@ class ApiService {
// reconnection params // reconnection params
Timer? reconnectionTimer; Timer? reconnectionTimer;
// int _reconnectionDelay = 5; int _reconnectionDelay = 5;
final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap(); final HashMap<Int64, server.ServerToClient?> messagesV0 = HashMap();
IOWebSocketChannel? _channel; IOWebSocketChannel? _channel;
@ -81,7 +81,7 @@ class ApiService {
if (!globalIsAppInBackground) { if (!globalIsAppInBackground) {
retransmitRawBytes(); retransmitRawBytes();
tryTransmitMessages(); tryTransmitMessages();
retryMediaUpload(); retryMediaUpload(false);
tryDownloadAllMediaFiles(); tryDownloadAllMediaFiles();
notifyContactsAboutProfileChange(); notifyContactsAboutProfileChange();
twonlyDB.markUpdated(); twonlyDB.markUpdated();
@ -92,6 +92,7 @@ class ApiService {
Future onConnected() async { Future onConnected() async {
await authenticate(); await authenticate();
_reconnectionDelay = 5;
globalCallbackConnectionState(true); globalCallbackConnectionState(true);
} }
@ -100,6 +101,12 @@ class ApiService {
isAuthenticated = false; isAuthenticated = false;
globalCallbackConnectionState(false); globalCallbackConnectionState(false);
await twonlyDB.messagesDao.resetPendingDownloadState(); await twonlyDB.messagesDao.resetPendingDownloadState();
reconnectionTimer ??= Timer(Duration(seconds: _reconnectionDelay), () {
Log.info("starting with reconnection.");
reconnectionTimer = null;
connect();
});
_reconnectionDelay += 5;
} }
Future close(Function callback) async { Future close(Function callback) async {
@ -113,11 +120,13 @@ class ApiService {
callback(); callback();
} }
Future<bool> connect() async { Future<bool> connect({bool force = false}) async {
if (reconnectionTimer != null && !force) {
return false;
}
reconnectionTimer?.cancel();
final user = await getUser(); final user = await getUser();
if (user != null && user.isDemoUser) { if (user != null && user.isDemoUser) {
print("DEMO user");
// the demo user should not be able to connect to the API server...
globalCallbackConnectionState(true); globalCallbackConnectionState(true);
return false; return false;
} }
@ -126,9 +135,7 @@ class ApiService {
return true; return true;
} }
// ensure that the connect function is not called again by the timer. // ensure that the connect function is not called again by the timer.
if (reconnectionTimer != null) { reconnectionTimer?.cancel();
reconnectionTimer!.cancel();
}
isAuthenticated = false; isAuthenticated = false;

View file

@ -12,8 +12,7 @@ import 'package:http/http.dart' as http;
// import 'package:twonly/src/providers/api/api_utils.dart'; // import 'package:twonly/src/providers/api/api_utils.dart';
import 'package:twonly/src/services/api/media_send.dart'; import 'package:twonly/src/services/api/media_send.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart' import 'package:twonly/src/services/api/utils.dart';
as client;
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -75,7 +74,8 @@ Future<bool> isAllowedToDownload(bool isVideo) async {
return false; return false;
} }
Future startDownloadMedia(Message message, bool force) async { Future startDownloadMedia(Message message, bool force,
{int retryCounter = 0}) async {
if (message.contentJson == null) return; if (message.contentJson == null) return;
if (downloadStartedForMediaReceived[message.messageId] != null) { if (downloadStartedForMediaReceived[message.messageId] != null) {
DateTime started = downloadStartedForMediaReceived[message.messageId]!; DateTime started = downloadStartedForMediaReceived[message.messageId]!;
@ -151,12 +151,10 @@ Future startDownloadMedia(Message message, bool force) async {
}, onDone: () async { }, onDone: () async {
if (r.statusCode != 200) { if (r.statusCode != 200) {
Log.error("Download error: $r"); Log.error("Download error: $r");
await twonlyDB.messagesDao.updateMessageByMessageId( if (r.statusCode == 418) {
message.messageId, Log.error("Got custom error code: ${chunks.toList()}");
MessagesCompanion( handleMediaError(message);
errorWhileSending: Value(true), }
),
);
return; return;
} }
@ -172,13 +170,18 @@ Future startDownloadMedia(Message message, bool force) async {
offset += chunk.length; offset += chunk.length;
} }
await writeMediaFile(message.messageId, "encrypted", bytes); await writeMediaFile(message.messageId, "encrypted", bytes);
handleEncryptedFile(message, encryptedBytesTmp: bytes); handleEncryptedFile(message,
encryptedBytesTmp: bytes, retryCounter: retryCounter);
return; return;
}); });
}); });
} }
Future handleEncryptedFile(Message msg, {Uint8List? encryptedBytesTmp}) async { Future handleEncryptedFile(
Message msg, {
Uint8List? encryptedBytesTmp,
int retryCounter = 0,
}) async {
Uint8List? encryptedBytes = Uint8List? encryptedBytes =
encryptedBytesTmp ?? await readMediaFile(msg.messageId, "encrypted"); encryptedBytesTmp ?? await readMediaFile(msg.messageId, "encrypted");
@ -211,16 +214,16 @@ Future handleEncryptedFile(Message msg, {Uint8List? encryptedBytesTmp}) async {
await writeMediaFile(msg.messageId, "png", imageBytes); await writeMediaFile(msg.messageId, "png", imageBytes);
} catch (e) { } catch (e) {
Log.error("Decryption error: $e"); if (retryCounter >= 1) {
await twonlyDB.messagesDao.updateMessageByMessageId( Log.error(
msg.messageId, "could not decrypt the media file in the second try. reporting error to user: $e");
MessagesCompanion( handleMediaError(msg);
errorWhileSending: Value(true), return;
), }
); Log.error("could not decrypt the media file trying again: $e");
// answers with ok, so the server will delete the message startDownloadMedia(msg, true, retryCounter: retryCounter + 1);
var ok = client.Response_Ok()..none = true; // try downloading again....
return client.Response()..ok = ok; return;
} }
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(

View file

@ -103,7 +103,7 @@ Future<bool> checkForFailedUploads() async {
} }
final lockingHandleMediaFile = Mutex(); final lockingHandleMediaFile = Mutex();
Future retryMediaUpload({int maxRetries = 3}) async { Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
if (maxRetries == 0) { if (maxRetries == 0) {
Log.error("retried media upload 3 times. abort retrying"); Log.error("retried media upload 3 times. abort retrying");
return; return;
@ -116,16 +116,23 @@ Future retryMediaUpload({int maxRetries = 3}) async {
Log.info("re uploading ${mediaFiles.length} media files."); Log.info("re uploading ${mediaFiles.length} media files.");
for (final mediaFile in mediaFiles) { for (final mediaFile in mediaFiles) {
if (mediaFile.messageIds == null || mediaFile.metadata == null) { if (mediaFile.messageIds == null || mediaFile.metadata == null) {
if (appRestarted) {
/// When the app got restarted and the messageIds or the metadata is not
/// set then the app was closed before the images was send.
// the media upload was canceled, // the media upload was canceled,
if (mediaFile.uploadTokens != null) { if (mediaFile.uploadTokens != null) {
/// the file was already uploaded. /// the file was already uploaded.
/// notify the server to remove the upload /// notify the server to remove the upload
apiService.getDownloadTokens(mediaFile.uploadTokens!.uploadToken, 0); apiService.getDownloadTokens(
mediaFile.uploadTokens!.uploadToken, 0);
} }
await twonlyDB.mediaUploadsDao await twonlyDB.mediaUploadsDao
.deleteMediaUpload(mediaFile.mediaUploadId); .deleteMediaUpload(mediaFile.mediaUploadId);
Log.info( Log.info(
"upload can be removed, the finalized function was never called..."); "upload can be removed, the finalized function was never called...",
);
}
continue; continue;
} }
@ -138,7 +145,7 @@ Future retryMediaUpload({int maxRetries = 3}) async {
return false; return false;
}); });
if (retry) { if (retry) {
await retryMediaUpload(maxRetries: maxRetries - 1); await retryMediaUpload(false, maxRetries: maxRetries - 1);
} }
} }

View file

@ -112,6 +112,7 @@ Future<Map<String, dynamic>> getAllMessagesForRetransmitting() async {
Future<Result> sendRetransmitMessage( Future<Result> sendRetransmitMessage(
String stateId, RetransmitMessage msg) async { String stateId, RetransmitMessage msg) async {
Log.info("Sending ${msg.messageId}");
Result resp = Result resp =
await apiService.sendTextMessage(msg.userId, msg.bytes, msg.pushData); await apiService.sendTextMessage(msg.userId, msg.bytes, msg.pushData);
@ -130,8 +131,8 @@ Future<Result> sendRetransmitMessage(
} }
if (resp.isSuccess) { if (resp.isSuccess) {
if (msg.messageId != null) {
retry = false; retry = false;
if (msg.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
msg.messageId!, msg.messageId!,
MessagesCompanion(acknowledgeByServer: Value(true)), MessagesCompanion(acknowledgeByServer: Value(true)),

View file

@ -86,7 +86,19 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
} }
} }
case MessageKind.receiveMediaError:
if (message.messageId != null) {
await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId,
message.messageId!,
MessagesCompanion(
errorWhileSending: Value(true),
),
);
}
case MessageKind.opened: case MessageKind.opened:
if (message.messageId != null) {
final update = MessagesCompanion(openedAt: Value(message.timestamp)); final update = MessagesCompanion(openedAt: Value(message.timestamp));
await twonlyDB.messagesDao.updateMessageByOtherUser( await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId, fromUserId,
@ -102,7 +114,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
fromUserId, fromUserId,
); );
} }
}
break; break;
case MessageKind.rejectRequest: case MessageKind.rejectRequest:

View file

@ -1,6 +1,8 @@
import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart' import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart'
as client; as client;
@ -66,3 +68,23 @@ Future rejectUser(int contactId) async {
), ),
); );
} }
Future handleMediaError(Message message) async {
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
MessagesCompanion(
errorWhileSending: Value(true),
),
);
if (message.messageOtherId != null) {
encryptAndSendMessageAsync(
null,
message.contactId,
MessageJson(
kind: MessageKind.receiveMediaError,
timestamp: DateTime.now(),
content: MessageContent(),
messageId: message.messageOtherId),
);
}
}

View file

@ -112,7 +112,7 @@ String formatDuration(int seconds) {
} }
} }
InputDecoration getInputDecoration(context, hintText) { InputDecoration getInputDecoration(BuildContext context, String hintText) {
final primaryColor = final primaryColor =
Theme.of(context).colorScheme.primary; // Get the primary color Theme.of(context).colorScheme.primary; // Get the primary color
return InputDecoration( return InputDecoration(
@ -279,7 +279,7 @@ Future insertDemoContacts() async {
if (config['accepted'] ?? false) { if (config['accepted'] ?? false) {
for (var i = 0; i < 20; i++) { for (var i = 0; i < 20; i++) {
int chatId = Random().nextInt(chatMessages.length); int chatId = Random().nextInt(chatMessages.length);
int? messageId = await twonlyDB.messagesDao.insertMessage( await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
contactId: Value(userId), contactId: Value(userId),
kind: Value(MessageKind.textMessage), kind: Value(MessageKind.textMessage),

View file

@ -1,7 +1,10 @@
import 'dart:convert'; import 'dart:convert';
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:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<bool> isUserCreated() async { Future<bool> isUserCreated() async {
@ -28,6 +31,17 @@ Future<UserData?> getUser() async {
} }
} }
Future updateUsersPlan(BuildContext context, String planId) async {
context.read<CustomChangeProvider>().plan = planId;
var user = await getUser();
if (user != null) {
user.subscriptionPlan = planId;
await updateUser(user);
}
if (!context.mounted) return;
context.read<CustomChangeProvider>().updatePlan(planId);
}
Future updateUser(UserData userData) async { Future updateUser(UserData userData) async {
final storage = FlutterSecureStorage(); final storage = FlutterSecureStorage();
storage.write(key: "userData", value: jsonEncode(userData)); storage.write(key: "userData", value: jsonEncode(userData));

View file

@ -173,7 +173,7 @@ class _ChatListViewState extends State<ChatListView> {
: RefreshIndicator( : RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await apiService.close(() {}); await apiService.close(() {});
await apiService.connect(); await apiService.connect(force: true);
await Future.delayed(Duration(seconds: 1)); await Future.delayed(Duration(seconds: 1));
}, },
child: ListView.builder( child: ListView.builder(

View file

@ -8,6 +8,7 @@ import 'package:lottie/lottie.dart';
import 'package:no_screenshot/no_screenshot.dart'; import 'package:no_screenshot/no_screenshot.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';
@ -252,13 +253,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if ((imageBytes == null && !content.isVideo) || if ((imageBytes == null && !content.isVideo) ||
(content.isVideo && videoController == null)) { (content.isVideo && videoController == null)) {
Log.error("media files are not found...");
// When the message should be downloaded but imageBytes are null then a error happened // When the message should be downloaded but imageBytes are null then a error happened
await twonlyDB.messagesDao.updateMessageByMessageId( await handleMediaError(current);
current.messageId,
MessagesCompanion(
errorWhileSending: Value(true),
),
);
return nextMediaOrExit(); return nextMediaOrExit();
} }

View file

@ -68,7 +68,7 @@ class HomeViewState extends State<HomeView> {
offsetRatio = offsetFromOne.abs(); offsetRatio = offsetFromOne.abs();
}); });
} }
if (cameraController == null && !initCameraStarted) { if (cameraController == null && !initCameraStarted && offsetRatio < 1) {
initCameraStarted = true; initCameraStarted = true;
selectCamera(selectedCameraDetails.cameraId, false, false); selectCamera(selectedCameraDetails.cameraId, false, false);
} }

View file

@ -135,7 +135,7 @@ class _CheckoutViewState extends State<CheckoutView> {
Navigator.pop(context); Navigator.pop(context);
} }
}, },
child: Text(context.lang.selectPaymentMethode)), child: Text(context.lang.selectPaymentMethod)),
), ),
SizedBox(height: 20) SizedBox(height: 20)
], ],

View file

@ -1,8 +1,6 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/misc.dart'; 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/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
@ -82,7 +80,7 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
(balanceInCents == null || balanceInCents! >= checkoutInCents)); (balanceInCents == null || balanceInCents! >= checkoutInCents));
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.selectPaymentMethode), title: Text(context.lang.selectPaymentMethod),
), ),
body: SafeArea( body: SafeArea(
child: Column( child: Column(
@ -222,21 +220,12 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
widget.planId!, widget.payMonthly!, tryAutoRenewal); widget.planId!, widget.payMonthly!, tryAutoRenewal);
if (!context.mounted) return; if (!context.mounted) return;
if (res.isSuccess) { if (res.isSuccess) {
context.read<CustomChangeProvider>().plan = await updateUsersPlan(context, widget.planId!);
widget.planId!;
var user = await getUser();
if (user != null) {
user.subscriptionPlan = widget.planId!;
await updateUser(user);
}
if (!context.mounted) return; if (!context.mounted) return;
context
.read<CustomChangeProvider>()
.updatePlan(widget.planId!);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: content: Text(context.lang.planSuccessUpgraded),
Text(context.lang.planSuccessUpgraded)), ),
); );
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
} else { } else {

View file

@ -556,12 +556,12 @@ Future redeemUserInviteCode(BuildContext context, String newPlan) async {
if (res.isSuccess) { if (res.isSuccess) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(context.lang.redeemUserInviteCodeSuccess)), content: Text(context.lang.redeemUserInviteCodeSuccess),
),
); );
// reconnect to load new plan. // reconnect to load new plan.
apiService.close(() { await apiService.close(() {});
apiService.connect(); await apiService.connect();
});
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorCodeToText(context, res.error))), SnackBar(content: Text(errorCodeToText(context, res.error))),