diff --git a/lib/api/grpc/accounts.dart b/lib/api/grpc/accounts.dart new file mode 100644 index 0000000..8bf223f --- /dev/null +++ b/lib/api/grpc/accounts.dart @@ -0,0 +1,52 @@ + +import 'package:grpc/grpc_web.dart'; +import 'package:softplayer_dart_proto/accounts/accounts_v1.pbgrpc.dart'; +import 'package:softplayer_dart_proto/main.dart'; + +class AccountsGrpc { + final GrpcWebClientChannel channel; + late AccountsClient accountsStub; + AccountsGrpc({ + required this.channel, + }); + + void init() { + accountsStub = AccountsClient(channel); + } + + Future signIn(String username, String email, String password) async { + final request = AccountWithPassword( + data: AccountData( + name: username, + email: email, + ), + password: AccountPassword( + password: password, + )); + try { + final response = await accountsStub.signIn(request); + print("$response"); + return "1"; + } catch (e) { + rethrow; + } + } + + Future signUp(String username, String email, String password) async { + final request = AccountWithPassword( + data: AccountData( + name: username, + email: email, + ), + password: AccountPassword( + password: password, + )); + try { + final response = await accountsStub.signUp(request); + print("$response"); + return "1"; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/components/menubar.dart b/lib/components/menubar.dart index 3d15d3e..6b6477d 100644 --- a/lib/components/menubar.dart +++ b/lib/components/menubar.dart @@ -1,16 +1,21 @@ -// This project is not supposed to be cross-platform, +// This project is not supposed to be cross-platform, // so we don't care about this warning // ignore: avoid_web_libraries_in_flutter import 'dart:html'; import 'package:flutter/material.dart'; +import 'package:softplayer_web/api/grpc/accounts.dart'; import 'package:softplayer_web/components/sign_in_form.dart'; import 'package:softplayer_web/components/sign_up_form.dart'; class MenuPanel extends StatefulWidget implements PreferredSizeWidget { final TabName tab; - const MenuPanel({super.key, required this.tab}) - : preferredSize = const Size.fromHeight(kToolbarHeight); + final AccountsGrpc accountsGrpc; + const MenuPanel({ + super.key, + required this.tab, + required this.accountsGrpc, + }) : preferredSize = const Size.fromHeight(kToolbarHeight); @override final Size preferredSize; @override @@ -37,14 +42,18 @@ class _MenuPanel extends State { onPressed: () { showDialog( context: context, - builder: (BuildContext context) => const SignInForm()); + builder: (BuildContext context) => SignInForm( + accountsGrpc: widget.accountsGrpc, + )); }, child: const Text("sign in")), TextButton( onPressed: () { showDialog( context: context, - builder: (BuildContext context) => const SignUpForm()); + builder: (BuildContext context) => SignUpForm( + accountsGrpc: widget.accountsGrpc, + )); }, child: const Text("sign up")), ]; diff --git a/lib/components/sign_in_form.dart b/lib/components/sign_in_form.dart index 0b7b700..7e2bc6a 100644 --- a/lib/components/sign_in_form.dart +++ b/lib/components/sign_in_form.dart @@ -1,64 +1,98 @@ import 'package:flutter/material.dart'; +import 'package:grpc/grpc_web.dart'; +import 'package:softplayer_web/api/grpc/accounts.dart'; class SignInForm extends StatefulWidget { - const SignInForm({super.key}); + const SignInForm({ + super.key, + required this.accountsGrpc, + }); + final AccountsGrpc accountsGrpc; @override State createState() => _SignInFormState(); } class _SignInFormState extends State { final _formKey = GlobalKey(); + final usernameCtrl = TextEditingController(); + final passwordCtrl = TextEditingController(); static const dialogName = "Sign In"; + void submitForm() { + // Validate returns true if the form is valid, or false otherwise. + if (_formKey.currentState!.validate()) { + final username = usernameCtrl.text; + final password = passwordCtrl.text; + widget.accountsGrpc + .signIn(username, "", password) + .then((value) => null) + .catchError((e) { + GrpcError error = e; + String msg; + if (error.message != null) { + msg = error.message!; + } else { + msg = error.toString(); + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(msg), + backgroundColor: Colors.red, + showCloseIcon: true, + behavior: SnackBarBehavior.floating, + )); + passwordCtrl.clear(); + }); + } + } + @override Widget build(BuildContext context) => AlertDialog( title: const Text(dialogName), content: SizedBox( - width: 420, - height: 140, - child: Form( - key: _formKey, - child: Center( - child: Column(children: [ - TextFormField( - autofocus: true, - decoration: const InputDecoration( - hintText: "Enter your username or email", - icon: Icon(Icons.account_circle), - label: Text("Username"), - ), - cursorWidth: 1, - cursorHeight: 18, - cursorRadius: const Radius.circular(10), - ), - TextFormField( - obscureText: true, - decoration: const InputDecoration( - hintText: "Enter your password", - icon: Icon(Icons.password), - label: Text("Password") - ), - cursorWidth: 1, - cursorHeight: 18, - cursorRadius: const Radius.circular(10), - ), - ])))), + width: 420, + height: 140, + child: Form( + key: _formKey, + child: Center( + child: Column(children: [ + TextFormField( + controller: usernameCtrl, + autofocus: true, + decoration: const InputDecoration( + hintText: "Enter your username or email", + icon: Icon(Icons.account_circle), + label: Text("Username"), + ), + cursorWidth: 1, + cursorHeight: 18, + cursorRadius: const Radius.circular(10), + ), + TextFormField( + controller: passwordCtrl, + obscureText: true, + decoration: const InputDecoration( + hintText: "Enter your password", + icon: Icon(Icons.password), + label: Text("Password")), + cursorWidth: 1, + cursorHeight: 18, + cursorRadius: const Radius.circular(10), + onFieldSubmitted: (v) { + if (usernameCtrl.text.isEmpty) { + FocusScope.of(context).nextFocus(); + } else { + submitForm(); + } + }, + ), + ])))), actions: [ TextButton( onPressed: () => Navigator.pop(context, 'Cancel'), child: const Text('Cancel'), ), TextButton( - onPressed: () { - // Validate returns true if the form is valid, or false otherwise. - if (_formKey.currentState!.validate()) { - // If the form is valid, display a snackbar. In the real world, - // you'd often call a server or save the information in a database. - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(_formKey.toString())), - ); - } - }, + onPressed: submitForm, child: const Text('OK'), ), ], diff --git a/lib/components/sign_up_form.dart b/lib/components/sign_up_form.dart index dc7b008..8772779 100644 --- a/lib/components/sign_up_form.dart +++ b/lib/components/sign_up_form.dart @@ -1,86 +1,120 @@ import 'package:flutter/material.dart'; +import 'package:grpc/grpc_web.dart'; +import 'package:softplayer_web/api/grpc/accounts.dart'; class SignUpForm extends StatefulWidget { - const SignUpForm({super.key}); + const SignUpForm({ + super.key, + required this.accountsGrpc, + }); + + final AccountsGrpc accountsGrpc; @override State createState() => _SignUpFormState(); } class _SignUpFormState extends State { final _formKey = GlobalKey(); + + final usernameCtrl = TextEditingController(); + final passwordCtrl = TextEditingController(); + final passwordVerifyCtrl = TextEditingController(); + final emailCtrl = TextEditingController(); + static const dialogName = "Sign Up"; + + void submitForm() { + // Validate returns true if the form is valid, or false otherwise. + if (_formKey.currentState!.validate()) { + final username = usernameCtrl.text; + final password = passwordCtrl.text; + final email = emailCtrl.text; + widget.accountsGrpc + .signUp(username, email, password) + .then((value) => null) + .catchError((e) { + GrpcError error = e; + String msg; + if (error.message != null) { + msg = error.message!; + } else { + msg = error.toString(); + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(msg), + backgroundColor: Colors.red, + showCloseIcon: true, + behavior: SnackBarBehavior.floating, + )); + passwordCtrl.clear(); + }); + } + } @override Widget build(BuildContext context) => AlertDialog( title: const Text(dialogName), content: SizedBox( - width: 420, - height: 280, - child: Form( - key: _formKey, - child: Center( - child: Column(children: [ - TextFormField( - autofocus: true, - decoration: const InputDecoration( - hintText: "Enter your username", - icon: Icon(Icons.account_circle), - label: Text("Username"), - ), - cursorWidth: 1, - cursorHeight: 18, - cursorRadius: const Radius.circular(10), - ), - TextFormField( - autofocus: true, - decoration: const InputDecoration( - hintText: "Enter your email", - icon: Icon(Icons.email), - label: Text("Email"), - ), - cursorWidth: 1, - cursorHeight: 18, - cursorRadius: const Radius.circular(10), - ), - TextFormField( - obscureText: true, - decoration: const InputDecoration( - hintText: "Enter your password", - icon: Icon(Icons.password), - label: Text("Password") - ), - cursorWidth: 1, - cursorHeight: 18, - cursorRadius: const Radius.circular(10), - ), - TextFormField( - obscureText: true, - decoration: const InputDecoration( - hintText: "Verify your password", - icon: Icon(Icons.password), - label: Text("Confirm Password") - ), - cursorWidth: 1, - cursorHeight: 18, - cursorRadius: const Radius.circular(10), - ), - ])))), + width: 420, + height: 280, + child: Form( + key: _formKey, + child: Center( + child: Column(children: [ + TextFormField( + autofocus: true, + controller: usernameCtrl, + decoration: const InputDecoration( + hintText: "Enter your username", + icon: Icon(Icons.account_circle), + label: Text("Username"), + ), + cursorWidth: 1, + cursorHeight: 18, + cursorRadius: const Radius.circular(10), + ), + TextFormField( + controller: emailCtrl, + autofocus: true, + decoration: const InputDecoration( + hintText: "Enter your email", + icon: Icon(Icons.email), + label: Text("Email"), + ), + cursorWidth: 1, + cursorHeight: 18, + cursorRadius: const Radius.circular(10), + ), + TextFormField( + controller: passwordCtrl, + obscureText: true, + decoration: const InputDecoration( + hintText: "Enter your password", + icon: Icon(Icons.password), + label: Text("Password")), + cursorWidth: 1, + cursorHeight: 18, + cursorRadius: const Radius.circular(10), + ), + TextFormField( + controller: passwordVerifyCtrl, + obscureText: true, + decoration: const InputDecoration( + hintText: "Verify your password", + icon: Icon(Icons.password), + label: Text("Confirm Password")), + cursorWidth: 1, + cursorHeight: 18, + cursorRadius: const Radius.circular(10), + ), + ])))), actions: [ TextButton( onPressed: () => Navigator.pop(context, 'Cancel'), child: const Text('Cancel'), ), TextButton( - onPressed: () { - // Validate returns true if the form is valid, or false otherwise. - if (_formKey.currentState!.validate()) { - // If the form is valid, display a snackbar. Up the real world, - // you'd often call a server or save the information in a database. - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(_formKey.toString())), - ); - } - }, + onPressed: submitForm, child: const Text('OK'), ), ], diff --git a/lib/main.dart b/lib/main.dart index 45eaaea..f778def 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:grpc/grpc_web.dart'; +import 'package:softplayer_web/api/grpc/accounts.dart'; import 'package:softplayer_web/components/menubar.dart'; import 'package:softplayer_web/helpers/page_wrapper.dart'; import 'package:softplayer_web/pages/about.dart'; @@ -9,7 +10,7 @@ import 'package:softplayer_web/pages/home.dart'; void main() async { const String backendURL = String.fromEnvironment( 'SOFTPLAYER_BACKEND_URL', - defaultValue: 'http://softplayer.badhouseplants.net:8080', + defaultValue: 'https://softplayer-backend.badhouseplants.net:8080', ); GrpcWebClientChannel grpcChannel = GrpcWebClientChannel.xhr(Uri.parse(backendURL)); @@ -18,28 +19,36 @@ void main() async { } class MyApp extends StatelessWidget { - const MyApp({super.key, required this.channel}); - - // A channel that should be used to fire grpc calls + MyApp({super.key, required this.channel}); final GrpcWebClientChannel channel; - + late final AccountsGrpc accountsGrpc = AccountsGrpc(channel: channel); @override Widget build(BuildContext context) { + accountsGrpc.init(); return MaterialApp( debugShowCheckedModeBanner: false, title: 'Softplayer', routes: { - '/': (context) => const PageWrapper( - appBar: MenuPanel(tab: TabName.home), - child: HomePage(), + '/': (context) => PageWrapper( + appBar: MenuPanel( + tab: TabName.home, + accountsGrpc: accountsGrpc, + ), + child: const HomePage(), ), - '/catalog': (context) => const PageWrapper( - appBar: MenuPanel(tab: TabName.catalog), - child: CatalogPage(), + '/catalog': (context) => PageWrapper( + appBar: MenuPanel( + tab: TabName.catalog, + accountsGrpc: accountsGrpc, + ), + child: const CatalogPage(), ), - '/about': (context) => const PageWrapper( - appBar: MenuPanel(tab: TabName.about), - child: AboutPage(), + '/about': (context) => PageWrapper( + appBar: MenuPanel( + tab: TabName.about, + accountsGrpc: accountsGrpc, + ), + child: const AboutPage(), ) }, theme: ThemeData(