diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eae5f94..1dd97e7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ - + @@ -34,5 +35,5 @@ - + diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..e51c0a0 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,60 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + // ignore: missing_enum_constant_in_switch + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAmUEAR1SB7bf29XoGNpOExo_GWoNl68J8', + appId: '1:903921644523:android:8b1be795cfa3b71a079fae', + messagingSenderId: '903921644523', + projectId: 'leg-barkr', + storageBucket: 'leg-barkr.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyA6haaqDGLGt5gYLc_z1r0P-jWl0bF3Xlc', + appId: '1:903921644523:ios:9294b2d611ff71fc079fae', + messagingSenderId: '903921644523', + projectId: 'leg-barkr', + storageBucket: 'leg-barkr.appspot.com', + iosClientId: '903921644523-cq15mvp7kj64spro7ugkh781kvp4bm42.apps.googleusercontent.com', + iosBundleId: 'com.legbarkr.app', + ); +} diff --git a/lib/home.dart b/lib/home.dart index 495a51d..7599e0c 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import 'package:leg_barkr_app/view/metrics/metrics_page.dart'; import 'package:leg_barkr_app/view/steps/steps_page.dart'; import 'package:leg_barkr_app/view/map_page.dart'; @@ -15,6 +16,16 @@ class _HomeScreenState extends State { int _page = 0; PageController _pageController = PageController(); + _HomeScreenState(){ + FirebaseAuth.instance + .authStateChanges() + .listen((User? user) { + if (user == null) { + Navigator.pushNamed(context, "/login"); + } + }); + } + void onBottomBarPressed(int page) { setState(() { _page = page; diff --git a/lib/main.dart b/lib/main.dart index 6a0194c..e872c27 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,18 @@ +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:leg_barkr_app/home.dart'; import 'package:flutter/services.dart'; +import 'package:leg_barkr_app/view/auth/login_form.dart'; +import 'package:leg_barkr_app/view/auth/register_form.dart'; +import 'firebase_options.dart'; -void main() { +void main() async { SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: Colors.black12)); - runApp(const Main()); + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + runApp(Main()); } class Main extends StatelessWidget { @@ -14,7 +22,13 @@ class Main extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( theme: ThemeData.light(), - home: HomeScreen() + //home: HomeScreen(), + initialRoute: '/', + routes: { + '/': (context) => const HomeScreen(), + '/login': (context) => const LoginForm(), + '/register': (context) => const RegisterForm() + } ); } } diff --git a/lib/utils/endpoints.dart b/lib/utils/endpoints.dart new file mode 100644 index 0000000..bb342ec --- /dev/null +++ b/lib/utils/endpoints.dart @@ -0,0 +1,3 @@ +const String home = "https://leg-barkr.nw.r.appspot.com/"; +const String register = "https://leg-barkr.nw.r.appspot.com/authentication/register"; +const String verify = "https://leg-barkr.nw.r.appspot.com/authentication/verify"; diff --git a/lib/utils/resources.dart b/lib/utils/resources.dart new file mode 100644 index 0000000..4b038a8 --- /dev/null +++ b/lib/utils/resources.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +class Resources { + static final Color primaryColour = Colors.green; + static final Color primaryAccentColour = Colors.greenAccent; + +} \ No newline at end of file diff --git a/lib/view/auth/login_form.dart b/lib/view/auth/login_form.dart new file mode 100644 index 0000000..baa7e9a --- /dev/null +++ b/lib/view/auth/login_form.dart @@ -0,0 +1,150 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; + + +class LoginForm extends StatefulWidget { + const LoginForm({Key? key}) : super(key: key); + + @override + _LoginFormState createState() => _LoginFormState(); +} + +class _LoginFormState extends State { + final GlobalKey _formKey = GlobalKey(); + late String _email, _password; + bool _loggingIn = false; + + void attemptLogin(){ + final form = _formKey.currentState; + if (form!.validate()) { + form.save(); + setState(() { _loggingIn = true; }); + loginUser(); + } else { + setState(() { _loggingIn = false; }); + } + } + + Future loginUser() async { + try { + await FirebaseAuth.instance.signInWithEmailAndPassword(email: _email, password: _password); + Navigator.pushNamed(context, "/"); + } on FirebaseAuthException catch (e) { + if (e.code == 'user-not-found') { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Invalid email"))); + setState(() { _loggingIn = false; }); + } else if (e.code == 'wrong-password') { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Incorrect password"))); + setState(() { _loggingIn = false; }); + } + } + } + + @override + Widget build(BuildContext context) { + final TextFormField emailInput = TextFormField( + autofocus: false, + cursorColor: Colors.green, + decoration: const InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green, + width: 2 + ) + ), + hintText: 'Email', + //labelStyle: TextStyle(color: Colors.green), + ), + validator: (value) => value!.isEmpty ? "Please enter email" : null, + onSaved: (value) => _email = value!, + ); + + final TextFormField passwordInput = TextFormField( + autofocus: false, + obscureText: true, + cursorColor: Colors.green, + decoration: const InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green, + width: 2 + ) + ), + hintText: 'Password', + //labelStyle: TextStyle(color: Colors.green), + ), + validator: (value) => value!.isEmpty ? "Please enter password" : null, + onSaved: (value) => _password = value!, + ); + + final Container loading = Container( + alignment: Alignment.center, + child: CircularProgressIndicator( + backgroundColor: Colors.green, + color: Colors.green, + ), + ); + + final Container loginBtn = Container( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: attemptLogin, + child: Text('Sign In'), + style: ElevatedButton.styleFrom( + alignment: Alignment.center, + primary: Colors.green, + ) + ) + ); + + final Container registerBtn = Container( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: (){ Navigator.pushNamed(context, '/register'); }, + child: Text('No account? Register now!', + style: TextStyle( + color: Colors.black, + fontSize: 15 + ) + ), + style: ElevatedButton.styleFrom( + alignment: Alignment.center, + primary: Colors.white, + side: BorderSide( + color: Colors.green, + width: 2 + ) + ) + ) + ); + + return Scaffold( + appBar: null, + body: Center( + child: SingleChildScrollView( + child: Form( + key: _formKey, + child:Container( + padding: EdgeInsets.all(10), + child: Column( + children: [ + emailInput, + SizedBox(height: 20), + passwordInput, + SizedBox(height: 20), + _loggingIn ? loading : loginBtn, + SizedBox(height: 20), + _loggingIn ? Text("") : registerBtn + ] + ) + ) + ), + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/view/auth/register_form.dart b/lib/view/auth/register_form.dart new file mode 100644 index 0000000..be2f311 --- /dev/null +++ b/lib/view/auth/register_form.dart @@ -0,0 +1,239 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:leg_barkr_app/home.dart'; +import 'package:leg_barkr_app/utils/endpoints.dart' as Endpoints; +import 'package:leg_barkr_app/view/auth/login_form.dart'; + +class RegisterForm extends StatefulWidget { + const RegisterForm({Key? key}) : super(key: key); + + @override + _RegisterFormState createState() => _RegisterFormState(); +} + +class _RegisterFormState extends State { + final GlobalKey _formKey = GlobalKey(); + late String _firstName, _lastName, _email, _password, _confirmPassword, _deviceId; + bool _registering = false; + + void attemptRegistration(){ + final form = _formKey.currentState; + if (form!.validate()) { + form.save(); + if (_password != _confirmPassword) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Passwords do not match"))); + setState(() { _registering = false; }); + } + form.save(); + setState(() { _registering = true; }); + registerUser(); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Please enter all required fields"))); + setState(() { _registering = false; }); + } + } + + void registerUser() async { + final response = await http.post( + Uri.parse(Endpoints.register), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: jsonEncode({ + 'name': _firstName + _lastName, + 'deviceid': _deviceId, + 'email': _email, + 'password': _password + }), + ); + if (response.statusCode == 201){ + Navigator.push(context, MaterialPageRoute(builder: (context) => LoginForm())); + } else if (response.statusCode == 400){ + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Fields missing!"))); + } else if (response.statusCode == 409){ + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("User with given email already exists"))); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Failed registration, please try again later"))); + } + setState(() { + _registering = false; + }); + } + + @override + Widget build(BuildContext context) { + final TextFormField firstNameInput = TextFormField( + autofocus: false, + cursorColor: Colors.green, + decoration: const InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green, + width: 2 + ) + ), + hintText: 'First Name', + //labelStyle: TextStyle(color: Colors.green), + ), + validator: (value) => value!.isEmpty ? "Please enter first name" : null, + onSaved: (value) => _firstName = value!, + ); + + final TextFormField lastNameInput = TextFormField( + autofocus: false, + cursorColor: Colors.green, + decoration: const InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green, + width: 2 + ) + ), + hintText: 'Last Name', + //labelStyle: TextStyle(color: Colors.green), + ), + validator: (value) => value!.isEmpty ? "Please enter last name" : null, + onSaved: (value) => _lastName = value!, + ); + + final TextFormField deviceInput = TextFormField( + autofocus: false, + cursorColor: Colors.green, + decoration: const InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green, + width: 2 + ) + ), + hintText: 'Device ID', + //labelStyle: TextStyle(color: Colors.green), + ), + validator: (value) => value!.isEmpty ? "Please enter device ID" : null, + onSaved: (value) => _deviceId = value!, + ); + + final TextFormField emailInput = TextFormField( + autofocus: false, + cursorColor: Colors.green, + decoration: const InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green, + width: 2 + ) + ), + hintText: 'Email', + //labelStyle: TextStyle(color: Colors.green), + ), + validator: (value) => value!.isEmpty ? "Please enter email" : null, + onSaved: (value) => _email = value!, + ); + + final TextFormField passwordInput = TextFormField( + autofocus: false, + obscureText: true, + cursorColor: Colors.green, + decoration: const InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green, + width: 2 + ) + ), + hintText: 'Password', + //labelStyle: TextStyle(color: Colors.green), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter password'; + } + return null; + }, + onSaved: (value) => _password = value!, + ); + + final TextFormField confirmPasswordInput = TextFormField( + autofocus: false, + obscureText: true, + cursorColor: Colors.green, + decoration: const InputDecoration( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green, + width: 2 + ) + ), + hintText: 'Confirm password', + //labelStyle: TextStyle(color: Colors.green), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please confirm password'; + } + return null; + }, + onSaved: (value) => _confirmPassword = value!, + ); + + final Container loading = Container( + alignment: Alignment.center, + child: CircularProgressIndicator( + backgroundColor: Colors.green, + color: Colors.green, + ), + ); + + final Container registerBtn = Container( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: attemptRegistration, + child: Text('Register'), + style: ElevatedButton.styleFrom( + alignment: Alignment.center, + primary: Colors.green, + ) + ) + + ); + + return Scaffold( + appBar: null, + body: SingleChildScrollView( + child: Form( + key: _formKey, + child:Container( + padding: EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 100), + firstNameInput, + SizedBox(height: 20), + lastNameInput, + SizedBox(height: 20), + deviceInput, + SizedBox(height: 20), + emailInput, + SizedBox(height: 20), + passwordInput, + SizedBox(height: 20), + confirmPasswordInput, + SizedBox(height: 20), + _registering ? loading : registerBtn + ] + ) + ) + ), + ) + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 9990d18..b95cc4d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,6 +78,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.7" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.11" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.7" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.12.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.4" flutter: dependency: "direct main" description: flutter @@ -142,6 +184,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.15.0" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" intl: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c1ad566..6e6afc0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,9 @@ dependencies: google_maps_flutter_web: ^0.3.2+1 charts_flutter: ^0.12.0 syncfusion_flutter_gauges: ^19.4.50 + http: ^0.13.4 + firebase_core: ^1.12.0 + firebase_auth: ^3.3.7 dev_dependencies: