added demo modus

This commit is contained in:
otsmr 2025-06-01 23:13:22 +02:00
parent 602443eea8
commit a779512198
13 changed files with 310 additions and 43 deletions

View file

@ -8,3 +8,4 @@ late ApiService apiService;
late TwonlyDatabase twonlyDB;
List<CameraDescription> gCameras = <CameraDescription>[];
bool gIsDemoUser = false;

View file

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

View file

@ -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")

View file

@ -11,6 +11,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> 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<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson,
'avatarCounter': instance.avatarCounter,
'isDemoUser': instance.isDemoUser,
'defaultShowTime': instance.defaultShowTime,
'subscriptionPlan': instance.subscriptionPlan,
'useHighQuality': instance.useHighQuality,

View file

@ -114,6 +114,13 @@ class ApiService {
}
Future<bool> 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<bool>(() async {
if (_channel != null) {
return true;

View file

@ -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) {

View file

@ -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<String> commonUsernames = [
'James',
'Mary',
'John',
'Patricia',
'Robert',
'Jennifer',
'Michael',
'Linda',
'William',
'Elizabeth',
'David',
'Barbara',
'Richard',
'Susan',
'Joseph',
'Jessica',
'Charles',
'Sarah',
'Thomas',
'Karen',
];
final List<Map<String, dynamic>> 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<List<dynamic>> 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()],
];

View file

@ -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<ChatListView> {
GlobalKey firstUserListItemKey = GlobalKey();
GlobalKey searchForOtherUsers = GlobalKey();
Timer? tutorial;
@override
void initState() {
@ -54,7 +57,8 @@ class _ChatListViewState extends State<ChatListView> {
;
});
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<ChatListView> {
@override
void dispose() {
tutorial?.cancel();
_contactsSub.cancel();
super.dispose();
}
@ -183,14 +188,50 @@ class _ChatListViewState extends State<ChatListView> {
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,
);
}

View file

@ -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<ConnectionInfo>
@override
Widget build(BuildContext context) {
if (!showAnimation) return Container();
if (!showAnimation || gIsDemoUser) return Container();
double screenWidth = MediaQuery.of(context).size.width;
return SizedBox(

View file

@ -51,6 +51,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
Message? responseToMessage;
GlobalKey verifyShieldKey = GlobalKey();
late FocusNode textFieldFocus;
Timer? tutorial;
@override
void initState() {
@ -59,7 +60,8 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
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<ChatMessagesView> {
super.dispose();
userSub.cancel();
messageSub.cancel();
tutorial?.cancel();
textFieldFocus.dispose();
}

View file

@ -24,9 +24,10 @@ class _RegisterViewState extends State<RegisterView> {
bool _isTryingToRegister = false;
Future createNewUser() async {
String username = usernameController.text;
Future createNewUser({bool isDemoAccount = false}) async {
String username = (isDemoAccount) ? "<demo>" : usernameController.text;
String inviteCode = inviteCodeController.text;
setState(() {
_isTryingToRegister = true;
});
@ -35,30 +36,44 @@ class _RegisterViewState extends State<RegisterView> {
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<RegisterView> {
),
),
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"),
),
],
),
]),
// ),

View file

@ -49,8 +49,12 @@ class _SelectPaymentViewState extends State<SelectPaymentView> {
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<SelectPaymentView> {
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<SelectPaymentView> {
),
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),

View file

@ -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