This commit is contained in:
otsmr 2025-11-06 18:40:22 +01:00
parent a99f42c5c8
commit 0ab0303163
23 changed files with 281 additions and 104 deletions

View file

@ -6,6 +6,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/subscription.service.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/components/app_outdated.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/home.view.dart';
@ -36,8 +37,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
await setUserPlan(); await setUserPlan();
}; };
globalCallbackUpdatePlan = (String planId) async { globalCallbackUpdatePlan = (SubscriptionPlan plan) async {
await context.read<CustomChangeProvider>().updatePlan(planId); await context.read<CustomChangeProvider>().updatePlan(plan);
}; };
unawaited(initAsync()); unawaited(initAsync());
@ -47,9 +48,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
final user = await getUser(); final user = await getUser();
if (user != null && mounted) { if (user != null && mounted) {
if (mounted) { if (mounted) {
await context await context.read<CustomChangeProvider>().updatePlan(
.read<CustomChangeProvider>() planFromString(user.subscriptionPlan),
.updatePlan(user.subscriptionPlan); );
} }
} }
} }
@ -79,7 +80,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
globalCallbackConnectionState = ({required bool isConnected}) {}; globalCallbackConnectionState = ({required bool isConnected}) {};
globalCallbackUpdatePlan = (String planId) {}; globalCallbackUpdatePlan = (SubscriptionPlan planId) {};
super.dispose(); super.dispose();
} }

View file

@ -4,6 +4,7 @@ import 'package:camera/camera.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/subscription.service.dart';
late ApiService apiService; late ApiService apiService;
@ -26,7 +27,8 @@ void Function({required bool isConnected}) globalCallbackConnectionState = ({
}) {}; }) {};
void Function() globalCallbackAppIsOutdated = () {}; void Function() globalCallbackAppIsOutdated = () {};
void Function() globalCallbackNewDeviceRegistered = () {}; void Function() globalCallbackNewDeviceRegistered = () {};
void Function(String planId) globalCallbackUpdatePlan = (String planId) {}; void Function(SubscriptionPlan plan) globalCallbackUpdatePlan =
(SubscriptionPlan plan) {};
Map<String, VoidCallback> globalUserDataChangedCallBack = {}; Map<String, VoidCallback> globalUserDataChangedCallBack = {};

View file

@ -321,8 +321,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
if (updateFlame) { if (updateFlame) {
flameCounter += 1; flameCounter += 1;
lastFlameCounterChange = Value(timestamp); lastFlameCounterChange = Value(timestamp);
if (flameCounter > maxFlameCounter) { if ((flameCounter + 1) >= maxFlameCounter) {
maxFlameCounter = flameCounter; maxFlameCounter = flameCounter + 1;
maxFlameCounterFrom = DateTime.now(); maxFlameCounterFrom = DateTime.now();
} }
} }

View file

@ -105,16 +105,6 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> with _$ReceiptsDaoMixin {
..where((t) => t.receiptId.equals(receiptId))) ..where((t) => t.receiptId.equals(receiptId)))
.getSingleOrNull() != .getSingleOrNull() !=
null; null;
// try {
// return await (select()
// ..where(
// (t) => t.receiptId.equals(receiptId),
// ))
// .getSingleOrNull();
// } catch (e) {
// Log.error(e);
// return null;
// }
} }
Future<void> gotReceipt(String receiptId) async { Future<void> gotReceipt(String receiptId) async {

View file

@ -411,7 +411,7 @@
"@proFeature1": {}, "@proFeature1": {},
"proFeature2": "1 zusätzlicher Plus Benutzer", "proFeature2": "1 zusätzlicher Plus Benutzer",
"@proFeature2": {}, "@proFeature2": {},
"proFeature3": "Zusatzfunktionen (coming-soon)", "proFeature3": "Flammen wiederherstellen",
"@proFeature3": {}, "@proFeature3": {},
"proFeature4": "Cloud-Backup verschlüsselt (coming-soon)", "proFeature4": "Cloud-Backup verschlüsselt (coming-soon)",
"@proFeature4": {}, "@proFeature4": {},

View file

@ -705,7 +705,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get proFeature2 => '1 zusätzlicher Plus Benutzer'; String get proFeature2 => '1 zusätzlicher Plus Benutzer';
@override @override
String get proFeature3 => 'Zusatzfunktionen (coming-soon)'; String get proFeature3 => 'Flammen wiederherstellen';
@override @override
String get proFeature4 => 'Cloud-Backup verschlüsselt (coming-soon)'; String get proFeature4 => 'Cloud-Backup verschlüsselt (coming-soon)';

View file

@ -1251,6 +1251,7 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
$fixnum.Int64? flameCounter, $fixnum.Int64? flameCounter,
$fixnum.Int64? lastFlameCounterChange, $fixnum.Int64? lastFlameCounterChange,
$core.bool? bestFriend, $core.bool? bestFriend,
$core.bool? forceUpdate,
}) { }) {
final $result = create(); final $result = create();
if (flameCounter != null) { if (flameCounter != null) {
@ -1262,6 +1263,9 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
if (bestFriend != null) { if (bestFriend != null) {
$result.bestFriend = bestFriend; $result.bestFriend = bestFriend;
} }
if (forceUpdate != null) {
$result.forceUpdate = forceUpdate;
}
return $result; return $result;
} }
EncryptedContent_FlameSync._() : super(); EncryptedContent_FlameSync._() : super();
@ -1272,6 +1276,7 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
..aInt64(1, _omitFieldNames ? '' : 'flameCounter', protoName: 'flameCounter') ..aInt64(1, _omitFieldNames ? '' : 'flameCounter', protoName: 'flameCounter')
..aInt64(2, _omitFieldNames ? '' : 'lastFlameCounterChange', protoName: 'lastFlameCounterChange') ..aInt64(2, _omitFieldNames ? '' : 'lastFlameCounterChange', protoName: 'lastFlameCounterChange')
..aOB(3, _omitFieldNames ? '' : 'bestFriend', protoName: 'bestFriend') ..aOB(3, _omitFieldNames ? '' : 'bestFriend', protoName: 'bestFriend')
..aOB(4, _omitFieldNames ? '' : 'forceUpdate', protoName: 'forceUpdate')
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -1322,6 +1327,15 @@ class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
$core.bool hasBestFriend() => $_has(2); $core.bool hasBestFriend() => $_has(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearBestFriend() => clearField(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 { class EncryptedContent extends $pb.GeneratedMessage {

View file

@ -365,6 +365,7 @@ const EncryptedContent_FlameSync$json = {
{'1': 'flameCounter', '3': 1, '4': 1, '5': 3, '10': 'flameCounter'}, {'1': 'flameCounter', '3': 1, '4': 1, '5': 3, '10': 'flameCounter'},
{'1': 'lastFlameCounterChange', '3': 2, '4': 1, '5': 3, '10': 'lastFlameCounterChange'}, {'1': 'lastFlameCounterChange', '3': 2, '4': 1, '5': 3, '10': 'lastFlameCounterChange'},
{'1': 'bestFriend', '3': 3, '4': 1, '5': 8, '10': 'bestFriend'}, {'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' 'J5cHRlZENvbnRlbnQuUHVzaEtleXMuVHlwZVIEdHlwZRIZCgVrZXlJZBgCIAEoA0gAUgVrZXlJ'
'ZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdG' 'ZIgBARIVCgNrZXkYAyABKAxIAVIDa2V5iAEBEiEKCWNyZWF0ZWRBdBgEIAEoA0gCUgljcmVhdG'
'VkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9r' 'VkQXSIAQEiHwoEVHlwZRILCgdSRVFVRVNUEAASCgoGVVBEQVRFEAFCCAoGX2tleUlkQgYKBF9r'
'ZXlCDAoKX2NyZWF0ZWRBdBqHAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm' 'ZXlCDAoKX2NyZWF0ZWRBdBqpAQoJRmxhbWVTeW5jEiIKDGZsYW1lQ291bnRlchgBIAEoA1IMZm'
'xhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv' 'xhbWVDb3VudGVyEjYKFmxhc3RGbGFtZUNvdW50ZXJDaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv'
'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZEIKCghfZ3JvdXBJZE' 'dW50ZXJDaGFuZ2USHgoKYmVzdEZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIgCgtmb3JjZVVwZG'
'IPCg1faXNEaXJlY3RDaGF0QhcKFV9zZW5kZXJQcm9maWxlQ291bnRlckIQCg5fbWVzc2FnZVVw' 'F0ZRgEIAEoCFILZm9yY2VVcGRhdGVCCgoIX2dyb3VwSWRCDwoNX2lzRGlyZWN0Q2hhdEIXChVf'
'ZGF0ZUIICgZfbWVkaWFCDgoMX21lZGlhVXBkYXRlQhAKDl9jb250YWN0VXBkYXRlQhEKD19jb2' 'c2VuZGVyUHJvZmlsZUNvdW50ZXJCEAoOX21lc3NhZ2VVcGRhdGVCCAoGX21lZGlhQg4KDF9tZW'
'50YWN0UmVxdWVzdEIMCgpfZmxhbWVTeW5jQgsKCV9wdXNoS2V5c0ILCglfcmVhY3Rpb25CDgoM' 'RpYVVwZGF0ZUIQCg5fY29udGFjdFVwZGF0ZUIRCg9fY29udGFjdFJlcXVlc3RCDAoKX2ZsYW1l'
'X3RleHRNZXNzYWdlQg4KDF9ncm91cENyZWF0ZUIMCgpfZ3JvdXBKb2luQg4KDF9ncm91cFVwZG' 'U3luY0ILCglfcHVzaEtleXNCCwoJX3JlYWN0aW9uQg4KDF90ZXh0TWVzc2FnZUIOCgxfZ3JvdX'
'F0ZUIXChVfcmVzZW5kR3JvdXBQdWJsaWNLZXk='); 'BDcmVhdGVCDAoKX2dyb3VwSm9pbkIOCgxfZ3JvdXBVcGRhdGVCFwoVX3Jlc2VuZEdyb3VwUHVi'
'bGljS2V5');

View file

@ -169,6 +169,7 @@ message EncryptedContent {
int64 flameCounter = 1; int64 flameCounter = 1;
int64 lastFlameCounterChange = 2; int64 lastFlameCounterChange = 2;
bool bestFriend = 3; bool bestFriend = 3;
bool forceUpdate = 4;
} }
} }

View file

@ -1,15 +1,16 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:twonly/src/services/subscription.service.dart';
class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { class CustomChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
bool _isConnected = false; bool _isConnected = false;
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
String plan = 'Free'; SubscriptionPlan plan = SubscriptionPlan.Free;
Future<void> updateConnectionState(bool update) async { Future<void> updateConnectionState(bool update) async {
_isConnected = update; _isConnected = update;
notifyListeners(); notifyListeners();
} }
Future<void> updatePlan(String newPlan) async { Future<void> updatePlan(SubscriptionPlan newPlan) async {
plan = newPlan; plan = newPlan;
notifyListeners(); notifyListeners();
} }

View file

@ -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/identity.signal.dart';
import 'package:twonly/src/services/signal/prekeys.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/signal/utils.signal.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -384,7 +385,7 @@ class ApiService {
user.subscriptionPlan = authenticated.plan; user.subscriptionPlan = authenticated.plan;
return user; return user;
}); });
globalCallbackUpdatePlan(authenticated.plan); globalCallbackUpdatePlan(planFromString(authenticated.plan));
} }
Log.info('websocket is authenticated'); Log.info('websocket is authenticated');
unawaited(onAuthenticated()); unawaited(onAuthenticated());

View file

@ -123,18 +123,24 @@ Future<void> handleFlameSync(
Log.info('Got a flameSync from $contactId'); Log.info('Got a flameSync from $contactId');
final group = await twonlyDB.groupsDao.getDirectChat(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( var updates = GroupsCompanion(
alsoBestFriend: Value(flameSync.bestFriend), alsoBestFriend: Value(flameSync.bestFriend),
); );
if (isToday(group.lastFlameCounterChange!) && if (isToday(group.lastFlameCounterChange!) &&
isToday(fromTimestamp(flameSync.lastFlameCounterChange))) { isToday(fromTimestamp(flameSync.lastFlameCounterChange)) ||
flameSync.forceUpdate) {
if (flameSync.flameCounter > group.flameCounter) { if (flameSync.flameCounter > group.flameCounter) {
updates = GroupsCompanion( updates = updates.copyWith(
flameCounter: Value(flameSync.flameCounter.toInt()), 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); await twonlyDB.groupsDao.updateGroup(group.groupId, updates);
} }

View file

@ -9,7 +9,7 @@ import 'package:twonly/src/services/api/messages.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';
Future<void> syncFlameCounters() async { Future<void> syncFlameCounters({String? forceForGroup}) async {
final groups = await twonlyDB.groupsDao.getAllDirectChats(); final groups = await twonlyDB.groupsDao.getAllDirectChats();
if (groups.isEmpty) return; if (groups.isEmpty) return;
final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max; final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max;
@ -26,8 +26,10 @@ Future<void> syncFlameCounters() async {
for (final group in groups) { for (final group in groups) {
if (group.lastFlameCounterChange == null) continue; if (group.lastFlameCounterChange == null) continue;
if (!isToday(group.lastFlameCounterChange!)) continue; if (!isToday(group.lastFlameCounterChange!)) continue;
if (group.lastFlameSync != null) { if (forceForGroup == null || group.groupId != forceForGroup) {
if (isToday(group.lastFlameSync!)) continue; if (group.lastFlameSync != null) {
if (isToday(group.lastFlameSync!)) continue;
}
} }
final flameCounter = getFlameCounterFromGroup(group) - 1; final flameCounter = getFlameCounterFromGroup(group) - 1;
@ -49,6 +51,7 @@ Future<void> syncFlameCounters() async {
lastFlameCounterChange: lastFlameCounterChange:
Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch), Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch),
bestFriend: group.groupId == bestFriend.groupId, bestFriend: group.groupId == bestFriend.groupId,
forceUpdate: group.groupId == forceForGroup,
), ),
), ),
); );

View file

@ -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);
}

View file

@ -8,6 +8,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.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/providers/connection.provider.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
Future<bool> isUserCreated() async { Future<bool> isUserCreated() async {
@ -35,16 +36,19 @@ Future<UserData?> getUser() async {
} }
} }
Future<void> updateUsersPlan(BuildContext context, String planId) async { Future<void> updateUsersPlan(
context.read<CustomChangeProvider>().plan = planId; BuildContext context,
SubscriptionPlan plan,
) async {
context.read<CustomChangeProvider>().plan = plan;
await updateUserdata((user) { await updateUserdata((user) {
user.subscriptionPlan = planId; user.subscriptionPlan = plan.name;
return user; return user;
}); });
if (!context.mounted) return; if (!context.mounted) return;
await context.read<CustomChangeProvider>().updatePlan(planId); await context.read<CustomChangeProvider>().updatePlan(plan);
} }
Mutex updateProtection = Mutex(); Mutex updateProtection = Mutex();

View file

@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/providers/connection.provider.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/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/chats/add_new_user.view.dart'; import 'package:twonly/src/views/chats/add_new_user.view.dart';
@ -104,7 +105,7 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isConnected = context.watch<CustomChangeProvider>().isConnected; final isConnected = context.watch<CustomChangeProvider>().isConnected;
final planId = context.watch<CustomChangeProvider>().plan; final plan = context.watch<CustomChangeProvider>().plan;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(
@ -130,7 +131,7 @@ class _ChatListViewState extends State<ChatListView> {
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
const Text('twonly '), const Text('twonly '),
if (planId != 'Free') if (plan != SubscriptionPlan.Free)
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Navigator.push( Navigator.push(
@ -150,7 +151,7 @@ class _ChatListViewState extends State<ChatListView> {
padding: padding:
const EdgeInsets.symmetric(horizontal: 5, vertical: 3), const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
child: Text( child: Text(
planId, plan.name,
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View file

@ -82,14 +82,14 @@ class GroupContextMenu extends StatelessWidget {
context.lang.groupContextMenuDeleteGroup, context.lang.groupContextMenuDeleteGroup,
); );
if (ok) { if (ok) {
// await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId); await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId);
await twonlyDB.groupsDao.deleteGroup(group.groupId); // await twonlyDB.groupsDao.deleteGroup(group.groupId);
// await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
// group.groupId, group.groupId,
// const GroupsCompanion( const GroupsCompanion(
// deletedContent: Value(true), deletedContent: Value(true),
// ), ),
// ); );
} }
}, },
), ),

View file

@ -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<MaxFlameListTitle> createState() => _MaxFlameListTitleState();
}
class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
int _flameCounter = 0;
Group? _directChat;
late String _groupId;
late StreamSubscription<int> _flameCounterSub;
late StreamSubscription<Group?> _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<void> _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',
);
}
}

View file

@ -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/avatar_icon.component.dart';
import 'package:twonly/src/views/components/better_list_title.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/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/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';
import 'package:twonly/src/views/contact/contact_verify.view.dart'; import 'package:twonly/src/views/contact/contact_verify.view.dart';
@ -146,6 +147,9 @@ class _ContactViewState extends State<ContactView> {
groupId: getUUIDforDirectChat(widget.userId, gUser.userId), groupId: getUUIDforDirectChat(widget.userId, gUser.userId),
), ),
const Divider(), const Divider(),
MaxFlameListTitle(
contactId: widget.userId,
),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.shieldHeart, icon: FontAwesomeIcons.shieldHeart,
text: context.lang.contactVerifyNumberTitle, text: context.lang.contactVerifyNumberTitle,

View file

@ -1,17 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/services/subscription.service.dart';
import 'package:twonly/src/utils/misc.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/select_payment.view.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
class CheckoutView extends StatefulWidget { class CheckoutView extends StatefulWidget {
const CheckoutView({ const CheckoutView({
required this.planId, required this.plan,
super.key, super.key,
this.refund, this.refund,
this.disableMonthlyOption, this.disableMonthlyOption,
}); });
final String planId; final SubscriptionPlan plan;
final int? refund; final int? refund;
final bool? disableMonthlyOption; final bool? disableMonthlyOption;
@ -31,7 +32,7 @@ class _CheckoutViewState extends State<CheckoutView> {
} }
void setCheckout({bool init = false}) { void setCheckout({bool init = false}) {
checkoutInCents = getPlanPrice(widget.planId, paidMonthly: paidMonthly); checkoutInCents = getPlanPrice(widget.plan, paidMonthly: paidMonthly);
if (!init) { if (!init) {
setState(() {}); setState(() {});
} }
@ -52,7 +53,7 @@ class _CheckoutViewState extends State<CheckoutView> {
Expanded( Expanded(
child: ListView( child: ListView(
children: [ children: [
PlanCard(planId: widget.planId), PlanCard(plan: widget.plan),
if (widget.disableMonthlyOption == null || if (widget.disableMonthlyOption == null ||
!widget.disableMonthlyOption!) !widget.disableMonthlyOption!)
Padding( Padding(
@ -129,7 +130,7 @@ class _CheckoutViewState extends State<CheckoutView> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return SelectPaymentView( return SelectPaymentView(
planId: widget.planId, plan: widget.plan,
payMonthly: paidMonthly, payMonthly: paidMonthly,
refund: widget.refund, refund: widget.refund,
); );

View file

@ -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/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';
import 'package:twonly/src/providers/connection.provider.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/misc.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
@ -64,25 +65,24 @@ class _ManageSubscriptionViewState extends State<ManageSubscriptionView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final planId = context.read<CustomChangeProvider>().plan; final plan = context.read<CustomChangeProvider>().plan;
final myLocale = Localizations.localeOf(context); final myLocale = Localizations.localeOf(context);
final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS; final paidMonthly = ballance?.paymentPeriodDays == MONTHLY_PAYMENT_DAYS;
final isPayingUser = planId == 'Family' || planId == 'Pro';
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.manageSubscription), title: Text(context.lang.manageSubscription),
), ),
body: ListView( body: ListView(
children: [ children: [
PlanCard(planId: planId, paidMonthly: paidMonthly), PlanCard(plan: plan, paidMonthly: paidMonthly),
if (isPayingUser) const SizedBox(height: 20), if (isPayingUser(plan)) const SizedBox(height: 20),
if (widget.nextPayment != null && isPayingUser) if (widget.nextPayment != null && isPayingUser(plan))
ListTile( ListTile(
title: Text( title: Text(
'${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(widget.nextPayment!)}', '${context.lang.nextPayment}: ${DateFormat.yMMMMd(myLocale.toString()).format(widget.nextPayment!)}',
), ),
), ),
if (autoRenewal != null && isPayingUser) if (autoRenewal != null && isPayingUser(plan))
ListTile( ListTile(
title: Text(context.lang.autoRenewal), title: Text(context.lang.autoRenewal),
subtitle: Text( subtitle: Text(

View file

@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pbserver.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/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';
@ -13,13 +14,13 @@ import 'package:url_launcher/url_launcher.dart';
class SelectPaymentView extends StatefulWidget { class SelectPaymentView extends StatefulWidget {
const SelectPaymentView({ const SelectPaymentView({
super.key, super.key,
this.planId, this.plan,
this.payMonthly, this.payMonthly,
this.valueInCents, this.valueInCents,
this.refund, this.refund,
}); });
final String? planId; final SubscriptionPlan? plan;
final bool? payMonthly; final bool? payMonthly;
final int? valueInCents; final int? valueInCents;
final int? refund; final int? refund;
@ -62,9 +63,9 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
void setCheckout(bool init) { void setCheckout(bool init) {
if (widget.valueInCents != null && widget.valueInCents! > 0) { if (widget.valueInCents != null && widget.valueInCents! > 0) {
checkoutInCents = widget.valueInCents!; checkoutInCents = widget.valueInCents!;
} else if (widget.planId != null) { } else if (widget.plan != null) {
checkoutInCents = checkoutInCents =
getPlanPrice(widget.planId!, paidMonthly: widget.payMonthly!); getPlanPrice(widget.plan!, paidMonthly: widget.payMonthly!);
} else { } else {
/// Nothing to checkout for... /// Nothing to checkout for...
Navigator.pop(context); Navigator.pop(context);
@ -77,7 +78,7 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
@override @override
Widget build(BuildContext context) { 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)}/${(widget.payMonthly!) ? context.lang.month : context.lang.year}'
: localePrizing(context, checkoutInCents); : localePrizing(context, checkoutInCents);
final canPay = paymentMethods == PaymentMethods.twonlyCredit && final canPay = paymentMethods == PaymentMethods.twonlyCredit &&
@ -239,13 +240,13 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
onPressed: canPay onPressed: canPay
? () async { ? () async {
final res = await apiService.switchToPayedPlan( final res = await apiService.switchToPayedPlan(
widget.planId!, widget.plan!.name,
widget.payMonthly!, widget.payMonthly!,
tryAutoRenewal, tryAutoRenewal,
); );
if (!context.mounted) return; if (!context.mounted) return;
if (res.isSuccess) { if (res.isSuccess) {
await updateUsersPlan(context, widget.planId!); await updateUsersPlan(context, widget.plan!);
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(

View file

@ -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/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';
import 'package:twonly/src/providers/connection.provider.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/log.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';
@ -64,7 +65,7 @@ const int MONTHLY_PAYMENT_DAYS = 30;
const int YEARLY_PAYMENT_DAYS = 365; const int YEARLY_PAYMENT_DAYS = 365;
int calculateRefund(Response_PlanBallance current) { int calculateRefund(Response_PlanBallance current) {
var refund = getPlanPrice('Pro', paidMonthly: true); var refund = getPlanPrice(SubscriptionPlan.Pro, paidMonthly: true);
if (current.paymentPeriodDays == YEARLY_PAYMENT_DAYS) { if (current.paymentPeriodDays == YEARLY_PAYMENT_DAYS) {
final elapsedDays = DateTime.now() final elapsedDays = DateTime.now()
@ -81,7 +82,7 @@ int calculateRefund(Response_PlanBallance current) {
// => 5 // => 5
refund = (((YEARLY_PAYMENT_DAYS - elapsedDays) / YEARLY_PAYMENT_DAYS) * refund = (((YEARLY_PAYMENT_DAYS - elapsedDays) / YEARLY_PAYMENT_DAYS) *
getPlanPrice('Pro', paidMonthly: false) / getPlanPrice(SubscriptionPlan.Pro, paidMonthly: false) /
100) 100)
.ceil() * .ceil() *
100; 100;
@ -144,13 +145,12 @@ class _SubscriptionViewState extends State<SubscriptionView> {
String? formattedBalance; String? formattedBalance;
DateTime? nextPayment; DateTime? nextPayment;
final currentPlan = context.read<CustomChangeProvider>().plan; final currentPlan = context.read<CustomChangeProvider>().plan;
final isPayingUser = currentPlan == 'Family' || currentPlan == 'Pro';
if (ballance != null) { if (ballance != null) {
final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch( final lastPaymentDateTime = DateTime.fromMillisecondsSinceEpoch(
ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000, ballance!.lastPaymentDoneUnixTimestamp.toInt() * 1000,
); );
if (isPayingUser) { if (isPayingUser(currentPlan)) {
nextPayment = lastPaymentDateTime nextPayment = lastPaymentDateTime
.add(Duration(days: ballance!.paymentPeriodDays.toInt())); .add(Duration(days: ballance!.paymentPeriodDays.toInt()));
} }
@ -164,7 +164,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
} }
var refund = 0; var refund = 0;
if (currentPlan == 'Pro' && ballance != null) { if (currentPlan == SubscriptionPlan.Pro && ballance != null) {
refund = calculateRefund(ballance!); refund = calculateRefund(ballance!);
} }
@ -202,7 +202,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
), ),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
child: Text( child: Text(
currentPlan, currentPlan.name,
style: TextStyle( style: TextStyle(
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -220,7 +220,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
style: const TextStyle(color: Colors.orange), style: const TextStyle(color: Colors.orange),
), ),
), ),
if (currentPlan != 'Family' && currentPlan != 'Pro') if (!isPayingUser(currentPlan))
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
@ -231,16 +231,17 @@ class _SubscriptionViewState extends State<SubscriptionView> {
), ),
), ),
), ),
if (currentPlan != 'Family' && currentPlan != 'Pro') if (!isPayingUser(currentPlan) ||
currentPlan == SubscriptionPlan.Tester)
PlanCard( PlanCard(
planId: 'Pro', plan: SubscriptionPlan.Pro,
onTap: () async { onTap: () async {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return const CheckoutView( return const CheckoutView(
planId: 'Pro', plan: SubscriptionPlan.Pro,
); );
}, },
), ),
@ -248,9 +249,9 @@ class _SubscriptionViewState extends State<SubscriptionView> {
await initAsync(); await initAsync();
}, },
), ),
if (currentPlan != 'Family') if (currentPlan != SubscriptionPlan.Family)
PlanCard( PlanCard(
planId: 'Family', plan: SubscriptionPlan.Family,
refund: refund, refund: refund,
onTap: () async { onTap: () async {
await Navigator.push( await Navigator.push(
@ -258,11 +259,12 @@ class _SubscriptionViewState extends State<SubscriptionView> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return CheckoutView( return CheckoutView(
planId: 'Family', plan: SubscriptionPlan.Family,
refund: (refund > 0) ? refund : null, refund: (refund > 0) ? refund : null,
disableMonthlyOption: currentPlan == 'Pro' && disableMonthlyOption:
ballance!.paymentPeriodDays.toInt() == currentPlan == SubscriptionPlan.Pro &&
YEARLY_PAYMENT_DAYS, ballance!.paymentPeriodDays.toInt() ==
YEARLY_PAYMENT_DAYS,
); );
}, },
), ),
@ -270,7 +272,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
await initAsync(); await initAsync();
}, },
), ),
if (!isPayingUser) ...[ if (!isPayingUser(currentPlan)) ...[
const SizedBox(height: 10), const SizedBox(height: 10),
Center( Center(
child: Padding( child: Padding(
@ -284,15 +286,15 @@ class _SubscriptionViewState extends State<SubscriptionView> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
PlanCard( PlanCard(
planId: 'Plus', plan: SubscriptionPlan.Plus,
onTap: () async { onTap: () async {
await redeemUserInviteCode(context, 'Plus'); await redeemUserInviteCode(context, SubscriptionPlan.Plus.name);
await initAsync(); await initAsync();
}, },
), ),
], ],
const SizedBox(height: 10), const SizedBox(height: 10),
if (currentPlan != 'Family') const Divider(), if (currentPlan != SubscriptionPlan.Family) const Divider(),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.gears, icon: FontAwesomeIcons.gears,
text: context.lang.manageSubscription, text: context.lang.manageSubscription,
@ -337,7 +339,8 @@ class _SubscriptionViewState extends State<SubscriptionView> {
); );
}, },
), ),
if (isPayingUser || currentPlan == 'Tester') if (isPayingUser(currentPlan) ||
currentPlan == SubscriptionPlan.Tester)
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.userPlus, icon: FontAwesomeIcons.userPlus,
text: context.lang.manageAdditionalUsers, text: context.lang.manageAdditionalUsers,
@ -378,36 +381,38 @@ class _SubscriptionViewState extends State<SubscriptionView> {
} }
} }
int getPlanPrice(String planId, {required bool paidMonthly}) { int getPlanPrice(SubscriptionPlan plan, {required bool paidMonthly}) {
switch (planId) { switch (plan) {
case 'Pro': case SubscriptionPlan.Pro:
return paidMonthly ? 100 : 1000; return paidMonthly ? 100 : 1000;
case 'Family': case SubscriptionPlan.Family:
return paidMonthly ? 200 : 2000; return paidMonthly ? 200 : 2000;
// ignore: no_default_cases
default:
return 0;
} }
return 0;
} }
class PlanCard extends StatelessWidget { class PlanCard extends StatelessWidget {
const PlanCard({ const PlanCard({
required this.planId, required this.plan,
super.key, super.key,
this.refund, this.refund,
this.onTap, this.onTap,
this.paidMonthly, this.paidMonthly,
}); });
final String planId; final SubscriptionPlan plan;
final void Function()? onTap; final void Function()? onTap;
final int? refund; final int? refund;
final bool? paidMonthly; final bool? paidMonthly;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final yearlyPrice = getPlanPrice(planId, paidMonthly: false); final yearlyPrice = getPlanPrice(plan, paidMonthly: false);
final monthlyPrice = getPlanPrice(planId, paidMonthly: true); final monthlyPrice = getPlanPrice(plan, paidMonthly: true);
var features = <String>[]; var features = <String>[];
switch (planId) { switch (plan.name) {
case 'Free': case 'Free':
features = [context.lang.freeFeature1]; features = [context.lang.freeFeature1];
case 'Plus': case 'Plus':
@ -447,7 +452,7 @@ class PlanCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
Text( Text(
planId, plan.name,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 24,
@ -519,9 +524,12 @@ class PlanCard extends StatelessWidget {
padding: const EdgeInsets.only(top: 10), padding: const EdgeInsets.only(top: 10),
child: FilledButton.icon( child: FilledButton.icon(
onPressed: onTap, onPressed: onTap,
label: (planId == 'Free' || planId == 'Plus') label: (plan == SubscriptionPlan.Free ||
plan == SubscriptionPlan.Plus)
? Text(context.lang.redeemUserInviteCodeTitle) ? Text(context.lang.redeemUserInviteCodeTitle)
: Text(context.lang.upgradeToPaidPlanButton(planId)), : Text(
context.lang.upgradeToPaidPlanButton(plan.name),
),
), ),
), ),
], ],