aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/main.dart156
-rw-r--r--lib/models/instance.dart27
-rw-r--r--lib/models/login.dart66
-rw-r--r--lib/models/user.dart61
-rw-r--r--lib/screens/home.dart26
-rw-r--r--lib/screens/login.dart200
-rw-r--r--lib/screens/signup.dart140
-rw-r--r--lib/widgets/input_box.dart36
-rw-r--r--lib/widgets/snackbar.dart19
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