diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 2fcc4e7..630080f 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -355,7 +355,7 @@ class MatrixState extends State { @override void initState() { if (widget.client == null) { - client = Client(widget.clientName, debug: false); + client = Client(widget.clientName, debug: true); if (!kIsWeb) { _initWithStore(); } else { diff --git a/lib/main.dart b/lib/main.dart index 3ba2bbc..344d480 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,10 @@ import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/views/sign_up.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'components/matrix.dart'; import 'views/chat_list.dart'; -import 'views/login.dart'; void main() { SystemChrome.setSystemUIOverlayStyle( @@ -58,7 +58,7 @@ class App extends StatelessWidget { ); } if (Matrix.of(context).client.isLogged()) return ChatListView(); - return LoginPage(); + return SignUp(); }, ), ), diff --git a/lib/views/login.dart b/lib/views/login.dart index b71905a..98250e9 100644 --- a/lib/views/login.dart +++ b/lib/views/login.dart @@ -9,12 +9,12 @@ import 'chat_list.dart'; const String defaultHomeserver = "https://matrix.org"; -class LoginPage extends StatefulWidget { +class Login extends StatefulWidget { @override - _LoginPageState createState() => _LoginPageState(); + _LoginState createState() => _LoginState(); } -class _LoginPageState extends State { +class _LoginState extends State { final TextEditingController usernameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); final TextEditingController serverController = @@ -23,12 +23,12 @@ class _LoginPageState extends State { String passwordError; String serverError; bool loading = false; + bool showPassword = false; void login(BuildContext context) async { MatrixState matrix = Matrix.of(context); if (usernameController.text.isEmpty) { setState(() => usernameError = "Please enter your username."); - print("Please enter your username."); } else { setState(() => usernameError = null); } @@ -63,8 +63,9 @@ class _LoginPageState extends State { } try { print("[Login] Try to login..."); - await matrix.client - .login(usernameController.text, passwordController.text); + await matrix.client.login( + usernameController.text, passwordController.text, + initialDeviceDisplayName: matrix.widget.clientName); } on MatrixException catch (exception) { setState(() => passwordError = exception.errorMessage); return setState(() => loading = false); @@ -109,55 +110,67 @@ class _LoginPageState extends State { vertical: 16, horizontal: max((MediaQuery.of(context).size.width - 600) / 2, 16)), children: [ - Image.asset("assets/fluffychat-banner.png"), - TextField( - controller: usernameController, - decoration: InputDecoration( - hintText: "@username:domain", - icon: Icon(Icons.account_box), - errorText: usernameError, - labelText: "Username"), + Container( + height: 150, + color: Theme.of(context).secondaryHeaderColor, + child: Center( + child: Icon( + Icons.vpn_key, + color: Theme.of(context).primaryColor, + size: 40, + ), + ), ), - TextField( - controller: passwordController, - obscureText: true, - onSubmitted: (t) => login(context), - decoration: InputDecoration( - icon: Icon(Icons.vpn_key), - hintText: "****", - errorText: passwordError, - labelText: "Password"), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blue, + child: Icon(Icons.account_box), + ), + title: TextField( + controller: usernameController, + decoration: InputDecoration( + hintText: "@username:domain", + errorText: usernameError, + labelText: "Username"), + ), + ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.yellow, + child: Icon(Icons.lock), + ), + title: TextField( + controller: passwordController, + obscureText: !showPassword, + onSubmitted: (t) => login(context), + decoration: InputDecoration( + hintText: "****", + errorText: passwordError, + suffixIcon: IconButton( + icon: Icon( + showPassword ? Icons.visibility_off : Icons.visibility), + onPressed: () => + setState(() => showPassword = !showPassword), + ), + labelText: "Password"), + ), ), SizedBox(height: 20), - Card( - elevation: 7, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(50), - ), - child: Container( - width: 120.0, - height: 50.0, - decoration: BoxDecoration( + Container( + height: 50, + child: RaisedButton( + elevation: 7, + color: Theme.of(context).primaryColor, + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), - gradient: LinearGradient( - begin: Alignment.bottomLeft, - end: Alignment.topRight, - colors: [ - Colors.blue, - Theme.of(context).primaryColor, - ], - ), - ), - child: RawMaterialButton( - onPressed: () => loading ? null : login(context), - splashColor: Colors.grey, - child: loading - ? CircularProgressIndicator() - : Text( - "Login", - style: TextStyle(color: Colors.white, fontSize: 20.0), - ), ), + child: loading + ? CircularProgressIndicator() + : Text( + "Login", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + onPressed: () => loading ? null : login(context), ), ), ], diff --git a/lib/views/settings.dart b/lib/views/settings.dart index 2e48795..3064b25 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -6,7 +6,7 @@ import 'package:fluffychat/components/content_banner.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/utils/app_route.dart'; import 'package:fluffychat/views/chat_list.dart'; -import 'package:fluffychat/views/login.dart'; +import 'package:fluffychat/views/sign_up.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -37,7 +37,7 @@ class _SettingsState extends State { await matrix.tryRequestWithLoadingDialog(matrix.client.logout()); matrix.clean(); await Navigator.of(context).pushAndRemoveUntil( - AppRoute.defaultRoute(context, LoginPage()), (r) => false); + AppRoute.defaultRoute(context, SignUp()), (r) => false); } void setDisplaynameAction(BuildContext context, String displayname) async { diff --git a/lib/views/sign_up.dart b/lib/views/sign_up.dart new file mode 100644 index 0000000..66da5c9 --- /dev/null +++ b/lib/views/sign_up.dart @@ -0,0 +1,186 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/views/login.dart'; +import 'package:fluffychat/views/sign_up_password.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; + +class SignUp extends StatefulWidget { + @override + _SignUpState createState() => _SignUpState(); +} + +class _SignUpState extends State { + final TextEditingController usernameController = TextEditingController(); + final TextEditingController serverController = + TextEditingController(text: "matrix.org"); + String usernameError; + String serverError; + bool loading = false; + File avatar; + + void setAvatarAction() async { + File file = await ImagePicker.pickImage( + source: ImageSource.gallery, + maxHeight: 512, + maxWidth: 512, + imageQuality: 50, + ); + if (file != null) setState(() => avatar = file); + } + + void signUpAction(BuildContext context) async { + MatrixState matrix = Matrix.of(context); + if (usernameController.text.isEmpty) { + setState(() => usernameError = "Please choose a username."); + } else { + setState(() => usernameError = null); + } + serverError = null; + + if (usernameController.text.isEmpty) { + return; + } + + final String preferredUsername = + usernameController.text.toLowerCase().replaceAll(" ", "-"); + + String homeserver = serverController.text; + if (homeserver.isEmpty) homeserver = defaultHomeserver; + if (!homeserver.startsWith("https://")) { + homeserver = "https://" + homeserver; + } + + try { + print("[Sign Up] Check server..."); + setState(() => loading = true); + if (!await matrix.client.checkServer(homeserver)) { + setState(() => serverError = "Homeserver is not compatible."); + + return setState(() => loading = false); + } + } catch (exception) { + setState(() => serverError = "Connection attempt failed!"); + return setState(() => loading = false); + } + + try { + print("[Sign Up] Check if username is available..."); + await matrix.client.usernameAvailable(preferredUsername); + } on MatrixException catch (exception) { + setState(() => usernameError = exception.errorMessage); + return setState(() => loading = false); + } catch (exception) { + setState(() => usernameError = exception.toString()); + return setState(() => loading = false); + } + setState(() => loading = false); + await Navigator.of(context).push( + AppRoute.defaultRoute( + context, + SignUpPassword(preferredUsername, + avatar: avatar, displayname: usernameController.text), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: TextField( + controller: serverController, + decoration: InputDecoration( + icon: Icon(Icons.domain), + hintText: "matrix.org", + errorText: serverError, + errorMaxLines: 1, + prefixText: "https://", + labelText: serverError == null ? "Homeserver" : serverError), + ), + ), + body: ListView( + padding: EdgeInsets.symmetric( + vertical: 16, + horizontal: + max((MediaQuery.of(context).size.width - 600) / 2, 16)), + children: [ + Image.asset("assets/fluffychat-banner.png"), + ListTile( + leading: CircleAvatar( + backgroundImage: avatar == null ? null : FileImage(avatar), + backgroundColor: avatar == null + ? Colors.green + : Theme.of(context).secondaryHeaderColor, + child: avatar == null ? Icon(Icons.camera_alt) : null, + ), + trailing: avatar == null + ? null + : Icon( + Icons.close, + color: Colors.red, + ), + title: Text( + avatar == null ? "Set a profile picture" : "Discard picture"), + onTap: avatar == null + ? setAvatarAction + : () => setState(() => avatar = null), + ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blue, + child: Icon(Icons.account_box), + ), + title: TextField( + controller: usernameController, + onSubmitted: (s) => signUpAction(context), + decoration: InputDecoration( + hintText: "Username", + errorText: usernameError, + labelText: "Choose a username"), + ), + ), + SizedBox(height: 20), + Container( + height: 50, + child: RaisedButton( + elevation: 7, + color: Theme.of(context).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + child: loading + ? CircularProgressIndicator() + : Text( + "Sign up", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + onPressed: () => signUpAction(context), + ), + ), + Center( + child: FlatButton( + child: Text( + "Already have an account?", + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.blue, + ), + ), + onPressed: () => Navigator.of(context).push( + AppRoute.defaultRoute( + context, + Login(), + ), + ), + ), + ), + ]), + ); + } +} diff --git a/lib/views/sign_up_password.dart b/lib/views/sign_up_password.dart new file mode 100644 index 0000000..8c3d301 --- /dev/null +++ b/lib/views/sign_up_password.dart @@ -0,0 +1,151 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:flutter/material.dart'; + +import 'chat_list.dart'; + +class SignUpPassword extends StatefulWidget { + final File avatar; + final String username; + final String displayname; + const SignUpPassword(this.username, {this.avatar, this.displayname}); + @override + _SignUpPasswordState createState() => _SignUpPasswordState(); +} + +class _SignUpPasswordState extends State { + final TextEditingController passwordController = TextEditingController(); + String passwordError; + bool loading = false; + bool showPassword = true; + + void _signUpAction(BuildContext context, {Map auth}) async { + MatrixState matrix = Matrix.of(context); + if (passwordController.text.isEmpty) { + setState(() => passwordError = "Please enter your password."); + } else { + setState(() => passwordError = null); + } + + if (passwordController.text.isEmpty) { + return; + } + + try { + print("[Sign Up] Create account..."); + final Map response = await matrix.client.register( + username: widget.username, + password: passwordController.text, + initialDeviceDisplayName: matrix.widget.clientName, + auth: auth, + ); + if (response.containsKey("user_id") && + response.containsKey("access_token") && + response.containsKey("device_id")) { + await Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute(context, ChatListView()), (r) => false); + } else if (response.containsKey("flows")) { + final List stages = response["flows"]["stages"]; + for (int i = 0; i < stages.length;) { + if (stages[i] == "m.login.dummy") { + print("[Sign Up] Process m.login.dummy stage"); + _signUpAction(context, auth: { + "type": stages[i], + "session": response["session"], + }); + break; + } else if (stages[i] == "m.login.recaptcha") { + print("[Sign Up] Process m.login.recaptcha stage"); + final String publicKey = response["params"]["public_key"]; + + _signUpAction(context, auth: { + "type": stages[i], + "session": response["session"], + }); + break; + } + } + } + } on MatrixException catch (exception) { + setState(() => passwordError = exception.errorMessage); + return setState(() => loading = false); + } catch (exception) { + print(exception); + setState(() => passwordError = exception.toString()); + return setState(() => loading = false); + } + setState(() => loading = false); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Secure your account with a password"), + ), + body: ListView( + padding: EdgeInsets.symmetric( + vertical: 16, + horizontal: max((MediaQuery.of(context).size.width - 600) / 2, 16)), + children: [ + Container( + height: 150, + color: Theme.of(context).secondaryHeaderColor, + child: Center( + child: Icon( + Icons.vpn_key, + color: Theme.of(context).primaryColor, + size: 40, + ), + ), + ), + ListTile( + leading: CircleAvatar( + backgroundColor: Colors.yellow, + child: Icon(Icons.lock), + ), + title: TextField( + controller: passwordController, + obscureText: !showPassword, + autofocus: true, + autocorrect: false, + onSubmitted: (t) => _signUpAction(context), + decoration: InputDecoration( + hintText: "****", + errorText: passwordError, + suffixIcon: IconButton( + icon: Icon( + showPassword ? Icons.visibility_off : Icons.visibility), + onPressed: () => + setState(() => showPassword = !showPassword), + ), + labelText: "Password"), + ), + ), + SizedBox(height: 20), + Container( + height: 50, + child: RaisedButton( + elevation: 7, + color: Theme.of(context).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + child: loading + ? CircularProgressIndicator() + : Text( + "Create account now", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + onPressed: () => loading ? null : _signUpAction(context), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0cde9d6..855cd76 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -82,8 +82,8 @@ packages: dependency: "direct main" description: path: "." - ref: "5a3f88e979fc85cb876dbfecffd8230c9698f864" - resolved-ref: "5a3f88e979fc85cb876dbfecffd8230c9698f864" + ref: cc1be6bd18a5a3f73949f5448a301096ad62ee1b + resolved-ref: cc1be6bd18a5a3f73949f5448a301096ad62ee1b url: "https://gitlab.com/famedly/famedlysdk.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index 0f8575e..4d79253 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: famedlysdk: git: url: https://gitlab.com/famedly/famedlysdk.git - ref: 5a3f88e979fc85cb876dbfecffd8230c9698f864 + ref: cc1be6bd18a5a3f73949f5448a301096ad62ee1b localstorage: ^3.0.1+4 bubble: ^1.1.9+1