register process works

This commit is contained in:
otsmr 2025-01-19 23:03:37 +01:00
parent d895fd450e
commit d152240c16
8 changed files with 239 additions and 141 deletions

View file

@ -3,8 +3,8 @@ import 'views/home_view.dart';
import 'views/register_view.dart'; import 'views/register_view.dart';
import 'utils.dart'; import 'utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'settings/settings_controller.dart'; import 'settings/settings_controller.dart';
@ -34,16 +34,17 @@ class _MyAppState extends State<MyApp> {
builder: (BuildContext context, Widget? child) { builder: (BuildContext context, Widget? child) {
return MaterialApp( return MaterialApp(
restorationScopeId: 'app', restorationScopeId: 'app',
// localizationsDelegates: const [ localizationsDelegates: const [
// AppLocalizations.delegate, AppLocalizations.delegate,
// GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
// GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
// GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
// ],
supportedLocales: const [
Locale('en', ''), // English, no country code
], ],
onGenerateTitle: (BuildContext context) => "Connect!", supportedLocales: const [
Locale('en', ''),
Locale('de', ''),
],
onGenerateTitle: (BuildContext context) => "twonly",
theme: ThemeData( theme: ThemeData(
colorScheme: colorScheme:
ColorScheme.fromSeed(seedColor: const Color(0xFF57CC99)), ColorScheme.fromSeed(seedColor: const Color(0xFF57CC99)),

View file

@ -0,0 +1,6 @@
{
"@@locale": "de",
"registerTitle": "Willkommen bei twonly",
"@registerTitle": {},
"registerSlogan": "Sende Bilder in Echtzeit an Freunde und sei dir sicher, dass nur ihr sie sehen könnt."
}

View file

@ -1,7 +1,22 @@
{ {
"appTitle": "connect",
"@@locale": "en", "@@locale": "en",
"@appTitle": { "registerTitle": "Welcome to twonly!",
"description": "The title of the application" "registerSlogan": "Send pictures to friends in real time and be sure you are the only people who can see them.",
} "registerUsernameSlogan": "Please select a username so others can find you!",
"registerUsernameDecoration": "Username",
"registerUsernameLimits": "Username must be 4 to 12 characters long, consisting only of letters (a-z) and numbers (0-9).",
"registerSubmitButton": "Register now!",
"errorUnknown": "An unexpected error has occurred. Please try again later.",
"errorBadRequest": "The request could not be understood by the server due to malformed syntax. Please check your input and try again.",
"errorTooManyRequests": "You have made too many requests in a short period. Please wait a moment before trying again.",
"errorInternalError": "The server encountered an internal error. Please try again later.",
"errorInvalidInvitationCode": "The invitation code you provided is invalid. Please check the code and try again.",
"errorUsernameAlreadyTaken": "The username you want to use is already taken. Please choose a different username.",
"errorSignatureNotValid": "The provided signature is not valid. Please check your credentials and try again.",
"errorUsernameNotFound": "The username you entered does not exist. Please check the spelling or create a new account.",
"errorUsernameNotValid": "The username you provided does not meet the required criteria. Please choose a valid username.",
"errorInvalidPublicKey": "The public key you provided is invalid. Please check the key and try again.",
"errorSessionAlreadyAuthenticated": "You are already logged in. Please log out if you want to log in with a different account.",
"errorSessionNotAuthenticated": "Your session is not authenticated. Please log in to continue.",
"errorOnlyOneSessionAllowed": "Only one active session is allowed per user. Please log out from other devices to continue."
} }

View file

@ -4,13 +4,27 @@ import 'dart:ffi';
import 'dart:math'; import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:twonly/src/proto/api/client_to_server.pb.dart'; import 'package:twonly/src/proto/api/client_to_server.pb.dart';
import 'package:twonly/src/proto/api/error.pb.dart';
import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server;
import 'package:twonly/src/signal/signal_helper.dart'; import 'package:twonly/src/signal/signal_helper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
class Result<T, E> {
final T? value;
final E? error;
bool get isSuccess => value != null;
bool get isError => error != null;
Result.success(this.value) : error = null;
Result.error(this.error) : value = null;
}
class ApiProvider { class ApiProvider {
ApiProvider({required this.apiUrl}); ApiProvider({required this.apiUrl});
@ -77,7 +91,7 @@ class ApiProvider {
log.shout("Timeout for message $seq"); log.shout("Timeout for message $seq");
return null; return null;
} }
await Future.delayed(Duration(milliseconds: 1)); await Future.delayed(Duration(milliseconds: 10));
} }
} }
@ -101,21 +115,6 @@ class ApiProvider {
return await _waitForResponse(seq); return await _waitForResponse(seq);
} }
String? _getErrorMsg(server.ServerToClient msg) {
// if (msg.containsKey("Ok")) {
// return null;
// }
// if (msg.containsKey("Error")) {
// if (msg["Error"] != null) {
// if (msg["Error"].containsKey("AlertUser")) {
// return msg["Error"]["AlertUser"];
// }
// }
// return "There was an unknown error :/";
// }
return null;
}
ClientToServer createClientToServerFromHandshake(Handshake handshake) { ClientToServer createClientToServerFromHandshake(Handshake handshake) {
// Create the V0 message // Create the V0 message
var v0 = V0() var v0 = V0()
@ -128,27 +127,63 @@ class ApiProvider {
return clientToServer; return clientToServer;
} }
Future<String?> register( static String getLocalizedString(BuildContext context, ErrorCode code) {
String username, switch (code.toString()) {
// Uint8List publicIdentityKey, case "Unknown":
// Uint8List signedPrekey, return AppLocalizations.of(context)!.errorUnknown;
// Uint8List signedPrekeySignature, case "BadRequest":
String? inviteCode) async { return AppLocalizations.of(context)!.errorBadRequest;
case "TooManyRequests":
return AppLocalizations.of(context)!.errorTooManyRequests;
case "InternalError":
return AppLocalizations.of(context)!.errorInternalError;
case "InvalidInvitationCode":
return AppLocalizations.of(context)!.errorInvalidInvitationCode;
case "UsernameAlreadyTaken":
return AppLocalizations.of(context)!.errorUsernameAlreadyTaken;
case "SignatureNotValid":
return AppLocalizations.of(context)!.errorSignatureNotValid;
case "UsernameNotFound":
return AppLocalizations.of(context)!.errorUsernameNotFound;
case "UsernameNotValid":
return AppLocalizations.of(context)!.errorUsernameNotValid;
case "InvalidPublicKey":
return AppLocalizations.of(context)!.errorInvalidPublicKey;
case "SessionAlreadyAuthenticated":
return AppLocalizations.of(context)!.errorSessionAlreadyAuthenticated;
case "SessionNotAuthenticated":
return AppLocalizations.of(context)!.errorSessionNotAuthenticated;
case "OnlyOneSessionAllowed":
return AppLocalizations.of(context)!.errorOnlyOneSessionAllowed;
default:
return code.toString(); // Fallback for unrecognized keys
}
}
Result _asResult(server.ServerToClient msg) {
if (msg.v0.response.hasOk()) {
return Result.success(msg.v0.response.ok);
} else {
return Result.error(msg.v0.response.error);
}
}
Future<Result> register(String username, String? inviteCode) async {
final reqSignal = await SignalHelper.getRegisterData(); final reqSignal = await SignalHelper.getRegisterData();
if (reqSignal == null) { if (reqSignal == null) {
print("NULL"); return Result.error(
return null; "There was an fatal error. Try reinstalling the app.");
} }
print(reqSignal);
var register = Handshake_Register() var register = Handshake_Register()
..username = username ..username = username
..publicIdentityKey = reqSignal["identityKey"]
..signedPrekey = reqSignal["signedPreKey"]?["key"] ..signedPrekey = reqSignal["signedPreKey"]?["key"]
..signedPrekeySignature = reqSignal["signedPreKey"]?["signature"] ..signedPrekeySignature = reqSignal["signedPreKey"]?["signature"]
..signedPrekeyId = Int64(reqSignal["signedPreKey"]?["id"]); ..signedPrekeyId = Int64(reqSignal["signedPreKey"]?["id"]);
if (inviteCode != null) { if (inviteCode != null && inviteCode != "") {
register.inviteCode = inviteCode; register.inviteCode = inviteCode;
} }
// Create the Handshake message // Create the Handshake message
@ -157,8 +192,8 @@ class ApiProvider {
final resp = await _sendRequestV0(req); final resp = await _sendRequestV0(req);
if (resp == null) { if (resp == null) {
return "Server is not reachable!"; return Result.error("Server is not reachable!");
} }
return _getErrorMsg(resp); return _asResult(resp);
} }
} }

View file

@ -1,9 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:twonly/main.dart'; import 'package:twonly/main.dart';
import 'package:twonly/src/signal/signal_helper.dart'; import 'package:twonly/src/signal/signal_helper.dart';
import 'package:twonly/src/providers/api_provider.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'model/signal_identity_json.dart';
import 'model/user_data_json.dart'; import 'model/user_data_json.dart';
import 'package:logging/logging.dart';
// Just a helper function to get the secure storage // Just a helper function to get the secure storage
FlutterSecureStorage getSecureStorage() { FlutterSecureStorage getSecureStorage() {
@ -38,24 +39,25 @@ Future<UserData?> getUser() async {
Future<bool> deleteLocalUserData() async { Future<bool> deleteLocalUserData() async {
final storage = getSecureStorage(); final storage = getSecureStorage();
await storage.delete(key: "user"); await storage.delete(key: "user_data");
await storage.delete(key: "signal_identity");
return true; return true;
} }
Future<String?> createNewUser(String username, String inviteCode) async { Future<Result> createNewUser(String username, String inviteCode) async {
final storage = getSecureStorage(); final storage = getSecureStorage();
await SignalHelper.createIfNotExistsSignalIdentity(); await SignalHelper.createIfNotExistsSignalIdentity();
// TODO: API call to server to check username and inviteCode // TODO: API call to server to check username and inviteCode
final check = await apiProvider.register(username, inviteCode); final res = await apiProvider.register(username, inviteCode);
if (check != null) {
return check; if (res.isSuccess) {
print("Got user_id ${res.value}");
final userData = UserData(
userId: res.value.userid, username: username, displayName: username);
storage.write(key: "user_data", value: jsonEncode(userData));
} }
print(check); return res;
// await storage.write(key: "user_data", value: jsonEncode(userData));
return null;
} }

View file

@ -1,6 +1,9 @@
import 'package:twonly/src/providers/api_provider.dart';
import '../utils.dart'; import '../utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class RegisterView extends StatefulWidget { class RegisterView extends StatefulWidget {
const RegisterView({super.key, required this.callbackOnSuccess}); const RegisterView({super.key, required this.callbackOnSuccess});
@ -10,36 +13,12 @@ class RegisterView extends StatefulWidget {
State<RegisterView> createState() => _RegisterViewState(); State<RegisterView> createState() => _RegisterViewState();
} }
class MyButton extends StatelessWidget {
final void Function()? onTap;
final String text;
const MyButton({super.key, required this.onTap, required this.text});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(9),
),
child: Center(
child: Text(
text,
textAlign: TextAlign.center,
),
),
),
);
}
}
class _RegisterViewState extends State<RegisterView> { class _RegisterViewState extends State<RegisterView> {
final TextEditingController usernameController = TextEditingController(); final TextEditingController usernameController = TextEditingController();
final TextEditingController inviteCodeController = TextEditingController(); final TextEditingController inviteCodeController = TextEditingController();
bool _isTryingToRegister = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
InputDecoration getInputDecoration(hintText) { InputDecoration getInputDecoration(hintText) {
@ -48,77 +27,136 @@ class _RegisterViewState extends State<RegisterView> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text("Welcome to Connect!"), title: Text(""),
), ),
body: Padding( body: Padding(
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10),
child: ListView( child: ListView(
children: [ children: [
const SizedBox(height: 20), const SizedBox(height: 50),
Center( Text(
AppLocalizations.of(context)!.registerTitle,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 30),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 30),
child: Text( child: Text(
"You made the right decision using Connect which is like SnXpchat but encrypted using the Signal protocol.", AppLocalizations.of(context)!.registerSlogan,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
),
const SizedBox(height: 60),
Center(
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10),
child: Text(
AppLocalizations.of(context)!.registerUsernameSlogan,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 15), style: TextStyle(fontSize: 15),
), ),
), ),
const SizedBox(height: 40),
Center(
child: Text(
"Choice wisely, this username can't be changed. Only lowercase and numbers are allowed!",
textAlign: TextAlign.center,
), ),
), const SizedBox(height: 15),
const SizedBox(height: 10),
TextField( TextField(
controller: usernameController, controller: usernameController,
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter( LengthLimitingTextInputFormatter(12),
12), // Limit to 12 characters FilteringTextInputFormatter.allow(RegExp(r'[a-z0-9]')),
FilteringTextInputFormatter.allow(RegExp(
r'[a-z0-9]')), // Allow only lowercase letters and numbers
], ],
decoration: getInputDecoration("Username")), style: TextStyle(fontSize: 17),
const SizedBox(height: 15), decoration: getInputDecoration(
AppLocalizations.of(context)!.registerUsernameDecoration,
),
),
const SizedBox(height: 5),
Center( Center(
child: Padding(
padding: EdgeInsets.only(left: 10, right: 10),
child: Text( child: Text(
"To protect this small experimental project you need an invitation code! To get one just ask the right person!", AppLocalizations.of(context)!.registerUsernameLimits,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 7),
), ),
), ),
const SizedBox(height: 10),
TextField(
controller: inviteCodeController,
decoration: getInputDecoration("Invitation code")),
const SizedBox(height: 25),
Center(
child: Text(
"Where is the password? There is none! So make a backup of your Connect identity in the settings or you will lose your access if you lose your device!",
textAlign: TextAlign.center,
), ),
), // const SizedBox(height: 15),
const SizedBox(height: 30), // Center(
// child: Text(
// "To protect this small experimental project you need an invitation code! To get one just ask the right person!",
// textAlign: TextAlign.center,
// ),
// ),
// const SizedBox(height: 10),
// TextField(
// controller: inviteCodeController,
// decoration: getInputDecoration("Voucher code")),
// const SizedBox(height: 25),
// Center(
// child: Text(
// "Please ",
// textAlign: TextAlign.center,
// ),
// ),
const SizedBox(height: 50),
// Padding(
// padding: EdgeInsets.symmetric(horizontal: 10),
Column(children: [
FilledButton.icon( FilledButton.icon(
icon: Icon(Icons.group), icon: _isTryingToRegister
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.black,
strokeWidth: 2,
),
)
: Icon(Icons.group),
onPressed: () async { onPressed: () async {
final success = await createNewUser( setState(() {
_isTryingToRegister = true;
});
final res = await createNewUser(
usernameController.text, inviteCodeController.text); usernameController.text, inviteCodeController.text);
if (success == null) { setState(() {
_isTryingToRegister = false;
});
if (res.isSuccess) {
widget.callbackOnSuccess(); widget.callbackOnSuccess();
return; return;
} }
showAlertDialog(context, "Oh no!", success); final errMsg =
ApiProvider.getLocalizedString(context, res.error);
showAlertDialog(context, "Oh no!", errMsg);
}, },
label: Text("Komm in die Gruppe!"), style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
), ),
backgroundColor: _isTryingToRegister
? WidgetStateProperty.all<MaterialColor>(
Colors.grey)
: null),
label: Text(
AppLocalizations.of(context)!.registerSubmitButton,
style: TextStyle(fontSize: 17),
),
),
const SizedBox(height: 10),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () { onPressed: () {
showAlertDialog(context, "Coming soon", showAlertDialog(context, "Coming soon",
"This feature is not yet implemented! Just create a new account :/"); "This feature is not yet implemented! Just create a new account :/");
}, },
label: Text("Restore identity")), label: Text("Restore identity")),
// MyButton(onTap: () {}, text: "Komm in die Gruppe!") ]),
// ),
], ],
),
)), )),
); );
} }

View file

@ -475,7 +475,7 @@ packages:
source: hosted source: hosted
version: "4.5.2" version: "4.5.2"
intl: intl:
dependency: transitive dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf

View file

@ -21,6 +21,7 @@ dependencies:
flutter_secure_storage: ^9.2.2 flutter_secure_storage: ^9.2.2
google_fonts: ^6.2.1 google_fonts: ^6.2.1
image: ^4.3.0 image: ^4.3.0
intl: any
json_annotation: ^4.9.0 json_annotation: ^4.9.0
libsignal_protocol_dart: ^0.7.1 libsignal_protocol_dart: ^0.7.1
logging: ^1.3.0 logging: ^1.3.0