From a77951219861ca979ed9d46eb53646dcff58fbef Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 1 Jun 2025 23:13:22 +0200 Subject: [PATCH] added demo modus --- lib/globals.dart | 1 + lib/main.dart | 12 +- lib/src/model/json/userdata.dart | 15 +- lib/src/model/json/userdata.g.dart | 2 + lib/src/services/api.service.dart | 7 + lib/src/services/api/messages.dart | 3 + lib/src/utils/misc.dart | 166 ++++++++++++++++++ lib/src/views/chats/chat_list.view.dart | 47 ++++- .../connection_info.comp.dart | 3 +- lib/src/views/chats/chat_messages.view.dart | 5 +- lib/src/views/register.view.dart | 78 +++++--- .../subscription/select_payment.view.dart | 12 +- pubspec.yaml | 2 +- 13 files changed, 310 insertions(+), 43 deletions(-) diff --git a/lib/globals.dart b/lib/globals.dart index 0083ee7..3b8e486 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -8,3 +8,4 @@ late ApiService apiService; late TwonlyDatabase twonlyDB; List gCameras = []; +bool gIsDemoUser = false; diff --git a/lib/main.dart b/lib/main.dart index 4046238..2064686 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,19 +13,27 @@ import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/storage.dart'; import 'app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await initFCMService(); + initLogger(); + + final user = await getUser(); + if (user != null) { + if (user.isDemoUser) { + await deleteLocalUserData(); + } + } + final settingsController = SettingsChangeProvider(); // Load the user's preferred theme while the splash screen is displayed. // This prevents a sudden theme change when the app is first displayed. await settingsController.loadSettings(); - initLogger(); - await setupPushNotification(); await initMediaStorage(); diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index fb6ed71..6b36c49 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -4,11 +4,13 @@ part 'userdata.g.dart'; @JsonSerializable() class UserData { - UserData( - {required this.userId, - required this.username, - required this.displayName, - required this.subscriptionPlan}); + UserData({ + required this.userId, + required this.username, + required this.displayName, + required this.subscriptionPlan, + required this.isDemoUser, + }); String username; String displayName; @@ -17,6 +19,9 @@ class UserData { String? avatarJson; int? avatarCounter; + @JsonKey(defaultValue: false) + bool isDemoUser = false; + // settings int? defaultShowTime; @JsonKey(defaultValue: "Preview") diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 3694f5b..09f27ce 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -11,6 +11,7 @@ UserData _$UserDataFromJson(Map json) => UserData( username: json['username'] as String, displayName: json['displayName'] as String, subscriptionPlan: json['subscriptionPlan'] as String? ?? 'Preview', + isDemoUser: json['isDemoUser'] as bool? ?? false, ) ..avatarSvg = json['avatarSvg'] as String? ..avatarJson = json['avatarJson'] as String? @@ -51,6 +52,7 @@ Map _$UserDataToJson(UserData instance) => { 'avatarSvg': instance.avatarSvg, 'avatarJson': instance.avatarJson, 'avatarCounter': instance.avatarCounter, + 'isDemoUser': instance.isDemoUser, 'defaultShowTime': instance.defaultShowTime, 'subscriptionPlan': instance.subscriptionPlan, 'useHighQuality': instance.useHighQuality, diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index d44d249..9b814f0 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -114,6 +114,13 @@ class ApiService { } Future connect() async { + final user = await getUser(); + if (user != null && user.isDemoUser) { + print("DEMO user"); + // the demo user should not be able to connect to the API server... + globalCallbackConnectionState(true); + return false; + } return lockConnecting.protect(() async { if (_channel != null) { return true; diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 724e9e3..c40e6c1 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -201,6 +201,9 @@ Future<(String, RetransmitMessage)?> encryptMessage( // encrypts and stores the message and then sends it in the background Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg, {PushKind? pushKind}) async { + if (gIsDemoUser) { + return; + } (String, RetransmitMessage)? stateData = await encryptMessage(messageId, userId, msg, pushKind: pushKind); if (stateData != null) { diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 8480e07..862822f 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,5 +1,7 @@ +import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,7 +10,10 @@ import 'package:gal/gal.dart'; import 'package:local_auth/local_auth.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/twonly_database.dart'; +import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/protobuf/api/error.pb.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/settings.provider.dart'; @@ -218,3 +223,164 @@ String truncateString(String input, {int maxLength = 20}) { } return input; } + +Future insertDemoContacts() async { + List commonUsernames = [ + 'James', + 'Mary', + 'John', + 'Patricia', + 'Robert', + 'Jennifer', + 'Michael', + 'Linda', + 'William', + 'Elizabeth', + 'David', + 'Barbara', + 'Richard', + 'Susan', + 'Joseph', + 'Jessica', + 'Charles', + 'Sarah', + 'Thomas', + 'Karen', + ]; + final List> contactConfigs = [ + {'count': 3, 'requested': true}, + {'count': 4, 'requested': false, 'accepted': true}, + {'count': 1, 'accepted': true, 'blocked': true}, + {'count': 1, 'accepted': true, 'archived': true}, + {'count': 2, 'accepted': true, 'pinned': true}, + {'count': 1, 'requested': false}, + ]; + + int counter = 0; + + for (var config in contactConfigs) { + for (int i = 0; i < config['count']; i++) { + if (counter >= commonUsernames.length) { + break; + } + String username = commonUsernames[counter]; + int userId = Random().nextInt(1000000); + await twonlyDB.contactsDao.insertContact( + ContactsCompanion( + username: Value(username), + userId: Value(userId), + requested: Value(config['requested'] ?? false), + accepted: Value(config['accepted'] ?? false), + blocked: Value(config['blocked'] ?? false), + archived: Value(config['archived'] ?? false), + pinned: Value(config['pinned'] ?? false), + ), + ); + if (config['accepted'] ?? false) { + for (var i = 0; i < 20; i++) { + int chatId = Random().nextInt(chatMessages.length); + int? messageId = await twonlyDB.messagesDao.insertMessage( + MessagesCompanion( + contactId: Value(userId), + kind: Value(MessageKind.textMessage), + sendAt: Value(chatMessages[chatId][1]), + acknowledgeByServer: Value(true), + acknowledgeByUser: Value(true), + messageOtherId: + Value(Random().nextBool() ? Random().nextInt(10000) : null), + // responseToOtherMessageId: Value(content.responseToMessageId), + // responseToMessageId: Value(content.responseToOtherMessageId), + downloadState: Value(DownloadState.downloaded), + contentJson: Value( + jsonEncode(TextMessageContent(text: chatMessages[chatId][0])), + ), + ), + ); + } + } + counter++; + } + } +} + +Future createFakeDemoData() async { + await insertDemoContacts(); +} + +List> chatMessages = [ + [ + "Lorem ipsum dolor sit amet.", + DateTime.now().subtract(Duration(minutes: 20)) + ], + [ + "Consectetur adipiscing elit.", + DateTime.now().subtract(Duration(minutes: 19)) + ], + [ + "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + DateTime.now().subtract(Duration(minutes: 18)) + ], + ["Ut enim ad minim veniam.", DateTime.now().subtract(Duration(minutes: 17))], + [ + "Quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + DateTime.now().subtract(Duration(minutes: 16)) + ], + [ + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.", + DateTime.now().subtract(Duration(minutes: 15)) + ], + [ + "Excepteur sint occaecat cupidatat non proident.", + DateTime.now().subtract(Duration(minutes: 14)) + ], + [ + "Sunt in culpa qui officia deserunt mollit anim id est laborum.", + DateTime.now().subtract(Duration(minutes: 13)) + ], + [ + "Curabitur pretium tincidunt lacus.", + DateTime.now().subtract(Duration(minutes: 12)) + ], + ["Nulla facilisi.", DateTime.now().subtract(Duration(minutes: 11))], + [ + "Aenean lacinia bibendum nulla sed consectetur.", + DateTime.now().subtract(Duration(minutes: 10)) + ], + [ + "Sed posuere consectetur est at lobortis.", + DateTime.now().subtract(Duration(minutes: 9)) + ], + [ + "Vestibulum id ligula porta felis euismod semper.", + DateTime.now().subtract(Duration(minutes: 8)) + ], + [ + "Cras justo odio, dapibus ac facilisis in, egestas eget quam.", + DateTime.now().subtract(Duration(minutes: 7)) + ], + [ + "Morbi leo risus, porta ac consectetur ac, vestibulum at eros.", + DateTime.now().subtract(Duration(minutes: 6)) + ], + [ + "Praesent commodo cursus magna, vel scelerisque nisl consectetur et.", + DateTime.now().subtract(Duration(minutes: 5)) + ], + [ + "Donec ullamcorper nulla non metus auctor fringilla.", + DateTime.now().subtract(Duration(minutes: 4)) + ], + [ + "Etiam porta sem malesuada magna mollis euismod.", + DateTime.now().subtract(Duration(minutes: 3)) + ], + [ + "Aenean lacinia bibendum nulla sed consectetur.", + DateTime.now().subtract(Duration(minutes: 2)) + ], + [ + "Nullam quis risus eget urna mollis ornare vel eu leo.", + DateTime.now().subtract(Duration(minutes: 1)) + ], + ["Curabitur blandit tempus porttitor.", DateTime.now()], +]; diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index a37751e..ac92bb2 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; +import 'package:restart_app/restart_app.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/services/api/media_received.dart'; +import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/chats/chat_list_components/connection_info.comp.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/initialsavatar.dart'; @@ -37,6 +39,7 @@ class _ChatListViewState extends State { GlobalKey firstUserListItemKey = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey(); + Timer? tutorial; @override void initState() { @@ -54,7 +57,8 @@ class _ChatListViewState extends State { ; }); - Future.delayed(Duration(seconds: 1), () async { + tutorial = Timer(Duration(seconds: 1), () async { + tutorial = null; if (!mounted) return; await showChatListTutorialSearchOtherUsers(context, searchForOtherUsers); if (!mounted) return; @@ -66,6 +70,7 @@ class _ChatListViewState extends State { @override void dispose() { + tutorial?.cancel(); _contactsSub.cancel(); super.dispose(); } @@ -183,14 +188,50 @@ class _ChatListViewState extends State { return 72; }, itemBuilder: (context, index) { + if (gIsDemoUser && index == 0) { + return Container( + color: isDarkMode(context) + ? Colors.white + : Colors.black, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "This is a Demo-Preview.", + textAlign: TextAlign.center, + style: TextStyle( + color: !isDarkMode(context) + ? Colors.white + : Colors.black, + fontSize: 18, + ), + ), + FilledButton( + onPressed: () async { + await deleteLocalUserData(); + Restart.restartApp( + notificationTitle: 'Demo-Mode exited.', + notificationBody: + 'Click here to open the app again', + ); + }, + child: Text("Register"), + ) + ], + ), + ); + } // Check if the index is for the pinned users if (index < _pinnedContacts.length) { final contact = _pinnedContacts[index]; return UserListItem( key: ValueKey(contact.userId), user: contact, - firstUserListItemKey: - (index == 0) ? firstUserListItemKey : null, + firstUserListItemKey: (index == 0 && !gIsDemoUser || + index == 1 && gIsDemoUser) + ? firstUserListItemKey + : null, ); } diff --git a/lib/src/views/chats/chat_list_components/connection_info.comp.dart b/lib/src/views/chats/chat_list_components/connection_info.comp.dart index 1edf804..d4d131d 100644 --- a/lib/src/views/chats/chat_list_components/connection_info.comp.dart +++ b/lib/src/views/chats/chat_list_components/connection_info.comp.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart'; class ConnectionInfo extends StatefulWidget { @@ -60,7 +61,7 @@ class _ConnectionInfoWidgetState extends State @override Widget build(BuildContext context) { - if (!showAnimation) return Container(); + if (!showAnimation || gIsDemoUser) return Container(); double screenWidth = MediaQuery.of(context).size.width; return SizedBox( diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index c18f1d9..543d2d6 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -51,6 +51,7 @@ class _ChatMessagesViewState extends State { Message? responseToMessage; GlobalKey verifyShieldKey = GlobalKey(); late FocusNode textFieldFocus; + Timer? tutorial; @override void initState() { @@ -59,7 +60,8 @@ class _ChatMessagesViewState extends State { textFieldFocus = FocusNode(); initStreams(); - Future.delayed(Duration(seconds: 1), () async { + tutorial = Timer(Duration(seconds: 1), () async { + tutorial = null; if (!mounted) return; await showVerifyShieldTutorial(context, verifyShieldKey); }); @@ -70,6 +72,7 @@ class _ChatMessagesViewState extends State { super.dispose(); userSub.cancel(); messageSub.cancel(); + tutorial?.cancel(); textFieldFocus.dispose(); } diff --git a/lib/src/views/register.view.dart b/lib/src/views/register.view.dart index 54db0cd..040e753 100644 --- a/lib/src/views/register.view.dart +++ b/lib/src/views/register.view.dart @@ -24,9 +24,10 @@ class _RegisterViewState extends State { bool _isTryingToRegister = false; - Future createNewUser() async { - String username = usernameController.text; + Future createNewUser({bool isDemoAccount = false}) async { + String username = (isDemoAccount) ? "" : usernameController.text; String inviteCode = inviteCodeController.text; + setState(() { _isTryingToRegister = true; }); @@ -35,30 +36,44 @@ class _RegisterViewState extends State { await createIfNotExistsSignalIdentity(); - final res = await apiService.register(username, inviteCode); + int userId = 0; + + if (!isDemoAccount) { + final res = await apiService.register(username, inviteCode); + if (res.isSuccess) { + Log.info("Got user_id ${res.value} from server"); + userId = res.value.userid.toInt(); + } else { + if (mounted) { + showAlertDialog( + context, + "Oh no!", + errorCodeToText(context, res.error), + ); + } + return; + } + } setState(() { _isTryingToRegister = false; }); - if (res.isSuccess) { - Log.info("Got user_id ${res.value} from server"); - final userData = UserData( - userId: res.value.userid.toInt(), - username: username, - displayName: username, - subscriptionPlan: "Preview", - ); - storage.write(key: "userData", value: jsonEncode(userData)); - apiService.authenticate(); - widget.callbackOnSuccess(); - return; - } - - if (context.mounted) { - // ignore: use_build_context_synchronously - showAlertDialog(context, "Oh no!", errorCodeToText(context, res.error)); + final userData = UserData( + userId: userId, + username: username, + displayName: username, + subscriptionPlan: "Preview", + isDemoUser: isDemoAccount, + ); + storage.write(key: "userData", value: jsonEncode(userData)); + if (!isDemoAccount) { + await apiService.authenticate(); + } else { + gIsDemoUser = true; + createFakeDemoData(); } + widget.callbackOnSuccess(); } @override @@ -174,12 +189,23 @@ class _RegisterViewState extends State { ), ), const SizedBox(height: 10), - OutlinedButton.icon( - onPressed: () { - showAlertDialog(context, "Coming soon", - "This feature is not yet implemented! Just create a new account :/"); - }, - label: Text("Restore identity"), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + OutlinedButton.icon( + onPressed: () { + createNewUser(isDemoAccount: true); + }, + label: Text("Demo"), + ), + OutlinedButton.icon( + onPressed: () { + showAlertDialog(context, "Coming soon", + "This feature is not yet implemented! Just create a new account :/"); + }, + label: Text("Restore identity"), + ), + ], ), ]), // ), diff --git a/lib/src/views/settings/subscription/select_payment.view.dart b/lib/src/views/settings/subscription/select_payment.view.dart index 17a0f76..6710067 100644 --- a/lib/src/views/settings/subscription/select_payment.view.dart +++ b/lib/src/views/settings/subscription/select_payment.view.dart @@ -49,8 +49,12 @@ class _SelectPaymentViewState extends State { Future initAsync() async { final balance = await loadPlanBalance(); - balanceInCents = - balance!.transactions.map((a) => a.depositCents.toInt()).sum; + if (balance == null) { + balanceInCents = 0; + } else { + balanceInCents = + balance.transactions.map((a) => a.depositCents.toInt()).sum; + } setState(() {}); } @@ -253,7 +257,7 @@ class _SelectPaymentViewState extends State { children: [ TextButton( onPressed: () => launchUrl(Uri.parse( - "https://twonly.eu/legal/de/#revocation-policy")), + "https://twonly.eu/de/legal/#revocation-policy")), child: Text( "Widerrufsbelehrung", style: TextStyle(color: Colors.blue), @@ -261,7 +265,7 @@ class _SelectPaymentViewState extends State { ), TextButton( onPressed: () => launchUrl( - Uri.parse("https://twonly.eu/legal/de/agb.html")), + Uri.parse("https://twonly.eu/de/legal/agb.html")), child: Text( "ABG", style: TextStyle(color: Colors.blue), diff --git a/pubspec.yaml b/pubspec.yaml index c782279..b646d61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec # Prevent accidental publishing to pub.dev. publish_to: 'none' -version: 0.0.30+30 +version: 0.0.31+31 environment: sdk: ^3.6.0