diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/main.dart | 156 | ||||
-rw-r--r-- | lib/models/instance.dart | 27 | ||||
-rw-r--r-- | lib/models/login.dart | 66 | ||||
-rw-r--r-- | lib/models/user.dart | 61 | ||||
-rw-r--r-- | lib/screens/home.dart | 26 | ||||
-rw-r--r-- | lib/screens/login.dart | 200 | ||||
-rw-r--r-- | lib/screens/signup.dart | 140 | ||||
-rw-r--r-- | lib/widgets/input_box.dart | 36 | ||||
-rw-r--r-- | lib/widgets/snackbar.dart | 19 |
9 files changed, 731 insertions, 0 deletions
diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..09d16d7 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,156 @@ +import "package:flutter/material.dart"; +import "package:flutter_secure_storage/flutter_secure_storage.dart"; +//import "package:window_manager/window_manager.dart"; + +import "package:openbills/screens/login.dart"; +import "package:openbills/widgets/snackbar.dart"; +import "package:openbills/models/instance.dart"; + +void main() async { + //WidgetsFlutterBinding.ensureInitialized(); + //await windowManager.ensureInitialized(); + + //WindowOptions windowOptions = const WindowOptions( + // minimumSize: Size(400, 600), + // center: true, + // backgroundColor: Colors.transparent, + // skipTaskbar: false, + // titleBarStyle: TitleBarStyle.normal, + //); + + //windowManager.waitUntilReadyToShow(windowOptions, () async { + // await windowManager.show(); + // await windowManager.focus(); + //}); + + runApp(MaterialApp( + title: "OpenBills", + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + //themeMode: ThemeMode.system, + themeMode: ThemeMode.dark, + //home: const HomeScreen(), + home: const MyApp(), + )); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State<MyApp> createState() => _MyAppState(); +} + +class _MyAppState extends State<MyApp> { + final _storage = const FlutterSecureStorage(); + + String _authToken = ""; + String _refreshToken = ""; + String _version = ""; + + @override + void initState() { + super.initState(); + + _readAll(); + } + + Future<void> _readAll() async { + final authToken = await _storage.read(key: "auth_token", aOptions: const AndroidOptions( + encryptedSharedPreferences: true, + )); + final refreshToken = await _storage.read(key: "refresh_token", aOptions: const AndroidOptions( + encryptedSharedPreferences: true, + )); + final Instance instance = await getInstance("https://openbills.vidhukant.com"); + + setState(() { + if (authToken != null) { + _authToken = authToken.toString(); + } + + if (refreshToken != null) { + _refreshToken = refreshToken.toString(); + } + + _version = instance.version; + }); + } + + @override + Widget build(BuildContext context) { + if (_authToken == "") { + return const LoginScreen(); + } else { + return Scaffold( + appBar: AppBar( + title: const Text("OpenBills Home Screen"), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Column( + children: <Widget>[ + Text( + "You are logged into OpenBills $_version!", + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + ), + ), + + const SizedBox( + height: 50, + ), + + Text( + "Auth Bearer Token: $_authToken", + ), + + const SizedBox( + height: 25, + ), + + Text( + "Refresh Token: $_refreshToken", + ), + + const SizedBox( + height: 50, + ), + + const Text( + "Well yeah this doesn't do much but yay this is pretty cool", + ), + + const SizedBox( + height: 50, + ), + + Row ( + mainAxisAlignment: MainAxisAlignment.end, + children: <Widget>[ + TextButton( + child: const Text("Log Out"), + onPressed: () async { + await _storage.delete(key: "auth_token"); + await _storage.delete(key: "refresh_token"); + + if (context.mounted) { + MySnackBar.show(context, "You have been logged out."); + } + + setState(() { + _authToken = ""; + }); + }, + ), + ], + ) + ], + ), + ) + ); + } + } +} diff --git a/lib/models/instance.dart b/lib/models/instance.dart new file mode 100644 index 0000000..b01b052 --- /dev/null +++ b/lib/models/instance.dart @@ -0,0 +1,27 @@ +import "package:http/http.dart" as http; +import "dart:convert"; + +class Instance { + // TODO: add other fields + final String version; + + Instance({ + required this.version, + }); + + factory Instance.fromJson(Map<String, dynamic> json) { + return Instance( + version: json["Version"], + ); + } +} + +Future<Instance> getInstance(String instanceURL) async { + final res = await http.get(Uri.parse("$instanceURL/info")); + + final json = jsonDecode(res.body); + + // TODO: handle errors + + return Instance.fromJson(json); +}
\ No newline at end of file diff --git a/lib/models/login.dart b/lib/models/login.dart new file mode 100644 index 0000000..6412921 --- /dev/null +++ b/lib/models/login.dart @@ -0,0 +1,66 @@ +import "package:http/http.dart" as http; +import "dart:convert"; + +import "package:openbills/models/user.dart"; + +enum LoginMethod { email, username } + +extension LoginMethodString on LoginMethod { + String get value { + switch(this) { + case LoginMethod.username: + return "username"; + case LoginMethod.email: + return "email"; + } + } +} + +class LoginRes { + final User user; + final String authToken; + final String refreshToken; + + LoginRes({ + required this.user, + required this.authToken, + required this.refreshToken, + }); + + factory LoginRes.fromJson(Map<String, dynamic> json) { + return LoginRes( + user: User.fromJson(json["data"]), + authToken: json["auth_token"], + refreshToken: json["refresh_token"], + ); + } +} + +Future<LoginRes> userSignIn(String accountName, method, password) async { + final res = await http.post( + Uri.parse("https://openbills.vidhukant.com/api/auth/signin"), + headers: <String, String> { + "Content-Type": "application/json; charset=UTF-8", + }, + body: jsonEncode(<String, String> { + "AccountName": accountName, + "Method": method, + "Password": password, + }) + ); + + final json = jsonDecode(res.body); + + if (res.statusCode != 200) { + switch(res.statusCode) { + case 404: + throw "This user does not exist"; + case 500: + throw "Internal Server Error"; + default: + throw json["error"]; + } + } + + return LoginRes.fromJson(json); +} diff --git a/lib/models/user.dart b/lib/models/user.dart new file mode 100644 index 0000000..07c2c09 --- /dev/null +++ b/lib/models/user.dart @@ -0,0 +1,61 @@ +import 'package:http/http.dart' as http; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import "dart:convert"; + +class User { + final int id; + String? createdAt; + String? updatedAt; + String? username; + String? email; + bool? isVerified; + + User({ + required this.id, + this.createdAt, + this.updatedAt, + this.username, + this.email, + this.isVerified + }); + + factory User.fromJson(Map<String, dynamic> json) { + return User( + id: json["ID"], + createdAt: json["CreatedAt"], + updatedAt: json["UpdatedAt"], + username: json["Username"], + email: json["Email"], + isVerified: json["IsVerified"] + ); + } +} + +Future<User> userSignUp(User newUser, String password) async { + final res = await http.post( + Uri.parse("https://openbills.vidhukant.com/api/auth/signup"), + headers: <String, String> { + "Content-Type": "application/json; charset=UTF-8", + }, + body: jsonEncode(<String, String> { + "Username": newUser.username as String, + "Password": password, + "Email": newUser.email as String, + }), + ); + + final json = jsonDecode(res.body); + + if (res.statusCode != 200) { + switch(res.statusCode) { + case 404: + throw "This user does not exist"; + case 500: + throw "Internal Server Error"; + default: + throw json["error"]; + } + } + + return User.fromJson(json["data"]); +} diff --git a/lib/screens/home.dart b/lib/screens/home.dart new file mode 100644 index 0000000..474fc48 --- /dev/null +++ b/lib/screens/home.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import "login.dart"; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Home Page") + ), + body: Center( + child: ElevatedButton( + child: const Text("Log In"), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const LoginScreen()) + ); + }, + ), + ), + ); + } +} diff --git a/lib/screens/login.dart b/lib/screens/login.dart new file mode 100644 index 0000000..1a0db55 --- /dev/null +++ b/lib/screens/login.dart @@ -0,0 +1,200 @@ +import "package:flutter/material.dart"; +import "package:flutter_secure_storage/flutter_secure_storage.dart"; + +import "package:openbills/models/login.dart"; +import "package:openbills/widgets/input_box.dart"; +import "package:openbills/widgets/snackbar.dart"; +import "package:openbills/screens/signup.dart"; + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: const Image( + height: 50, + image: AssetImage("assets/images/logo.png"), + ), + ), + body: const Center( + child: SizedBox( + width: 500, + child: Body(), + ) + ), + ); + } +} + +class Body extends StatefulWidget { + const Body({super.key}); + + @override + State<Body> createState() => _BodyState(); +} + +class _BodyState extends State<Body> { + final accountNameController = TextEditingController(); + final passwordController = TextEditingController(); + LoginMethod? _method = LoginMethod.username; + + final _storage = const FlutterSecureStorage(); + + @override + void dispose() { + accountNameController.dispose(); + passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.sizeOf(context); + MainAxisAlignment alignment = MainAxisAlignment.center; + + if (size.width < 700) { + alignment = MainAxisAlignment.spaceBetween; + } + + if (size.height < 700) { + alignment = MainAxisAlignment.center; + } + + return Column ( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: alignment, + children: <Widget> [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Text( + "OpenBills Log In", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 30, + ), + ), + ), + + Column( + children: <Widget>[ + FormInputBox( + controller: accountNameController, + hintText: _method == LoginMethod.email ? "E-Mail" : "Username", + ), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Column( + children: <Widget>[ + const Text( + "Log in with:", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 18, + ), + ), + + Row( + children: <Widget>[ + Flexible( + child: RadioListTile<LoginMethod>( + title: const Text("username"), + value: LoginMethod.username, + groupValue: _method, + onChanged: (LoginMethod? value) { + setState(() { + _method = value; + }); + }, + ), + ), + + Flexible( + child: RadioListTile<LoginMethod>( + title: const Text("email"), + value: LoginMethod.email, + groupValue: _method, + onChanged: (LoginMethod? value) { + setState(() { + _method = value; + }); + }, + ), + ), + ], + ), + ], + ) + ), + + FormInputBox( + controller: passwordController, + hintText: "Password", + obscureText: true, + ), + ], + ), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget> [ + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SignupScreen()) + ); + }, + child: const Text("Don't have an account? Sign up.") + ), + + ElevatedButton( + onPressed: () async { + String name = accountNameController.text; + String? method = _method?.value; + String password = passwordController.text; + + try { + LoginRes res = await userSignIn(name, method, password); + String? username = res.user.username; + + await _storage.write( + key: "auth_token", + value: res.authToken, + aOptions: const AndroidOptions( + encryptedSharedPreferences: true, + ), + ); + + await _storage.write( + key: "refresh_token", + value: res.refreshToken, + aOptions: const AndroidOptions( + encryptedSharedPreferences: true, + ), + ); + + if (context.mounted) { + MySnackBar.show(context, "Hello, $username! Please restart the app to see the homepage."); + } + } on String catch(e) { + if (context.mounted) { + MySnackBar.show(context, "Error: $e"); + } + } + }, + child: const Text("Log In") + ), + ], + ), + ), + ], + ); + } +}
\ No newline at end of file diff --git a/lib/screens/signup.dart b/lib/screens/signup.dart new file mode 100644 index 0000000..b155d8e --- /dev/null +++ b/lib/screens/signup.dart @@ -0,0 +1,140 @@ +import "package:flutter/material.dart"; + +import "package:openbills/models/user.dart"; +import "package:openbills/widgets/input_box.dart"; +import "package:openbills/widgets/snackbar.dart"; + +class SignupScreen extends StatelessWidget { + const SignupScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: const Image( + height: 50, + image: AssetImage("assets/images/logo.png"), + ), + ), + body: const Center( + child: SizedBox( + width: 500, + child: Body(), + ) + ), + ); + } +} + +class Body extends StatefulWidget { + const Body({super.key}); + + @override + State<Body> createState() => _BodyState(); +} + +class _BodyState extends State<Body> { + final usernameController = TextEditingController(); + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + + @override + void dispose() { + usernameController.dispose(); + emailController.dispose(); + passwordController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.sizeOf(context); + MainAxisAlignment alignment = MainAxisAlignment.center; + + if (size.width < 700) { + alignment = MainAxisAlignment.spaceBetween; + } + + if (size.height < 700) { + alignment = MainAxisAlignment.center; + } + + return Column ( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: alignment, + children: <Widget> [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Text( + "OpenBills Sign Up", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 30, + ), + ), + ), + + Column( + children: <Widget>[ + FormInputBox( + controller: usernameController, + hintText: "Username", + ), + + FormInputBox( + controller: emailController, + hintText: "E-mail", + ), + + FormInputBox( + controller: passwordController, + hintText: "Password", + obscureText: true, + ), + ], + ), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget> [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("Already have an account? Sign in.") + ), + + ElevatedButton( + onPressed: () async { + User u = User( + id: 0, + username: usernameController.text, + email: emailController.text, + ); + + try { + await userSignUp(u, passwordController.text); + + if (context.mounted) { + MySnackBar.show(context, "Successfully created an account!"); + Navigator.pop(context); + } + } on String catch (e) { + if (context.mounted) { + MySnackBar.show(context, "Error: $e"); + } + } + }, + child: const Text("Sign Up") + ), + ], + ), + ), + ], + ); + } +}
\ No newline at end of file diff --git a/lib/widgets/input_box.dart b/lib/widgets/input_box.dart new file mode 100644 index 0000000..c035635 --- /dev/null +++ b/lib/widgets/input_box.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class FormInputBox extends StatefulWidget { + const FormInputBox({ + super.key, + //this.textInputType, + required this.controller, + this.hintText = "", + this.obscureText = false, + }); + + //final TextInputType textInputType; + final String hintText; + final TextEditingController controller; + final bool obscureText; + + @override + FormInputBoxState createState() => FormInputBoxState(); +} + +class FormInputBoxState extends State<FormInputBox> { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + child: TextFormField( + obscureText: widget.obscureText, + controller: widget.controller, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: widget.hintText, + ), + ), + ); + } +}
\ No newline at end of file diff --git a/lib/widgets/snackbar.dart b/lib/widgets/snackbar.dart new file mode 100644 index 0000000..8da41a1 --- /dev/null +++ b/lib/widgets/snackbar.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class MySnackBar { + final String message; + + const MySnackBar({ + required this.message, + }); + + static show(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + behavior: SnackBarBehavior.floating, + width: 400.0, + ), + ); + } +}
\ No newline at end of file |