diff --git a/lib/app.dart b/lib/app.dart index 5b430a7..aa43d90 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,6 +6,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/home.view.dart'; @@ -36,8 +37,8 @@ class _AppState extends State with WidgetsBindingObserver { await setUserPlan(); }; - globalCallbackUpdatePlan = (String planId) async { - await context.read().updatePlan(planId); + globalCallbackUpdatePlan = (SubscriptionPlan plan) async { + await context.read().updatePlan(plan); }; unawaited(initAsync()); @@ -47,9 +48,9 @@ class _AppState extends State with WidgetsBindingObserver { final user = await getUser(); if (user != null && mounted) { if (mounted) { - await context - .read() - .updatePlan(user.subscriptionPlan); + await context.read().updatePlan( + planFromString(user.subscriptionPlan), + ); } } } @@ -79,7 +80,7 @@ class _AppState extends State with WidgetsBindingObserver { void dispose() { WidgetsBinding.instance.removeObserver(this); globalCallbackConnectionState = ({required bool isConnected}) {}; - globalCallbackUpdatePlan = (String planId) {}; + globalCallbackUpdatePlan = (SubscriptionPlan planId) {}; super.dispose(); } diff --git a/lib/globals.dart b/lib/globals.dart index 3bf3b22..5a9acbf 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -4,6 +4,7 @@ import 'package:camera/camera.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/services/api.service.dart'; +import 'package:twonly/src/services/subscription.service.dart'; late ApiService apiService; @@ -26,7 +27,8 @@ void Function({required bool isConnected}) globalCallbackConnectionState = ({ }) {}; void Function() globalCallbackAppIsOutdated = () {}; void Function() globalCallbackNewDeviceRegistered = () {}; -void Function(String planId) globalCallbackUpdatePlan = (String planId) {}; +void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = + (SubscriptionPlan plan) {}; Map globalUserDataChangedCallBack = {}; diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 3565b03..0dd04e8 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -321,8 +321,8 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { if (updateFlame) { flameCounter += 1; lastFlameCounterChange = Value(timestamp); - if (flameCounter > maxFlameCounter) { - maxFlameCounter = flameCounter; + if ((flameCounter + 1) >= maxFlameCounter) { + maxFlameCounter = flameCounter + 1; maxFlameCounterFrom = DateTime.now(); } } diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index ecbfeee..8b44397 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -105,16 +105,6 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { ..where((t) => t.receiptId.equals(receiptId))) .getSingleOrNull() != null; - // try { - // return await (select() - // ..where( - // (t) => t.receiptId.equals(receiptId), - // )) - // .getSingleOrNull(); - // } catch (e) { - // Log.error(e); - // return null; - // } } Future gotReceipt(String receiptId) async { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index b2f5f4b..182be3d 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -411,7 +411,7 @@ "@proFeature1": {}, "proFeature2": "1 zusätzlicher Plus Benutzer", "@proFeature2": {}, - "proFeature3": "Zusatzfunktionen (coming-soon)", + "proFeature3": "Flammen wiederherstellen", "@proFeature3": {}, "proFeature4": "Cloud-Backup verschlüsselt (coming-soon)", "@proFeature4": {}, diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index a58f148..bc98dea 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -705,7 +705,7 @@ class AppLocalizationsDe extends AppLocalizations { String get proFeature2 => '1 zusätzlicher Plus Benutzer'; @override - String get proFeature3 => 'Zusatzfunktionen (coming-soon)'; + String get proFeature3 => 'Flammen wiederherstellen'; @override String get proFeature4 => 'Cloud-Backup verschlüsselt (coming-soon)'; diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index 2586ee4..3babd94 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -1251,6 +1251,7 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage { $fixnum.Int64? flameCounter, $fixnum.Int64? lastFlameCounterChange, $core.bool? bestFriend, + $core.bool? forceUpdate, }) { final $result = create(); if (flameCounter != null) { @@ -1262,6 +1263,9 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage { if (bestFriend != null) { $result.bestFriend = bestFriend; } + if (forceUpdate != null) { + $result.forceUpdate = forceUpdate; + } return $result; } EncryptedContent_FlameSync._() : super(); @@ -1272,6 +1276,7 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage { ..aInt64(1, _omitFieldNames ? '' : 'flameCounter', protoName: 'flameCounter') ..aInt64(2, _omitFieldNames ? '' : 'lastFlameCounterChange', protoName: 'lastFlameCounterChange') ..aOB(3, _omitFieldNames ? '' : 'bestFriend', protoName: 'bestFriend') + ..aOB(4, _omitFieldNames ? '' : 'forceUpdate', protoName: 'forceUpdate') ..hasRequiredFields = false ; @@ -1322,6 +1327,15 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage { $core.bool hasBestFriend() => $_has(2); @$pb.TagNumber(3) void clearBestFriend() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get forceUpdate => $_getBF(3); + @$pb.TagNumber(4) + set forceUpdate($core.bool v) { $_setBool(3, v); } + @$pb.TagNumber(4) + $core.bool hasForceUpdate() => $_has(3); + @$pb.TagNumber(4) + void clearForceUpdate() => clearField(4); } class EncryptedContent extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 37824d4..a0fd2af 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -365,6 +365,7 @@ const EncryptedContent_FlameSync$json = { {'1': 'flameCounter', '3': 1, '4': 1, '5': 3, '10': 'flameCounter'}, {'1': 'lastFlameCounterChange', '3': 2, '4': 1, '5': 3, '10': 'lastFlameCounterChange'}, {'1': 'bestFriend', '3': 3, '4': 1, '5': 8, '10': 'bestFriend'}, + {'1': 'forceUpdate', '3': 4, '4': 1, '5': 8, '10': 'forceUpdate'}, ], }; @@ -434,12 +435,13 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJ' 'ZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdG' 'VkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9r' - 'ZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm' + 'ZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm' 'xhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv' - 'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIKCghfZ3JvdXBJZE' - 'IPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVw' - 'ZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb2' - '50YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoM' - 'X3RleHRNZXNzYWdlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZG' - 'F0ZUIXChVfcmVzZW5kR3JvdXBQdWJsaWNLZXk='); + 'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZG' + 'F0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVf' + 'c2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZW' + 'RpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1l' + 'U3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdX' + 'BDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVi' + 'bGljS2V5'); diff --git a/lib/src/model/protobuf/client/messages.proto b/lib/src/model/protobuf/client/messages.proto index 72b3230..301ac2e 100644 --- a/lib/src/model/protobuf/client/messages.proto +++ b/lib/src/model/protobuf/client/messages.proto @@ -169,6 +169,7 @@ message EncryptedContent { int64 flameCounter = 1; int64 lastFlameCounterChange = 2; bool bestFriend = 3; + bool forceUpdate = 4; } } \ No newline at end of file diff --git a/lib/src/providers/connection.provider.dart b/lib/src/providers/connection.provider.dart index 5b63969..f09472c 100644 --- a/lib/src/providers/connection.provider.dart +++ b/lib/src/providers/connection.provider.dart @@ -1,15 +1,16 @@ import 'package:flutter/foundation.dart'; +import 'package:twonly/src/services/subscription.service.dart'; class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { bool _isConnected = false; bool get isConnected => _isConnected; - String plan = 'Free'; + SubscriptionPlan plan = SubscriptionPlan.Free; Future updateConnectionState(bool update) async { _isConnected = update; notifyListeners(); } - Future updatePlan(String newPlan) async { + Future updatePlan(SubscriptionPlan newPlan) async { plan = newPlan; notifyListeners(); } diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 41d7498..0d8a829 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -35,6 +35,7 @@ import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -384,7 +385,7 @@ class ApiService { user.subscriptionPlan = authenticated.plan; return user; }); - globalCallbackUpdatePlan(authenticated.plan); + globalCallbackUpdatePlan(planFromString(authenticated.plan)); } Log.info('websocket is authenticated'); unawaited(onAuthenticated()); diff --git a/lib/src/services/api/client2client/contact.c2c.dart b/lib/src/services/api/client2client/contact.c2c.dart index 607c872..5a63668 100644 --- a/lib/src/services/api/client2client/contact.c2c.dart +++ b/lib/src/services/api/client2client/contact.c2c.dart @@ -123,18 +123,24 @@ Future handleFlameSync( Log.info('Got a flameSync from $contactId'); final group = await twonlyDB.groupsDao.getDirectChat(contactId); - if (group == null || group.lastFlameCounterChange != null) return; + if (group == null || group.lastFlameCounterChange == null) return; var updates = GroupsCompanion( alsoBestFriend: Value(flameSync.bestFriend), ); if (isToday(group.lastFlameCounterChange!) && - isToday(fromTimestamp(flameSync.lastFlameCounterChange))) { + isToday(fromTimestamp(flameSync.lastFlameCounterChange)) || + flameSync.forceUpdate) { if (flameSync.flameCounter > group.flameCounter) { - updates = GroupsCompanion( + updates = updates.copyWith( flameCounter: Value(flameSync.flameCounter.toInt()), ); } + if (flameSync.flameCounter > group.maxFlameCounter) { + updates = updates.copyWith( + maxFlameCounter: Value(flameSync.flameCounter.toInt()), + ); + } } await twonlyDB.groupsDao.updateGroup(group.groupId, updates); } diff --git a/lib/src/services/flame.service.dart b/lib/src/services/flame.service.dart index 9f05b1c..8eff7fb 100644 --- a/lib/src/services/flame.service.dart +++ b/lib/src/services/flame.service.dart @@ -9,7 +9,7 @@ import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; -Future syncFlameCounters() async { +Future syncFlameCounters({String? forceForGroup}) async { final groups = await twonlyDB.groupsDao.getAllDirectChats(); if (groups.isEmpty) return; final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max; @@ -26,8 +26,10 @@ Future syncFlameCounters() async { for (final group in groups) { if (group.lastFlameCounterChange == null) continue; if (!isToday(group.lastFlameCounterChange!)) continue; - if (group.lastFlameSync != null) { - if (isToday(group.lastFlameSync!)) continue; + if (forceForGroup == null || group.groupId != forceForGroup) { + if (group.lastFlameSync != null) { + if (isToday(group.lastFlameSync!)) continue; + } } final flameCounter = getFlameCounterFromGroup(group) - 1; @@ -49,6 +51,7 @@ Future syncFlameCounters() async { lastFlameCounterChange: Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch), bestFriend: group.groupId == bestFriend.groupId, + forceUpdate: group.groupId == forceForGroup, ), ), ); diff --git a/lib/src/services/subscription.service.dart b/lib/src/services/subscription.service.dart new file mode 100644 index 0000000..c36797a --- /dev/null +++ b/lib/src/services/subscription.service.dart @@ -0,0 +1,35 @@ +// ignore_for_file: constant_identifier_names + +import 'package:twonly/globals.dart'; + +enum SubscriptionPlan { + Free, + Tester, + Family, + Pro, + Plus, +} + +bool isAdditionalAccount(SubscriptionPlan plan) { + return plan == SubscriptionPlan.Free || plan == SubscriptionPlan.Plus; +} + +bool isPayingUser(SubscriptionPlan plan) { + return plan == SubscriptionPlan.Family || + plan == SubscriptionPlan.Pro || + plan == SubscriptionPlan.Tester; +} + +SubscriptionPlan planFromString(String value) { + final input = value.trim().toLowerCase(); + for (final v in SubscriptionPlan.values) { + final name = v.name; + final compareName = name.toLowerCase(); + if (compareName == input) return v; + } + return SubscriptionPlan.Free; +} + +SubscriptionPlan getCurrentPlan() { + return planFromString(gUser.subscriptionPlan); +} diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index 1a9f3a5..9466769 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -8,6 +8,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/log.dart'; Future isUserCreated() async { @@ -35,16 +36,19 @@ Future getUser() async { } } -Future updateUsersPlan(BuildContext context, String planId) async { - context.read().plan = planId; +Future updateUsersPlan( + BuildContext context, + SubscriptionPlan plan, +) async { + context.read().plan = plan; await updateUserdata((user) { - user.subscriptionPlan = planId; + user.subscriptionPlan = plan.name; return user; }); if (!context.mounted) return; - await context.read().updatePlan(planId); + await context.read().updatePlan(plan); } Mutex updateProtection = Mutex(); diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index 1ba8dec..c1956c9 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart'; @@ -104,7 +105,7 @@ class _ChatListViewState extends State { @override Widget build(BuildContext context) { final isConnected = context.watch().isConnected; - final planId = context.watch().plan; + final plan = context.watch().plan; return Scaffold( appBar: AppBar( title: Row( @@ -130,7 +131,7 @@ class _ChatListViewState extends State { ), const SizedBox(width: 10), const Text('twonly '), - if (planId != 'Free') + if (plan != SubscriptionPlan.Free) GestureDetector( onTap: () { Navigator.push( @@ -150,7 +151,7 @@ class _ChatListViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3), child: Text( - planId, + plan.name, style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, diff --git a/lib/src/views/components/group_context_menu.component.dart b/lib/src/views/components/group_context_menu.component.dart index 45af6a7..28808a1 100644 --- a/lib/src/views/components/group_context_menu.component.dart +++ b/lib/src/views/components/group_context_menu.component.dart @@ -82,14 +82,14 @@ class GroupContextMenu extends StatelessWidget { context.lang.groupContextMenuDeleteGroup, ); if (ok) { - // await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); - await twonlyDB.groupsDao.deleteGroup(group.groupId); - // await twonlyDB.groupsDao.updateGroup( - // group.groupId, - // const GroupsCompanion( - // deletedContent: Value(true), - // ), - // ); + await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); + // await twonlyDB.groupsDao.deleteGroup(group.groupId); + await twonlyDB.groupsDao.updateGroup( + group.groupId, + const GroupsCompanion( + deletedContent: Value(true), + ), + ); } }, ), diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart new file mode 100644 index 0000000..4c863b7 --- /dev/null +++ b/lib/src/views/components/max_flame_list_title.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'package:drift/drift.dart' show Value; +import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/flame.service.dart'; +import 'package:twonly/src/services/subscription.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/animate_icon.dart'; +import 'package:twonly/src/views/components/better_list_title.dart'; +import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; + +class MaxFlameListTitle extends StatefulWidget { + const MaxFlameListTitle({ + required this.contactId, + super.key, + }); + final int contactId; + + @override + State createState() => _MaxFlameListTitleState(); +} + +class _MaxFlameListTitleState extends State { + int _flameCounter = 0; + Group? _directChat; + late String _groupId; + + late StreamSubscription _flameCounterSub; + late StreamSubscription _groupSub; + + @override + void initState() { + _groupId = getUUIDforDirectChat(widget.contactId, gUser.userId); + final stream = twonlyDB.groupsDao.watchFlameCounter(_groupId); + _flameCounterSub = stream.listen((counter) { + if (mounted) { + setState(() { + _flameCounter = counter; + }); + } + }); + final stream2 = twonlyDB.groupsDao.watchGroup(_groupId); + _groupSub = stream2.listen((update) { + if (mounted) { + setState(() { + _directChat = update; + }); + } + }); + super.initState(); + } + + @override + void dispose() { + _flameCounterSub.cancel(); + _groupSub.cancel(); + super.dispose(); + } + + Future _restoreFlames() async { + if (!isPayingUser(getCurrentPlan())) { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const SubscriptionView(); + }, + ), + ); + return; + } + await twonlyDB.groupsDao.updateGroup( + _groupId, + GroupsCompanion( + flameCounter: Value(_directChat!.maxFlameCounter - 1), + lastFlameCounterChange: Value(DateTime.now()), + ), + ); + await syncFlameCounters(forceForGroup: _groupId); + } + + @override + Widget build(BuildContext context) { + if (_directChat == null || + _flameCounter >= (_directChat!.maxFlameCounter + 1) || + _directChat!.lastFlameCounterChange! + .isBefore(DateTime.now().subtract(const Duration(days: 5)))) { + return Container(); + } + return BetterListTile( + onTap: _restoreFlames, + leading: const SizedBox( + width: 24, + child: EmojiAnimation( + emoji: '🔥', + ), + ), + text: 'Restore your ${_directChat!.maxFlameCounter} lost flames', + ); + } +} diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 960bbbe..d911a1b 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -10,6 +10,7 @@ import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/flame.dart'; +import 'package:twonly/src/views/components/max_flame_list_title.dart'; import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart'; @@ -146,6 +147,9 @@ class _ContactViewState extends State { groupId: getUUIDforDirectChat(widget.userId, gUser.userId), ), const Divider(), + MaxFlameListTitle( + contactId: widget.userId, + ), BetterListTile( icon: FontAwesomeIcons.shieldHeart, text: context.lang.contactVerifyNumberTitle, diff --git a/lib/src/views/settings/subscription/checkout.view.dart b/lib/src/views/settings/subscription/checkout.view.dart index 174c2d2..83804a5 100644 --- a/lib/src/views/settings/subscription/checkout.view.dart +++ b/lib/src/views/settings/subscription/checkout.view.dart @@ -1,17 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/settings/subscription/select_payment.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; class CheckoutView extends StatefulWidget { const CheckoutView({ - required this.planId, + required this.plan, super.key, this.refund, this.disableMonthlyOption, }); - final String planId; + final SubscriptionPlan plan; final int? refund; final bool? disableMonthlyOption; @@ -31,7 +32,7 @@ class _CheckoutViewState extends State { } void setCheckout({bool init = false}) { - checkoutInCents = getPlanPrice(widget.planId, paidMonthly: paidMonthly); + checkoutInCents = getPlanPrice(widget.plan, paidMonthly: paidMonthly); if (!init) { setState(() {}); } @@ -52,7 +53,7 @@ class _CheckoutViewState extends State { Expanded( child: ListView( children: [ - PlanCard(planId: widget.planId), + PlanCard(plan: widget.plan), if (widget.disableMonthlyOption == null || !widget.disableMonthlyOption!) Padding( @@ -129,7 +130,7 @@ class _CheckoutViewState extends State { MaterialPageRoute( builder: (context) { return SelectPaymentView( - planId: widget.planId, + plan: widget.plan, payMonthly: paidMonthly, refund: widget.refund, ); diff --git a/lib/src/views/settings/subscription/manage_subscription.view.dart b/lib/src/views/settings/subscription/manage_subscription.view.dart index 7fb96ab..6c887d4 100644 --- a/lib/src/views/settings/subscription/manage_subscription.view.dart +++ b/lib/src/views/settings/subscription/manage_subscription.view.dart @@ -7,6 +7,7 @@ import 'package:twonly/globals.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/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; @@ -64,25 +65,24 @@ class _ManageSubscriptionViewState extends State { @override Widget build(BuildContext context) { - final planId = context.read().plan; + final plan = context.read().plan; final myLocale = Localizations.localeOf(context); final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS; - final isPayingUser = planId == 'Family' || planId == 'Pro'; return Scaffold( appBar: AppBar( title: Text(context.lang.manageSubscription), ), body: ListView( children: [ - PlanCard(planId: planId, paidMonthly: paidMonthly), - if (isPayingUser) const SizedBox(height: 20), - if (widget.nextPayment != null && isPayingUser) + PlanCard(plan: plan, paidMonthly: paidMonthly), + if (isPayingUser(plan)) const SizedBox(height: 20), + if (widget.nextPayment != null && isPayingUser(plan)) ListTile( title: Text( '${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(widget.nextPayment!)}', ), ), - if (autoRenewal != null && isPayingUser) + if (autoRenewal != null && isPayingUser(plan)) ListTile( title: Text(context.lang.autoRenewal), subtitle: Text( diff --git a/lib/src/views/settings/subscription/select_payment.view.dart b/lib/src/views/settings/subscription/select_payment.view.dart index c92f115..4ae1ae2 100644 --- a/lib/src/views/settings/subscription/select_payment.view.dart +++ b/lib/src/views/settings/subscription/select_payment.view.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; @@ -13,13 +14,13 @@ import 'package:url_launcher/url_launcher.dart'; class SelectPaymentView extends StatefulWidget { const SelectPaymentView({ super.key, - this.planId, + this.plan, this.payMonthly, this.valueInCents, this.refund, }); - final String? planId; + final SubscriptionPlan? plan; final bool? payMonthly; final int? valueInCents; final int? refund; @@ -62,9 +63,9 @@ class _SelectPaymentViewState extends State { void setCheckout(bool init) { if (widget.valueInCents != null && widget.valueInCents! > 0) { checkoutInCents = widget.valueInCents!; - } else if (widget.planId != null) { + } else if (widget.plan != null) { checkoutInCents = - getPlanPrice(widget.planId!, paidMonthly: widget.payMonthly!); + getPlanPrice(widget.plan!, paidMonthly: widget.payMonthly!); } else { /// Nothing to checkout for... Navigator.pop(context); @@ -77,7 +78,7 @@ class _SelectPaymentViewState extends State { @override Widget build(BuildContext context) { - final totalPrice = (widget.planId != null && widget.payMonthly != null) + final totalPrice = (widget.plan != null && widget.payMonthly != null) ? '${localePrizing(context, checkoutInCents)}/${(widget.payMonthly!) ? context.lang.month : context.lang.year}' : localePrizing(context, checkoutInCents); final canPay = paymentMethods == PaymentMethods.twonlyCredit && @@ -239,13 +240,13 @@ class _SelectPaymentViewState extends State { onPressed: canPay ? () async { final res = await apiService.switchToPayedPlan( - widget.planId!, + widget.plan!.name, widget.payMonthly!, tryAutoRenewal, ); if (!context.mounted) return; if (res.isSuccess) { - await updateUsersPlan(context, widget.planId!); + await updateUsersPlan(context, widget.plan!); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/src/views/settings/subscription/subscription.view.dart b/lib/src/views/settings/subscription/subscription.view.dart index 4c0c8fc..6d1ccbf 100644 --- a/lib/src/views/settings/subscription/subscription.view.dart +++ b/lib/src/views/settings/subscription/subscription.view.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/database/daos/contacts.dao.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/providers/connection.provider.dart'; +import 'package:twonly/src/services/subscription.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -64,7 +65,7 @@ const int MONTHLY_PAYMENT_DAYS = 30; const int YEARLY_PAYMENT_DAYS = 365; int calculateRefund(Response_PlanBallance current) { - var refund = getPlanPrice('Pro', paidMonthly: true); + var refund = getPlanPrice(SubscriptionPlan.Pro, paidMonthly: true); if (current.paymentPeriodDays == YEARLY_PAYMENT_DAYS) { final elapsedDays = DateTime.now() @@ -81,7 +82,7 @@ int calculateRefund(Response_PlanBallance current) { // => 5€ refund = (((YEARLY_PAYMENT_DAYS - elapsedDays) / YEARLY_PAYMENT_DAYS) * - getPlanPrice('Pro', paidMonthly: false) / + getPlanPrice(SubscriptionPlan.Pro, paidMonthly: false) / 100) .ceil() * 100; @@ -144,13 +145,12 @@ class _SubscriptionViewState extends State { String? formattedBalance; DateTime? nextPayment; final currentPlan = context.read().plan; - final isPayingUser = currentPlan == 'Family' || currentPlan == 'Pro'; if (ballance != null) { final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch( ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000, ); - if (isPayingUser) { + if (isPayingUser(currentPlan)) { nextPayment = lastPaymentDateTime .add(Duration(days: ballance!.paymentPeriodDays.toInt())); } @@ -164,7 +164,7 @@ class _SubscriptionViewState extends State { } var refund = 0; - if (currentPlan == 'Pro' && ballance != null) { + if (currentPlan == SubscriptionPlan.Pro && ballance != null) { refund = calculateRefund(ballance!); } @@ -202,7 +202,7 @@ class _SubscriptionViewState extends State { ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), child: Text( - currentPlan, + currentPlan.name, style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, @@ -220,7 +220,7 @@ class _SubscriptionViewState extends State { style: const TextStyle(color: Colors.orange), ), ), - if (currentPlan != 'Family' && currentPlan != 'Pro') + if (!isPayingUser(currentPlan)) Center( child: Padding( padding: const EdgeInsets.all(18), @@ -231,16 +231,17 @@ class _SubscriptionViewState extends State { ), ), ), - if (currentPlan != 'Family' && currentPlan != 'Pro') + if (!isPayingUser(currentPlan) || + currentPlan == SubscriptionPlan.Tester) PlanCard( - planId: 'Pro', + plan: SubscriptionPlan.Pro, onTap: () async { await Navigator.push( context, MaterialPageRoute( builder: (context) { return const CheckoutView( - planId: 'Pro', + plan: SubscriptionPlan.Pro, ); }, ), @@ -248,9 +249,9 @@ class _SubscriptionViewState extends State { await initAsync(); }, ), - if (currentPlan != 'Family') + if (currentPlan != SubscriptionPlan.Family) PlanCard( - planId: 'Family', + plan: SubscriptionPlan.Family, refund: refund, onTap: () async { await Navigator.push( @@ -258,11 +259,12 @@ class _SubscriptionViewState extends State { MaterialPageRoute( builder: (context) { return CheckoutView( - planId: 'Family', + plan: SubscriptionPlan.Family, refund: (refund > 0) ? refund : null, - disableMonthlyOption: currentPlan == 'Pro' && - ballance!.paymentPeriodDays.toInt() == - YEARLY_PAYMENT_DAYS, + disableMonthlyOption: + currentPlan == SubscriptionPlan.Pro && + ballance!.paymentPeriodDays.toInt() == + YEARLY_PAYMENT_DAYS, ); }, ), @@ -270,7 +272,7 @@ class _SubscriptionViewState extends State { await initAsync(); }, ), - if (!isPayingUser) ...[ + if (!isPayingUser(currentPlan)) ...[ const SizedBox(height: 10), Center( child: Padding( @@ -284,15 +286,15 @@ class _SubscriptionViewState extends State { ), const SizedBox(height: 10), PlanCard( - planId: 'Plus', + plan: SubscriptionPlan.Plus, onTap: () async { - await redeemUserInviteCode(context, 'Plus'); + await redeemUserInviteCode(context, SubscriptionPlan.Plus.name); await initAsync(); }, ), ], const SizedBox(height: 10), - if (currentPlan != 'Family') const Divider(), + if (currentPlan != SubscriptionPlan.Family) const Divider(), BetterListTile( icon: FontAwesomeIcons.gears, text: context.lang.manageSubscription, @@ -337,7 +339,8 @@ class _SubscriptionViewState extends State { ); }, ), - if (isPayingUser || currentPlan == 'Tester') + if (isPayingUser(currentPlan) || + currentPlan == SubscriptionPlan.Tester) BetterListTile( icon: FontAwesomeIcons.userPlus, text: context.lang.manageAdditionalUsers, @@ -378,36 +381,38 @@ class _SubscriptionViewState extends State { } } -int getPlanPrice(String planId, {required bool paidMonthly}) { - switch (planId) { - case 'Pro': +int getPlanPrice(SubscriptionPlan plan, {required bool paidMonthly}) { + switch (plan) { + case SubscriptionPlan.Pro: return paidMonthly ? 100 : 1000; - case 'Family': + case SubscriptionPlan.Family: return paidMonthly ? 200 : 2000; + // ignore: no_default_cases + default: + return 0; } - return 0; } class PlanCard extends StatelessWidget { const PlanCard({ - required this.planId, + required this.plan, super.key, this.refund, this.onTap, this.paidMonthly, }); - final String planId; + final SubscriptionPlan plan; final void Function()? onTap; final int? refund; final bool? paidMonthly; @override Widget build(BuildContext context) { - final yearlyPrice = getPlanPrice(planId, paidMonthly: false); - final monthlyPrice = getPlanPrice(planId, paidMonthly: true); + final yearlyPrice = getPlanPrice(plan, paidMonthly: false); + final monthlyPrice = getPlanPrice(plan, paidMonthly: true); var features = []; - switch (planId) { + switch (plan.name) { case 'Free': features = [context.lang.freeFeature1]; case 'Plus': @@ -447,7 +452,7 @@ class PlanCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( - planId, + plan.name, textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, @@ -519,9 +524,12 @@ class PlanCard extends StatelessWidget { padding: const EdgeInsets.only(top: 10), child: FilledButton.icon( onPressed: onTap, - label: (planId == 'Free' || planId == 'Plus') + label: (plan == SubscriptionPlan.Free || + plan == SubscriptionPlan.Plus) ? Text(context.lang.redeemUserInviteCodeTitle) - : Text(context.lang.upgradeToPaidPlanButton(planId)), + : Text( + context.lang.upgradeToPaidPlanButton(plan.name), + ), ), ), ],