mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 12:48:41 +00:00
fix #283
This commit is contained in:
parent
a99f42c5c8
commit
0ab0303163
23 changed files with 281 additions and 104 deletions
13
lib/app.dart
13
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<App> with WidgetsBindingObserver {
|
|||
await setUserPlan();
|
||||
};
|
||||
|
||||
globalCallbackUpdatePlan = (String planId) async {
|
||||
await context.read<CustomChangeProvider>().updatePlan(planId);
|
||||
globalCallbackUpdatePlan = (SubscriptionPlan plan) async {
|
||||
await context.read<CustomChangeProvider>().updatePlan(plan);
|
||||
};
|
||||
|
||||
unawaited(initAsync());
|
||||
|
|
@ -47,9 +48,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
final user = await getUser();
|
||||
if (user != null && mounted) {
|
||||
if (mounted) {
|
||||
await context
|
||||
.read<CustomChangeProvider>()
|
||||
.updatePlan(user.subscriptionPlan);
|
||||
await context.read<CustomChangeProvider>().updatePlan(
|
||||
planFromString(user.subscriptionPlan),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +80,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
globalCallbackConnectionState = ({required bool isConnected}) {};
|
||||
globalCallbackUpdatePlan = (String planId) {};
|
||||
globalCallbackUpdatePlan = (SubscriptionPlan planId) {};
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, VoidCallback> globalUserDataChangedCallBack = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -321,8 +321,8 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
if (updateFlame) {
|
||||
flameCounter += 1;
|
||||
lastFlameCounterChange = Value(timestamp);
|
||||
if (flameCounter > maxFlameCounter) {
|
||||
maxFlameCounter = flameCounter;
|
||||
if ((flameCounter + 1) >= maxFlameCounter) {
|
||||
maxFlameCounter = flameCounter + 1;
|
||||
maxFlameCounterFrom = DateTime.now();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,16 +105,6 @@ class ReceiptsDao extends DatabaseAccessor<TwonlyDB> 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<void> gotReceipt(String receiptId) async {
|
||||
|
|
|
|||
|
|
@ -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": {},
|
||||
|
|
|
|||
|
|
@ -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)';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ message EncryptedContent {
|
|||
int64 flameCounter = 1;
|
||||
int64 lastFlameCounterChange = 2;
|
||||
bool bestFriend = 3;
|
||||
bool forceUpdate = 4;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<void> updateConnectionState(bool update) async {
|
||||
_isConnected = update;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> updatePlan(String newPlan) async {
|
||||
Future<void> updatePlan(SubscriptionPlan newPlan) async {
|
||||
plan = newPlan;
|
||||
notifyListeners();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -123,18 +123,24 @@ Future<void> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> syncFlameCounters() async {
|
||||
Future<void> syncFlameCounters({String? forceForGroup}) async {
|
||||
final groups = await twonlyDB.groupsDao.getAllDirectChats();
|
||||
if (groups.isEmpty) return;
|
||||
final maxMessageCounter = groups.map((x) => x.totalMediaCounter).max;
|
||||
|
|
@ -26,9 +26,11 @@ Future<void> syncFlameCounters() async {
|
|||
for (final group in groups) {
|
||||
if (group.lastFlameCounterChange == null) continue;
|
||||
if (!isToday(group.lastFlameCounterChange!)) 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<void> syncFlameCounters() async {
|
|||
lastFlameCounterChange:
|
||||
Int64(group.lastFlameCounterChange!.millisecondsSinceEpoch),
|
||||
bestFriend: group.groupId == bestFriend.groupId,
|
||||
forceUpdate: group.groupId == forceForGroup,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
35
lib/src/services/subscription.service.dart
Normal file
35
lib/src/services/subscription.service.dart
Normal 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);
|
||||
}
|
||||
|
|
@ -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<bool> isUserCreated() async {
|
||||
|
|
@ -35,16 +36,19 @@ Future<UserData?> getUser() async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> updateUsersPlan(BuildContext context, String planId) async {
|
||||
context.read<CustomChangeProvider>().plan = planId;
|
||||
Future<void> updateUsersPlan(
|
||||
BuildContext context,
|
||||
SubscriptionPlan plan,
|
||||
) async {
|
||||
context.read<CustomChangeProvider>().plan = plan;
|
||||
|
||||
await updateUserdata((user) {
|
||||
user.subscriptionPlan = planId;
|
||||
user.subscriptionPlan = plan.name;
|
||||
return user;
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
await context.read<CustomChangeProvider>().updatePlan(planId);
|
||||
await context.read<CustomChangeProvider>().updatePlan(plan);
|
||||
}
|
||||
|
||||
Mutex updateProtection = Mutex();
|
||||
|
|
|
|||
|
|
@ -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<ChatListView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isConnected = context.watch<CustomChangeProvider>().isConnected;
|
||||
final planId = context.watch<CustomChangeProvider>().plan;
|
||||
final plan = context.watch<CustomChangeProvider>().plan;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
|
|
@ -130,7 +131,7 @@ class _ChatListViewState extends State<ChatListView> {
|
|||
),
|
||||
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<ChatListView> {
|
|||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
|
||||
child: Text(
|
||||
planId,
|
||||
plan.name,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
|||
102
lib/src/views/components/max_flame_list_title.dart
Normal file
102
lib/src/views/components/max_flame_list_title.dart
Normal 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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ContactView> {
|
|||
groupId: getUUIDforDirectChat(widget.userId, gUser.userId),
|
||||
),
|
||||
const Divider(),
|
||||
MaxFlameListTitle(
|
||||
contactId: widget.userId,
|
||||
),
|
||||
BetterListTile(
|
||||
icon: FontAwesomeIcons.shieldHeart,
|
||||
text: context.lang.contactVerifyNumberTitle,
|
||||
|
|
|
|||
|
|
@ -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<CheckoutView> {
|
|||
}
|
||||
|
||||
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<CheckoutView> {
|
|||
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<CheckoutView> {
|
|||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return SelectPaymentView(
|
||||
planId: widget.planId,
|
||||
plan: widget.plan,
|
||||
payMonthly: paidMonthly,
|
||||
refund: widget.refund,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<ManageSubscriptionView> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final planId = context.read<CustomChangeProvider>().plan;
|
||||
final plan = context.read<CustomChangeProvider>().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(
|
||||
|
|
|
|||
|
|
@ -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<SelectPaymentView> {
|
|||
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<SelectPaymentView> {
|
|||
|
||||
@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<SelectPaymentView> {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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<SubscriptionView> {
|
|||
String? formattedBalance;
|
||||
DateTime? nextPayment;
|
||||
final currentPlan = context.read<CustomChangeProvider>().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<SubscriptionView> {
|
|||
}
|
||||
|
||||
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<SubscriptionView> {
|
|||
),
|
||||
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<SubscriptionView> {
|
|||
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<SubscriptionView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
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<SubscriptionView> {
|
|||
await initAsync();
|
||||
},
|
||||
),
|
||||
if (currentPlan != 'Family')
|
||||
if (currentPlan != SubscriptionPlan.Family)
|
||||
PlanCard(
|
||||
planId: 'Family',
|
||||
plan: SubscriptionPlan.Family,
|
||||
refund: refund,
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
|
|
@ -258,9 +259,10 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CheckoutView(
|
||||
planId: 'Family',
|
||||
plan: SubscriptionPlan.Family,
|
||||
refund: (refund > 0) ? refund : null,
|
||||
disableMonthlyOption: currentPlan == 'Pro' &&
|
||||
disableMonthlyOption:
|
||||
currentPlan == SubscriptionPlan.Pro &&
|
||||
ballance!.paymentPeriodDays.toInt() ==
|
||||
YEARLY_PAYMENT_DAYS,
|
||||
);
|
||||
|
|
@ -270,7 +272,7 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
await initAsync();
|
||||
},
|
||||
),
|
||||
if (!isPayingUser) ...[
|
||||
if (!isPayingUser(currentPlan)) ...[
|
||||
const SizedBox(height: 10),
|
||||
Center(
|
||||
child: Padding(
|
||||
|
|
@ -284,15 +286,15 @@ class _SubscriptionViewState extends State<SubscriptionView> {
|
|||
),
|
||||
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<SubscriptionView> {
|
|||
);
|
||||
},
|
||||
),
|
||||
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<SubscriptionView> {
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 = <String>[];
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue