diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..dd4a91f
Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..c654428
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index db77bb4..ca28e8c 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..f909877
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index 17987b7..b94341e 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..ffcc6ee
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 09d4391..b7603ff 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..4476cc4
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index d5f1c8d..98805e7 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..6f73ff2
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 4d6372e..f9321a2 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..bd0fdb2
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..1e31290
--- /dev/null
+++ b/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FFA188A6
+
\ No newline at end of file
diff --git a/assets/app_icon.png b/assets/app_icon.png
new file mode 100644
index 0000000..74b20ad
Binary files /dev/null and b/assets/app_icon.png differ
diff --git a/assets/app_icon1.png b/assets/app_icon1.png
new file mode 100644
index 0000000..6bc06a5
Binary files /dev/null and b/assets/app_icon1.png differ
diff --git a/assets/app_icon_round.png b/assets/app_icon_round.png
new file mode 100644
index 0000000..b6ab00d
Binary files /dev/null and b/assets/app_icon_round.png differ
diff --git a/assets/splash_icon.png b/assets/splash_icon.png
new file mode 100644
index 0000000..5bead22
Binary files /dev/null and b/assets/splash_icon.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index d36b1fa..eabd851 100644
--- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,122 +1,122 @@
{
- "images" : [
+ "images": [
{
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
+ "filename": "Icon-App-20x20@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "20x20"
},
{
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
+ "filename": "Icon-App-20x20@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "20x20"
},
{
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
+ "filename": "Icon-App-29x29@1x.png",
+ "idiom": "iphone",
+ "scale": "1x",
+ "size": "29x29"
},
{
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
+ "filename": "Icon-App-29x29@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "29x29"
},
{
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
+ "filename": "Icon-App-29x29@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "29x29"
},
{
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
+ "filename": "Icon-App-40x40@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "40x40"
},
{
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
+ "filename": "Icon-App-40x40@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "40x40"
},
{
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
+ "filename": "Icon-App-60x60@2x.png",
+ "idiom": "iphone",
+ "scale": "2x",
+ "size": "60x60"
},
{
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
+ "filename": "Icon-App-60x60@3x.png",
+ "idiom": "iphone",
+ "scale": "3x",
+ "size": "60x60"
},
{
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
+ "filename": "Icon-App-20x20@1x.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "20x20"
},
{
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
+ "filename": "Icon-App-20x20@2x.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "20x20"
},
{
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
+ "filename": "Icon-App-29x29@1x.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "29x29"
},
{
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
+ "filename": "Icon-App-29x29@2x.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "29x29"
},
{
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
+ "filename": "Icon-App-40x40@1x.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "40x40"
},
{
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
+ "filename": "Icon-App-40x40@2x.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "40x40"
},
{
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
+ "filename": "Icon-App-76x76@1x.png",
+ "idiom": "ipad",
+ "scale": "1x",
+ "size": "76x76"
},
{
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
+ "filename": "Icon-App-76x76@2x.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "76x76"
},
{
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
+ "filename": "Icon-App-83.5x83.5@2x.png",
+ "idiom": "ipad",
+ "scale": "2x",
+ "size": "83.5x83.5"
},
{
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
+ "filename": "Icon-App-1024x1024@1x.png",
+ "idiom": "ios-marketing",
+ "scale": "1x",
+ "size": "1024x1024"
}
],
- "info" : {
- "version" : 1,
- "author" : "xcode"
+ "info": {
+ "author": "icons_launcher",
+ "version": 1
}
-}
+}
\ No newline at end of file
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index dc9ada4..d275a09 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 7353c41..c2b159c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index 797d452..8193ea0 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 6ed2d93..b19fcea 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 4cd7b00..27da476 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index fe73094..732620d 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 321773c..399aeab 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index 797d452..8193ea0 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 502f463..e99c025 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 0ec3034..815298c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 0ec3034..815298c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index e9f5fea..d242a73 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 84ac32a..3a5c12c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index 8953cba..2a0280d 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 0467bf1..1d94fda 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/lib/features/account/account_create_screen.dart b/lib/features/account/account_create_screen.dart
new file mode 100644
index 0000000..a61f7e9
--- /dev/null
+++ b/lib/features/account/account_create_screen.dart
@@ -0,0 +1,115 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:helpers/helpers/misc_build/build_media.dart';
+import 'package:rluv/features/account/signup.dart';
+import 'package:rluv/global/styles.dart';
+
+import '../../global/api.dart';
+import '../../global/widgets/ui_button.dart';
+import '../../main.dart';
+import 'login.dart';
+
+class AccountCreateScreen extends ConsumerStatefulWidget {
+ const AccountCreateScreen({super.key});
+
+ @override
+ ConsumerState createState() =>
+ _AccountCreateScreenState();
+}
+
+enum _AccountScreen { options, login, signup }
+
+class _AccountCreateScreenState extends ConsumerState {
+ _AccountScreen currentScreen = _AccountScreen.options;
+ static final signupFormKey = GlobalKey();
+ static final loginFormKey = GlobalKey();
+
+ bool usingUsername = true;
+ bool hasFamilyCode = false;
+
+ @override
+ Widget build(BuildContext context) {
+ if (ref.watch(tokenProvider) != null) {
+ WidgetsBinding.instance.addPostFrameCallback(
+ (_) {
+ Navigator.pushAndRemoveUntil(
+ context,
+ MaterialPageRoute(builder: (ctx) => const Home()),
+ (r) => false,
+ );
+ },
+ );
+ }
+ final screen = BuildMedia(context).size;
+ return Scaffold(
+ backgroundColor: Styles.purpleNurple,
+ body: SafeArea(
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ child: currentScreen == _AccountScreen.options
+ ? Center(
+ child: SizedBox(
+ width: screen.width * 0.5 > 400 ? 400 : screen.width * 0.5,
+ child: Column(
+ children: [
+ const Spacer(),
+ const Text(
+ 'Welcome!',
+ style: TextStyle(fontSize: 28),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 40.0),
+ child: Image.asset("assets/app_icon.png",
+ height:
+ screen.width > 500 ? 250 : screen.width * 0.5,
+ width: screen.width > 500
+ ? 250
+ : screen.width * 0.5),
+ ),
+ UiButton(
+ color: Styles.sunflower,
+ onPressed: () {
+ setState(
+ () => currentScreen = _AccountScreen.signup,
+ );
+ },
+ text: 'Signup',
+ ),
+ const SizedBox(height: 20),
+ UiButton(
+ color: Styles.flounderBlue,
+ onPressed: () {
+ setState(
+ () => currentScreen = _AccountScreen.login,
+ );
+ },
+ text: 'Login',
+ ),
+ const Spacer(),
+ ],
+ ),
+ ),
+ )
+ : currentScreen == _AccountScreen.login
+ ? Login(formKey: loginFormKey, exitNav: exitNav())
+ : Signup(
+ formKey: signupFormKey,
+ exitNav: exitNav(),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget exitNav() => Align(
+ alignment: Alignment.topLeft,
+ child: IconButton(
+ icon: const Icon(Icons.chevron_left),
+ onPressed: () {
+ setState(
+ () => currentScreen = _AccountScreen.options,
+ );
+ },
+ ),
+ );
+}
diff --git a/lib/features/account/login.dart b/lib/features/account/login.dart
new file mode 100644
index 0000000..bc834b2
--- /dev/null
+++ b/lib/features/account/login.dart
@@ -0,0 +1,232 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:helpers/helpers/misc_build/build_media.dart';
+import 'package:helpers/helpers/print.dart';
+
+import '../../global/api.dart';
+import '../../global/styles.dart';
+import '../../global/utils.dart';
+import '../../global/widgets/ui_button.dart';
+
+class Login extends ConsumerStatefulWidget {
+ const Login({super.key, required this.formKey, required this.exitNav});
+
+ final GlobalKey formKey;
+ final Widget exitNav;
+
+ @override
+ ConsumerState createState() => _LoginState();
+}
+
+class _LoginState extends ConsumerState {
+ bool usingUsername = false;
+ final emailController = TextEditingController();
+ final usernameController = TextEditingController();
+ final passwordController = TextEditingController();
+
+ final focusNodes = [
+ FocusNode(),
+ FocusNode(),
+ FocusNode(),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ final screen = BuildMedia(context).size;
+ return Form(
+ key: widget.formKey,
+ child: Stack(
+ children: [
+ Column(
+ children: [
+ const Spacer(),
+ const Text(
+ 'Login',
+ style: TextStyle(fontSize: 24),
+ ),
+ const Spacer(flex: 2),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Column(
+ children: [
+ const Padding(
+ padding: EdgeInsets.all(2.0),
+ child: Text(
+ 'Username',
+ style: TextStyle(
+ fontSize: 16,
+ ),
+ ),
+ ),
+ Checkbox(
+ activeColor: Styles.lavender,
+ onChanged: (bool? value) {
+ setState(() => usingUsername = true);
+ },
+ value: usingUsername,
+ ),
+ ],
+ ),
+ const SizedBox(
+ width: 35,
+ ),
+ Column(
+ children: [
+ const Padding(
+ padding: EdgeInsets.all(2.0),
+ child: Text(
+ 'Email',
+ style: TextStyle(
+ fontSize: 16,
+ ),
+ ),
+ ),
+ Checkbox(
+ activeColor: Styles.lavender,
+ onChanged: (bool? value) {
+ setState(() => usingUsername = false);
+ },
+ value: !usingUsername,
+ ),
+ ],
+ ),
+ ],
+ ),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ child: !usingUsername
+ ? generateTextField(
+ controller: emailController,
+ size: screen,
+ text: 'Email: ',
+ validatorFunc: (s) {
+ if (s != null && s.isNotEmpty && !isEmailValid(s)) {
+ return 'Email entered is invalid';
+ }
+ return null;
+ },
+ index: 0)
+ : generateTextField(
+ controller: usernameController,
+ size: screen,
+ text: 'Username: ',
+ validatorFunc: (s) {
+ if (s == null || s.isEmpty) {
+ return 'Invalid Username';
+ }
+ if (s.length < 3) {
+ return 'Username Not 3 Letters Long';
+ }
+ if (s.length > 20) {
+ return 'Username Too Long';
+ }
+ if (!isUsernameValid(s)) {
+ return 'Letters, Numbers, and ., -, _ Allowed';
+ }
+ return null;
+ },
+ index: 0),
+ ),
+ generateTextField(
+ controller: passwordController,
+ size: screen,
+ text: 'Password: ',
+ validatorFunc: (s) {
+ if (s != null && s.length < 6) {
+ return 'Please do a better password (you have to)';
+ }
+ return null;
+ },
+ index: 1,
+ isPassword: true),
+ const Spacer(flex: 2),
+ UiButton(
+ width: screen.width * 0.85 > 400 ? 400 : screen.width * 0.85,
+ showLoading: true,
+ onPressed: () => login(),
+ text: 'Submit',
+ color: Styles.seaweedGreen,
+ ),
+ const Spacer(),
+ ],
+ ),
+ widget.exitNav,
+ ],
+ ),
+ );
+ }
+
+ Widget generateTextField({
+ required String text,
+ String? label,
+ required int index,
+ required TextEditingController controller,
+ isPassword = false,
+ required Size size,
+ required String? Function(String?) validatorFunc,
+ }) {
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(
+ text,
+ style: TextStyle(fontSize: size.width < 350 ? 16 : 20),
+ ),
+ ),
+ const Spacer(),
+ Container(
+ decoration: Styles.boxLavenderBubble,
+ width: size.width * 0.5 > 300 ? 300 : size.width * 0.5,
+ child: TextFormField(
+ validator: validatorFunc,
+ controller: controller,
+ decoration: Styles.inputLavenderBubble(labelText: label),
+ obscureText: isPassword,
+ focusNode: focusNodes[index],
+ style: const TextStyle(fontSize: 16),
+ onFieldSubmitted: (_) {
+ if (index != focusNodes.length - 1) {
+ focusNodes[index + 1].requestFocus();
+ }
+ },
+ ),
+ ),
+ SizedBox(
+ width: size.width * 0.05,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Future login() async {
+ try {
+ if (widget.formKey.currentState != null &&
+ !widget.formKey.currentState!.validate()) {
+ return;
+ }
+ final data =
+ await ref.read(apiProvider.notifier).post(path: 'auth/login', data: {
+ 'username':
+ usernameController.text.isEmpty ? null : usernameController.text,
+ 'email': emailController.text.isEmpty ? null : emailController.text,
+ 'password': passwordController.text,
+ });
+
+ final bool success = data?['success'] ?? false;
+ showSnack(
+ ref: ref,
+ text: data?['message'] ?? success
+ ? 'Login successful'
+ : 'Login unsuccessful',
+ type: !success ? SnackType.error : SnackType.success);
+ printAmber(data);
+ } catch (err, st) {
+ printRed('Error in login: $err\n$st');
+ }
+ }
+}
diff --git a/lib/features/account/signup.dart b/lib/features/account/signup.dart
new file mode 100644
index 0000000..b7075fa
--- /dev/null
+++ b/lib/features/account/signup.dart
@@ -0,0 +1,250 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:helpers/helpers/misc_build/build_media.dart';
+import 'package:helpers/helpers/print.dart';
+
+import '../../global/api.dart';
+import '../../global/styles.dart';
+import '../../global/utils.dart';
+import '../../global/widgets/ui_button.dart';
+
+class Signup extends ConsumerStatefulWidget {
+ const Signup({super.key, required this.formKey, required this.exitNav});
+
+ final GlobalKey formKey;
+ final Widget exitNav;
+
+ @override
+ ConsumerState createState() => _SignupState();
+}
+
+class _SignupState extends ConsumerState {
+ bool hasFamilyCode = false;
+ final nameCotroller = TextEditingController();
+ final usernameController = TextEditingController();
+ final emailController = TextEditingController();
+ final passwordController = TextEditingController();
+ final familyCodeController = TextEditingController();
+
+ final focusNodes = [
+ FocusNode(),
+ FocusNode(),
+ FocusNode(),
+ FocusNode(),
+ FocusNode(),
+ ];
+
+ @override
+ Widget build(BuildContext context) {
+ final screen = BuildMedia(context).size;
+ return Form(
+ key: widget.formKey,
+ child: Stack(
+ children: [
+ Column(
+ children: [
+ const Spacer(),
+ const Text(
+ 'Signup',
+ style: TextStyle(fontSize: 24),
+ ),
+ const Spacer(),
+ generateTextField(
+ controller: nameCotroller,
+ size: screen,
+ text: 'Name:',
+ validatorFunc: (s) {
+ if (s == null || s.isEmpty) {
+ return 'You matter! Enter a name :)';
+ }
+ return null;
+ },
+ index: 0),
+ generateTextField(
+ controller: usernameController,
+ size: screen,
+ text: 'Username: ',
+ validatorFunc: (s) {
+ if (s == null || s.isEmpty) {
+ return 'Invalid Username';
+ }
+ if (s.length < 3) {
+ return 'Username Not 3 Letters Long';
+ }
+ if (s.length > 20) {
+ return 'Username Too Long';
+ }
+ if (!isUsernameValid(s)) {
+ return 'Letters, Numbers, and ., -, _ Allowed';
+ }
+ return null;
+ },
+ index: 1),
+ generateTextField(
+ controller: emailController,
+ size: screen,
+ validatorFunc: (s) {
+ if (s != null && s.isNotEmpty && !isEmailValid(s)) {
+ return 'Email entered is invalid';
+ }
+ return null;
+ },
+ text: 'Email: (optional)',
+ index: 2),
+ generateTextField(
+ controller: passwordController,
+ size: screen,
+ validatorFunc: (s) {
+ if (s != null && s.length < 6) {
+ return 'Please do a better password (you have to)';
+ }
+ return null;
+ },
+ text: 'Password:',
+ index: 3,
+ isPassword: true),
+ const SizedBox(height: 30),
+ AnimatedSwitcher(
+ duration: const Duration(milliseconds: 300),
+ child: hasFamilyCode
+ ? Column(
+ children: [
+ generateTextField(
+ controller: familyCodeController,
+ size: screen,
+ validatorFunc: (s) {
+ if (hasFamilyCode) {
+ if (s == null || s.length != 5) {
+ return 'Invalid Code';
+ }
+ }
+ return null;
+ },
+ text: 'Family Code:',
+ index: 4,
+ ),
+ UiButton(
+ width: screen.width * 0.4,
+ icon: const Icon(Icons.cancel,
+ color: Styles.washedStone, size: 20),
+ onPressed: () {
+ setState(
+ () => hasFamilyCode = false,
+ );
+ },
+ text: 'Cancel',
+ ),
+ ],
+ )
+ : Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: UiButton(
+ width: screen.width * 0.5 > 400
+ ? 400
+ : screen.width * 0.5,
+ text: 'JOIN FAMILY',
+ // color: Styles.flounderBlue,
+ onPressed: () {
+ setState(
+ () => hasFamilyCode = true,
+ );
+ },
+ ),
+ )),
+ const Spacer(),
+ UiButton(
+ showLoading: true,
+ onPressed: () => signup(),
+ text: 'Submit',
+ color: Styles.seaweedGreen,
+ ),
+ const Spacer(),
+ ],
+ ),
+ widget.exitNav,
+ ],
+ ),
+ );
+ }
+
+ Widget generateTextField({
+ required String text,
+ String? label,
+ required int index,
+ required TextEditingController controller,
+ isPassword = false,
+ required Size size,
+ required String? Function(String?) validatorFunc,
+ }) {
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(
+ text,
+ style: TextStyle(fontSize: size.width < 350 ? 16 : 20),
+ ),
+ ),
+ const Spacer(),
+ Container(
+ decoration: Styles.boxLavenderBubble,
+ width: size.width * 0.5 > 300 ? 300 : size.width * 0.5,
+ child: TextFormField(
+ validator: validatorFunc,
+ controller: controller,
+ decoration: Styles.inputLavenderBubble(labelText: label),
+ obscureText: isPassword,
+ focusNode: focusNodes[index],
+ style: const TextStyle(fontSize: 16),
+ onFieldSubmitted: (_) {
+ if (index != focusNodes.length - 1) {
+ focusNodes[index + 1].requestFocus();
+ }
+ },
+ ),
+ ),
+ SizedBox(
+ width: size.width * 0.05,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Future signup() async {
+ try {
+ if (widget.formKey.currentState != null &&
+ !widget.formKey.currentState!.validate()) {
+ return;
+ }
+ final data =
+ await ref.read(apiProvider.notifier).post(path: 'auth/signup', data: {
+ 'name': nameCotroller.text,
+ 'username':
+ usernameController.text.isEmpty ? null : usernameController.text,
+ 'email': emailController.text.isEmpty ? null : emailController.text,
+ 'password': passwordController.text,
+ 'family_code': familyCodeController.text.isEmpty
+ ? null
+ : familyCodeController.text.toUpperCase(),
+ 'budget_name': 'Budget'
+ });
+
+ final success = data?['success'] ?? false;
+ showSnack(
+ ref: ref,
+ text: data?['message'] ?? success
+ ? 'Login successful'
+ : 'Login unsuccessful',
+ type: !success ? SnackType.error : SnackType.success);
+
+ printAmber(data);
+ // final user = User.fromJson(data?['user']);
+ // ref.read(tokenProvider.notifier).state =
+ } catch (err) {
+ printRed(err);
+ }
+ }
+}
diff --git a/lib/features/budget/screens/budget_overview.dart b/lib/features/budget/screens/budget_overview.dart
index d02f703..c26b74f 100644
--- a/lib/features/budget/screens/budget_overview.dart
+++ b/lib/features/budget/screens/budget_overview.dart
@@ -26,256 +26,285 @@ class _BudgetOverviewScreenState extends ConsumerState {
final budgetListScrollController = ScrollController();
@override
Widget build(BuildContext context) {
- final budgetCategories = ref.watch(Store().budgetCategoriesProvider);
- final transactions = ref.watch(Store().transactionsProvider);
+ final budgetCategories = ref.watch(budgetCategoriesProvider);
+ final transactions = ref.watch(transactionsProvider);
final screen = BuildMedia(context).size;
double netExpense = 0.0;
double netIncome = 0.0;
Map budgetCategoryNetMap = {};
- netExpense = transactions
- .where((t) => t.type == TransactionType.expense)
- .fold(netExpense, (net, t) => net + t.amount);
- netIncome = transactions
- .where((t) => t.type == TransactionType.income)
- .fold(netIncome, (net, t) => net + t.amount);
- for (final bud in budgetCategories) {
- double net = 0.0;
- net = transactions
- .where((t) => t.budgetCategoryId == bud.id)
- .fold(net, (net, t) => net + t.amount);
- budgetCategoryNetMap[bud.id!] = net;
- }
- return RefreshIndicator(
- displacement: 20.0,
- onRefresh: () async {
- final family = ref.read(Store().familyProvider).valueOrNull;
- final _ =
- ref.read(Store().dashboardProvider.notifier).fetchDashboard(family);
- },
- child: Column(
- children: [
- /// TOP HALF, TITLE & OVERVIEW
- Container(
- height: screen.height * 0.3,
- width: screen.width,
- color: Styles.purpleNurple,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- const Spacer(flex: 2),
- Text(
- formatDate(DateTime.now()),
- style:
- const TextStyle(fontSize: 16, color: Styles.electricBlue),
- ),
- const Spacer(),
- const Text('MONTHLY',
- style: TextStyle(fontSize: 42, color: Styles.electricBlue)),
- const Spacer(flex: 2),
- BudgetNetBar(isPositive: true, net: netIncome),
- const Spacer(),
- BudgetNetBar(isPositive: false, net: netExpense),
- const Spacer(),
- ],
- ),
- ),
-
- /// BOTTOM HALF, BUDGET BREAKDOWN
- Expanded(
- child: Container(
- color: Styles.sunflower,
+ netExpense = transactions
+ .where((t) => t.type == TransactionType.expense)
+ .fold(netExpense, (net, t) => net + t.amount);
+ netIncome = transactions
+ .where((t) => t.type == TransactionType.income)
+ .fold(netIncome, (net, t) => net + t.amount);
+ for (final bud in budgetCategories) {
+ double net = 0.0;
+ net = transactions
+ .where((t) => t.budgetCategoryId == bud.id)
+ .fold(net, (net, t) => net + t.amount);
+ budgetCategoryNetMap[bud.id!] = net;
+ }
+ return Stack(
+ children: [
+ Column(
+ children: [
+ /// TOP HALF, TITLE & OVERVIEW
+ Container(
+ height: screen.height * 0.3,
width: screen.width,
+ color: Styles.purpleNurple,
child: Column(
- mainAxisSize: MainAxisSize.max,
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- Padding(
- padding: const EdgeInsets.all(14.0),
- child: Container(
- decoration: BoxDecoration(
- color: Styles.blushingPink,
- borderRadius: BorderRadius.circular(16.0),
- ),
- child: Column(
- children: [
- const Padding(
- padding: EdgeInsets.all(8.0),
- child: Text(
- 'BUDGET',
- style: TextStyle(
- fontSize: 28, color: Styles.electricBlue),
- ),
- ),
- budgetCategories.isEmpty
- ? Padding(
- padding: const EdgeInsets.all(8.0),
- child: SizedBox(
- width: screen.width * 0.8,
- child: Column(
- children: [
- const Padding(
- padding: EdgeInsets.all(8.0),
- child: Text(
- 'No budget categories created yet, add some!'),
- ),
- ElevatedButton(
- onPressed: ref.watch(
- Store().budgetProvider) ==
- null
- ? null
- : () => showDialog(
- context: context,
- builder: (context) => Dialog(
- shape:
- Styles.dialogShape,
- backgroundColor:
- Styles.dialogColor,
- child:
- const AddBudgetCategoryDialog()),
- ),
- child: const Text('Add Category'),
- ),
- ],
- ),
- ),
- )
- : Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 2.0, vertical: 4.0),
- child: SizedBox(
- height: screen.height * 0.36,
- child: Scrollbar(
- controller: budgetListScrollController,
- thumbVisibility: true,
- child: ListView(
- physics: const BouncingScrollPhysics(),
- controller: budgetListScrollController,
- shrinkWrap: true,
- children: [
- ...budgetCategories
- .mapIndexed((i, category) {
- return BudgetCategoryBar(
- budgetCategory: category,
- currentAmount:
- budgetCategoryNetMap[
- category.id]!,
- index: i,
- );
- }).toList(),
- const SizedBox(height: 20),
- Row(
- mainAxisAlignment:
- MainAxisAlignment.center,
- children: [
- SizedBox(
- width: 140,
- child: ElevatedButton(
- onPressed: ref.watch(Store()
- .budgetProvider) ==
- null
- ? null
- : () => showDialog(
- context: context,
- builder: (context) => Dialog(
- shape: Styles
- .dialogShape,
- backgroundColor:
- Styles
- .dialogColor,
- child:
- const AddBudgetCategoryDialog()),
- ),
- child: const Text(
- 'Add Category'),
- ),
- ),
- ],
- ),
- const SizedBox(height: 20),
- ],
- ),
- ),
- ),
- ),
- ],
- ),
- ),
+ const Spacer(flex: 2),
+ Text(
+ formatDate(DateTime.now()),
+ style: const TextStyle(
+ fontSize: 16, color: Styles.electricBlue),
),
const Spacer(),
- Row(
- children: [
- Expanded(
- child: Padding(
- padding:
- const EdgeInsets.only(left: 20.0, right: 15.0),
- child: Container(
- height: 70,
- decoration: BoxDecoration(
- color: Styles.seaweedGreen,
- borderRadius: BorderRadius.circular(15.0),
- ),
- child: InkWell(
- child: const Center(
- child: Padding(
- padding: EdgeInsets.symmetric(
- horizontal: 20.0, vertical: 14.0),
- child: FittedBox(
- child: Text(
- 'Transaction History',
- textAlign: TextAlign.center,
- style: TextStyle(fontSize: 25),
- ),
- ),
- ),
- ),
- onTap: () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) =>
- const TransactionsListview()));
- },
- ),
- ),
- ),
- ),
- Padding(
- padding: const EdgeInsets.only(right: 20.0),
- child: Container(
- decoration: BoxDecoration(
- color: Styles.purpleNurple,
- borderRadius: BorderRadius.circular(40.0),
- ),
- height: 80,
- width: 80,
- child: IconButton(
- icon: const Icon(
- Icons.add,
- size: 48,
- color: Styles.lavender,
- ),
- onPressed: () {
- showDialog(
- context: context,
- builder: (BuildContext context) {
- return Dialog(
- backgroundColor: Styles.dialogColor,
- shape: Styles.dialogShape,
- child: const AddTransactionDialog());
- },
- );
- },
- ),
- ),
- ),
- ],
- ),
+ const Text('MONTHLY',
+ style:
+ TextStyle(fontSize: 42, color: Styles.electricBlue)),
+ const Spacer(flex: 2),
+ BudgetNetBar(isPositive: true, net: netIncome),
+ const Spacer(),
+ BudgetNetBar(isPositive: false, net: netExpense),
const Spacer(),
],
),
),
- )
- ],
- ),
+
+ /// BOTTOM HALF, BUDGET BREAKDOWN
+ Expanded(
+ child: Container(
+ color: Styles.sunflower,
+ width: screen.width,
+ child: Column(
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(14.0),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Styles.blushingPink,
+ borderRadius: BorderRadius.circular(16.0),
+ ),
+ child: Column(
+ children: [
+ const Padding(
+ padding: EdgeInsets.all(8.0),
+ child: Text(
+ 'BUDGET',
+ style: TextStyle(
+ fontSize: 28, color: Styles.electricBlue),
+ ),
+ ),
+ budgetCategories.isEmpty
+ ? Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SizedBox(
+ width: screen.width * 0.8,
+ child: Column(
+ children: [
+ const Padding(
+ padding: EdgeInsets.all(8.0),
+ child: Text(
+ 'No budget categories created yet, add some!'),
+ ),
+ ElevatedButton(
+ onPressed:
+ ref.watch(budgetProvider) ==
+ null
+ ? null
+ : () => showDialog(
+ context: context,
+ builder: (context) => Dialog(
+ shape: Styles
+ .dialogShape,
+ backgroundColor:
+ Styles
+ .dialogColor,
+ child:
+ const AddBudgetCategoryDialog()),
+ ),
+ child: const Text('Add Category'),
+ ),
+ ],
+ ),
+ ),
+ )
+ : Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 2.0, vertical: 4.0),
+ child: SizedBox(
+ height: screen.height * 0.36,
+ child: Scrollbar(
+ controller: budgetListScrollController,
+ thumbVisibility: true,
+ child: ListView(
+ physics:
+ const BouncingScrollPhysics(),
+ controller:
+ budgetListScrollController,
+ shrinkWrap: true,
+ children: [
+ ...budgetCategories
+ .mapIndexed((i, category) {
+ return BudgetCategoryBar(
+ budgetCategory: category,
+ currentAmount:
+ budgetCategoryNetMap[
+ category.id]!,
+ index: i,
+ );
+ }).toList(),
+ const SizedBox(height: 20),
+ Row(
+ mainAxisAlignment:
+ MainAxisAlignment.center,
+ children: [
+ SizedBox(
+ width: 140,
+ child: ElevatedButton(
+ onPressed: ref.watch(
+ budgetProvider) ==
+ null
+ ? null
+ : () => showDialog(
+ context: context,
+ builder: (context) => Dialog(
+ shape: Styles
+ .dialogShape,
+ backgroundColor:
+ Styles
+ .dialogColor,
+ child:
+ const AddBudgetCategoryDialog()),
+ ),
+ child: const Text(
+ 'Add Category'),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 20),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ const Spacer(),
+ Row(
+ children: [
+ Expanded(
+ child: Padding(
+ padding:
+ const EdgeInsets.only(left: 20.0, right: 15.0),
+ child: Container(
+ height: 70,
+ decoration: BoxDecoration(
+ color: Styles.seaweedGreen,
+ borderRadius: BorderRadius.circular(15.0),
+ ),
+ child: InkWell(
+ child: const Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(
+ horizontal: 20.0, vertical: 14.0),
+ child: FittedBox(
+ child: Text(
+ 'Transaction History',
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 25),
+ ),
+ ),
+ ),
+ ),
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) =>
+ const TransactionsListview()));
+ },
+ ),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(right: 20.0),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Styles.purpleNurple,
+ borderRadius: BorderRadius.circular(40.0),
+ ),
+ height: 80,
+ width: 80,
+ child: IconButton(
+ icon: const Icon(
+ Icons.add,
+ size: 48,
+ color: Styles.lavender,
+ ),
+ onPressed: () {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return Dialog(
+ backgroundColor: Styles.dialogColor,
+ shape: Styles.dialogShape,
+ child: const AddTransactionDialog());
+ },
+ );
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ const Spacer(),
+ ],
+ ),
+ ),
+ )
+ ],
+ ),
+ Align(
+ alignment: Alignment.topRight,
+ child: Padding(
+ padding: const EdgeInsets.only(top: 5.0, right: 5.0),
+ child: IconButton(
+ icon: const Icon(Icons.loop),
+ color: Styles.seaweedGreen,
+ onPressed: () =>
+ ref.read(dashboardProvider.notifier).fetchDashboard(),
+ ),
+ ),
+ ),
+ IgnorePointer(
+ child: AnimatedOpacity(
+ duration: const Duration(milliseconds: 150),
+ opacity: ref.watch(loadingStateProvider) ? 0.75 : 0.0,
+ child: Container(
+ color: Colors.black12,
+ height: screen.height,
+ width: screen.width,
+ child: const Center(
+ child: CircularProgressIndicator(
+ color: Styles.lavender,
+ strokeWidth: 2.5,
+ ),
+ ),
+ ),
+ ),
+ )
+ ],
);
}
}
diff --git a/lib/features/budget/screens/transactions_listview.dart b/lib/features/budget/screens/transactions_listview.dart
index 8adc0c2..14afc2b 100644
--- a/lib/features/budget/screens/transactions_listview.dart
+++ b/lib/features/budget/screens/transactions_listview.dart
@@ -16,7 +16,7 @@ class TransactionsListview extends ConsumerStatefulWidget {
class _TransactionsListviewState extends ConsumerState {
@override
Widget build(BuildContext context) {
- final transactions = ref.watch(Store().transactionsProvider);
+ final transactions = ref.watch(transactionsProvider);
transactions.sort(
(a, b) => b.date.compareTo(a.date),
);
diff --git a/lib/features/budget/widgets/add_budget_category_dialog.dart b/lib/features/budget/widgets/add_budget_category_dialog.dart
index 3846bd2..840ef0b 100644
--- a/lib/features/budget/widgets/add_budget_category_dialog.dart
+++ b/lib/features/budget/widgets/add_budget_category_dialog.dart
@@ -8,6 +8,8 @@ import 'package:rluv/models/budget_category_model.dart';
import '../../../global/api.dart';
import '../../../global/store.dart';
+import '../../../global/utils.dart';
+import '../../../global/widgets/ui_button.dart';
class AddBudgetCategoryDialog extends ConsumerStatefulWidget {
const AddBudgetCategoryDialog({super.key});
@@ -19,8 +21,6 @@ class AddBudgetCategoryDialog extends ConsumerStatefulWidget {
class _AddBudgetCategoryDialogState
extends ConsumerState {
- bool loading = false;
- bool complete = false;
late final Budget? budget;
final categoryNameController = TextEditingController();
final amountController = TextEditingController();
@@ -34,7 +34,7 @@ class _AddBudgetCategoryDialogState
final formKey = GlobalKey();
@override
void initState() {
- budget = ref.read(Store().budgetProvider);
+ budget = ref.read(budgetProvider);
WidgetsBinding.instance.addPostFrameCallback((_) {
categoryFocusNode.requestFocus();
});
@@ -46,20 +46,10 @@ class _AddBudgetCategoryDialogState
if (budget == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pop(context);
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Could not get your budget'),
- ),
- );
- });
- return Container();
- } else if (complete) {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- Navigator.pop(context);
- ScaffoldMessenger.of(context).showSnackBar(
- const SnackBar(
- content: Text('Budget Category Added!'),
- ),
+ showSnack(
+ ref: ref,
+ text: 'Could not get your budget',
+ type: SnackType.error,
);
});
return Container();
@@ -215,10 +205,20 @@ class _AddBudgetCategoryDialogState
),
),
),
- ElevatedButton(
- onPressed: () => submitCategory(colors[selectedColorIndex]),
- child: const Text('SAVE'),
- )
+ UiButton(
+ showLoading: true,
+ color: Styles.lavender,
+ text: 'SAVE',
+ onPressed: () =>
+ submitCategory(colors[selectedColorIndex]).then((_) {
+ Navigator.pop(context);
+ showSnack(
+ ref: ref,
+ text: 'Budget Category Added!',
+ type: SnackType.success,
+ );
+ }),
+ ),
],
),
),
@@ -227,7 +227,6 @@ class _AddBudgetCategoryDialogState
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
- if (loading) const Center(child: CircularProgressIndicator()),
],
),
);
@@ -238,9 +237,6 @@ class _AddBudgetCategoryDialogState
printPink('Failed validation');
return;
}
- setState(
- () => loading = true,
- );
final newBudget = BudgetCategory(
id: null,
amount: double.parse(amountController.text),
@@ -249,17 +245,16 @@ class _AddBudgetCategoryDialogState
name: categoryNameController.text,
);
- final budgetData =
- await Api().post(path: 'budget_category', data: newBudget.toJson());
+ final budgetData = await ref
+ .read(apiProvider.notifier)
+ .post(path: 'budget_category', data: newBudget.toJson());
final success = budgetData != null ? budgetData['success'] as bool : false;
if (success) {
ref
- .read(Store().dashboardProvider.notifier)
+ .read(dashboardProvider.notifier)
.add({'budget_categories': budgetData});
+ } else {
+ showSnack(ref: ref, text: 'Could not add budget', type: SnackType.error);
}
- complete = true;
- setState(() {
- loading = false;
- });
}
}
diff --git a/lib/features/budget/widgets/add_transaction_dialog.dart b/lib/features/budget/widgets/add_transaction_dialog.dart
index 92fa676..5466350 100644
--- a/lib/features/budget/widgets/add_transaction_dialog.dart
+++ b/lib/features/budget/widgets/add_transaction_dialog.dart
@@ -2,10 +2,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:helpers/helpers/misc_build/build_media.dart';
+import 'package:rluv/global/utils.dart';
import '../../../global/api.dart';
import '../../../global/store.dart';
import '../../../global/styles.dart';
+import '../../../global/widgets/ui_button.dart';
import '../../../models/budget_category_model.dart';
import '../../../models/transaction_model.dart';
@@ -20,8 +22,7 @@ class AddTransactionDialog extends ConsumerStatefulWidget {
}
class _AddTransactionDialogState extends ConsumerState {
- bool loading = false;
- bool complete = false;
+ late DateTime selectedDate;
late final TextEditingController amountController;
late final TextEditingController memoController;
@@ -38,11 +39,13 @@ class _AddTransactionDialogState extends ConsumerState {
TextEditingController(text: widget.transaction!.amount.toString());
memoController = TextEditingController(text: widget.transaction!.memo);
transactionType = widget.transaction!.type;
+ selectedDate = widget.transaction!.date;
} else {
amountController = TextEditingController();
memoController = TextEditingController();
+ selectedDate = DateTime.now();
}
- final categories = ref.read(Store().budgetCategoriesProvider);
+ final categories = ref.read(budgetCategoriesProvider);
if (categories.isNotEmpty) {
selectedBudgetCategory = categories.first;
}
@@ -51,21 +54,8 @@ class _AddTransactionDialogState extends ConsumerState {
@override
Widget build(BuildContext context) {
- if (complete) {
- WidgetsBinding.instance.addPostFrameCallback((_) {
- Navigator.pop(context);
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(widget.transaction != null
- ? 'Transaction updated!'
- : 'Transaction added!'),
- ),
- );
- });
- return Container();
- }
final List budgetCategories =
- ref.read(Store().budgetCategoriesProvider);
+ ref.read(budgetCategoriesProvider);
return SizedBox(
width: BuildMedia(context).width * Styles.dialogScreenWidthFactor,
child: budgetCategories.isEmpty
@@ -260,9 +250,20 @@ class _AddTransactionDialogState extends ConsumerState {
onFieldSubmitted: (_) {},
),
),
- ElevatedButton(
- onPressed: loading ? null : () => submitTransaction(),
- child: const Text('Add'),
+ UiButton(
+ showLoading: true,
+ text: 'ADD',
+ color: Styles.lavender,
+ onPressed: () => submitTransaction().then((_) {
+ Navigator.pop(context);
+ showSnack(
+ ref: ref,
+ text: widget.transaction != null
+ ? 'Transaction updated!'
+ : 'Transaction added!',
+ type: SnackType.success,
+ );
+ }),
),
],
),
@@ -270,12 +271,9 @@ class _AddTransactionDialogState extends ConsumerState {
}
Future submitTransaction() async {
- setState(() {
- loading = true;
- });
Map? data;
if (widget.transaction != null) {
- data = await Api().put(
+ data = await ref.read(apiProvider.notifier).put(
path: 'transactions',
data: Transaction.copyWith(widget.transaction!, {
'memo': memoController.text.isNotEmpty ? memoController.text : null,
@@ -285,7 +283,7 @@ class _AddTransactionDialogState extends ConsumerState {
: selectedBudgetCategory.id,
}).toJson());
} else {
- data = await Api().post(
+ data = await ref.read(apiProvider.notifier).post(
path: 'transactions',
data: Transaction(
amount: double.parse(amountController.text),
@@ -304,26 +302,17 @@ class _AddTransactionDialogState extends ConsumerState {
final success = data != null ? data['success'] as bool : false;
if (success) {
if (widget.transaction != null) {
- ref
- .read(Store().dashboardProvider.notifier)
- .update({'transactions': data});
+ ref.read(dashboardProvider.notifier).update({'transactions': data});
} else {
- ref
- .read(Store().dashboardProvider.notifier)
- .add({'transactions': data});
+ ref.read(dashboardProvider.notifier).add({'transactions': data});
}
- complete = true;
} else {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(widget.transaction != null
+ showSnack(
+ ref: ref,
+ text: widget.transaction != null
? 'Failed to edit transaction'
- : 'Failed to add transaction'),
- ),
- );
+ : 'Failed to add transaction',
+ type: SnackType.error);
}
- setState(() {
- loading = false;
- });
}
}
diff --git a/lib/features/budget/widgets/budget_category_bar.dart b/lib/features/budget/widgets/budget_category_bar.dart
index 11d25a7..5dea869 100644
--- a/lib/features/budget/widgets/budget_category_bar.dart
+++ b/lib/features/budget/widgets/budget_category_bar.dart
@@ -14,7 +14,7 @@ class BudgetCategoryBar extends StatefulWidget {
required this.currentAmount,
required this.index,
this.height = 32,
- this.innerPadding = 1.5});
+ this.innerPadding = 2.5});
final int index;
final double currentAmount;
@@ -27,6 +27,7 @@ class BudgetCategoryBar extends StatefulWidget {
class _BudgetCategoryBarState extends State {
double percentSpent = 0.0;
+
@override
void initState() {
Future.delayed(Duration(milliseconds: min(1600, 200 * widget.index)), () {
@@ -76,7 +77,7 @@ class _BudgetCategoryBarState extends State {
height: widget.height,
decoration: BoxDecoration(
color: Colors.black,
- borderRadius: BorderRadius.circular(14.0),
+ borderRadius: BorderRadius.circular(13.0),
),
),
Padding(
diff --git a/lib/features/budget/widgets/transaction_list_item.dart b/lib/features/budget/widgets/transaction_list_item.dart
index 8bbb406..86604f7 100644
--- a/lib/features/budget/widgets/transaction_list_item.dart
+++ b/lib/features/budget/widgets/transaction_list_item.dart
@@ -35,8 +35,8 @@ class _TransactionListItemState extends ConsumerState {
@override
Widget build(BuildContext context) {
- final transaction = ref.watch(Store().transactionsProvider)[widget.index];
- final budgetCategories = ref.read(Store().budgetCategoriesProvider);
+ final transaction = ref.watch(transactionsProvider)[widget.index];
+ final budgetCategories = ref.read(budgetCategoriesProvider);
if (transaction.type == TransactionType.expense) {
budgetCategory = budgetCategories.singleWhere(
(category) => category.id == transaction.budgetCategoryId,
diff --git a/lib/features/notes/screens/notes_screen.dart b/lib/features/notes/screens/notes_screen.dart
index 02add73..cd47158 100644
--- a/lib/features/notes/screens/notes_screen.dart
+++ b/lib/features/notes/screens/notes_screen.dart
@@ -1,8 +1,15 @@
+import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:helpers/helpers/misc_build/build_media.dart';
+import 'package:helpers/helpers/print.dart';
+import 'package:rluv/global/utils.dart';
+import 'package:rluv/models/shared_note.dart';
+import '../../../global/api.dart';
import '../../../global/store.dart';
+import '../../../global/styles.dart';
+import '../../../global/widgets/ui_button.dart';
class SharedNotesScreen extends ConsumerStatefulWidget {
const SharedNotesScreen({super.key, required this.initialData});
@@ -15,7 +22,8 @@ class SharedNotesScreen extends ConsumerStatefulWidget {
class _SharedNotesScreenState extends ConsumerState {
@override
Widget build(BuildContext context) {
- final sharedNotes = ref.watch(Store().sharedNotesProvider);
+ final screen = BuildMedia(context).size;
+ final sharedNotes = ref.watch(sharedNotesProvider);
return Column(
children: [
const Padding(
@@ -26,26 +34,255 @@ class _SharedNotesScreenState extends ConsumerState {
textAlign: TextAlign.center,
),
),
- if (sharedNotes.isEmpty) const Text('Add notes to get started:'),
+ if (sharedNotes.isEmpty) ...[
+ const Text('Add notes to get started:'),
+ ],
if (sharedNotes.isNotEmpty)
- GridView.builder(
- gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 2,
- ),
- itemBuilder: (BuildContext context, int index) {
- final note = sharedNotes[index];
- return SizedBox(
- width: BuildMedia(context).width * 0.4,
- child: Card(
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: Text(note.content),
+ SizedBox(
+ height: screen.height * 0.65,
+ child: GridView.builder(
+ itemCount: sharedNotes.length,
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: 2,
+ ),
+ itemBuilder: (BuildContext context, int index) {
+ final note = sharedNotes[index];
+ final colorBrightness = note.color == null
+ ? Brightness.light
+ : getBrightness(note.color!);
+ final textStyle = TextStyle(
+ fontSize: 16.0,
+ color: colorBrightness == Brightness.light
+ ? Colors.black54
+ : Colors.white70,
+ );
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: InkWell(
+ child: Card(
+ color: note.color ?? Styles.washedStone,
+ child: SizedBox(
+ width: screen.width * 0.4,
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ children: [
+ FittedBox(
+ child: Text(note.title,
+ style: TextStyle(fontSize: 20))),
+ Flexible(
+ child: Text(
+ note.content.length > 80
+ ? "${note.content.substring(0, 75)}..."
+ : note.content,
+ style: textStyle,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ onTap: () {
+ showModalBottomSheet(
+ isScrollControlled: true,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20.0),
+ ),
+ backgroundColor: Colors.transparent,
+ context: context,
+ builder: (BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.only(
+ bottom:
+ MediaQuery.of(context).viewInsets.bottom),
+ child: NoteBottomSheet(
+ index: index,
+ ),
+ );
+ });
+ },
),
- ),
- );
- },
+ );
+ },
+ ),
),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 18.0, horizontal: 12.0),
+ child: UiButton(
+ text: 'Add Note',
+ onPressed: () {
+ showModalBottomSheet(
+ isScrollControlled: true,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(20.0),
+ ),
+ backgroundColor: Colors.transparent,
+ context: context,
+ builder: (BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.only(
+ bottom: MediaQuery.of(context).viewInsets.bottom),
+ child: const NoteBottomSheet(
+ index: null,
+ ),
+ );
+ });
+ }),
+ ),
],
);
}
}
+
+class NoteBottomSheet extends ConsumerStatefulWidget {
+ const NoteBottomSheet({super.key, required this.index});
+
+ final int? index;
+
+ @override
+ ConsumerState createState() => _NoteBottomSheetState();
+}
+
+class _NoteBottomSheetState extends ConsumerState {
+ late final SharedNote? oldNote;
+ late SharedNote newNote;
+
+ final titleFocusNode = FocusNode();
+ final contentFocusNode = FocusNode();
+ late final TextEditingController titleController;
+ late final TextEditingController contentController;
+
+ @override
+ void initState() {
+ super.initState();
+ if (widget.index == null) {
+ oldNote = null;
+ final family = ref.read(familyProvider);
+ final user = ref.read(userProvider);
+ newNote = SharedNote(
+ familyId: family!.id,
+ createdByUserId: user!.id!,
+ title: 'Title',
+ content: '',
+ tagIds: [],
+ color: Styles.washedStone,
+ isMarkdown: false,
+ );
+ } else {
+ oldNote = ref.read(sharedNotesProvider).elementAt(widget.index!);
+ newNote = SharedNote.copy(oldNote!);
+ }
+ titleController = TextEditingController(text: newNote.title);
+ contentController = TextEditingController(text: newNote.content);
+ WidgetsBinding.instance.addPostFrameCallback(
+ (timeStamp) => titleFocusNode.requestFocus(),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final screen = BuildMedia(context).size;
+ final bottomInset = MediaQuery.of(context).viewInsets.bottom;
+ return Container(
+ width: screen.width,
+ decoration: BoxDecoration(
+ color: newNote.color ?? Styles.washedStone,
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(20.0), topRight: Radius.circular(20.0)),
+ boxShadow: const [
+ BoxShadow(
+ color: Colors.black26,
+ spreadRadius: 5.0,
+ blurRadius: 2.0,
+ offset: Offset(0, -2))
+ ]),
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ TextField(
+ style: const TextStyle(fontSize: 18.0),
+ maxLines: 1,
+ focusNode: titleFocusNode,
+ textAlign: TextAlign.left,
+ controller: titleController,
+ // decoration: Styles.inputLavenderBubble(),
+ ),
+ TextField(
+ style: const TextStyle(fontSize: 18.0),
+ maxLines: 6,
+ focusNode: contentFocusNode,
+ textAlign: TextAlign.left,
+ controller: contentController,
+ // decoration: Styles.inputLavenderBubble(),
+ ),
+ Padding(
+ padding:
+ const EdgeInsets.symmetric(vertical: 28.0, horizontal: 36.0),
+ child: UiButton(
+ onPressed: !noteChanged(oldNote, newNote)
+ ? null
+ : () async {
+ newNote.content = contentController.text;
+ newNote.title = titleController.text;
+ try {
+ Response? res;
+ if (widget.index == null) {
+ res = await ref
+ .read(apiProvider)
+ .post('shared_notes', data: newNote.toJson());
+ } else {
+ res = await ref
+ .read(apiProvider)
+ .put('shared_notes', data: newNote.toJson());
+ }
+ if (res.data != null && res.data['success']) {
+ if (widget.index == null) {
+ ref
+ .read(dashboardProvider.notifier)
+ .add({'shared_notes': res.data});
+ } else {
+ ref
+ .read(dashboardProvider.notifier)
+ .update({'shared_notes': res.data});
+ }
+ showSnack(
+ ref: ref,
+ text: 'Added note',
+ type: SnackType.success);
+ } else {
+ showSnack(
+ ref: ref,
+ text: res.data['message'] ??
+ 'Unexpected error occurred',
+ type: SnackType.error);
+ }
+ } catch (err) {
+ showSnack(
+ ref: ref,
+ text: 'Unexpected error occurred',
+ type: SnackType.error);
+ printRed(err);
+ }
+ Navigator.pop(context);
+ },
+ text: 'Save',
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ bool noteChanged(SharedNote? n, SharedNote m) {
+ if (n == null) return true;
+ if (n.content != m.content) return true;
+ if (n.title != m.content) return true;
+ if (n.tagIds != m.tagIds) return true;
+ if (n.color != m.color) return true;
+ return false;
+ }
+}
diff --git a/lib/features/settings/screens/settings_screen.dart b/lib/features/settings/screens/settings_screen.dart
index c42ce36..4a1ff5d 100644
--- a/lib/features/settings/screens/settings_screen.dart
+++ b/lib/features/settings/screens/settings_screen.dart
@@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../../global/api.dart';
+import '../../../global/store.dart';
+import '../../../global/widgets/ui_button.dart';
+
class SettingsScreen extends ConsumerStatefulWidget {
const SettingsScreen({super.key});
@@ -11,9 +15,29 @@ class SettingsScreen extends ConsumerStatefulWidget {
class _SettingsScreenState extends ConsumerState {
@override
Widget build(BuildContext context) {
- return const Column(
+ final user = ref.read(userProvider);
+ final family = ref.read(familyProvider);
+ return Column(
children: [
- Text('Settings'),
+ const Text('Settings'),
+ const SizedBox(height: 20),
+ Text(user!.name),
+ Text("Username: ${user.username ?? 'N/A'}"),
+ Text("Email: ${user.email ?? 'N/A'}"),
+ Text("Family Code: ${family?.code ?? 'N/A'}"),
+ const Spacer(),
+ UiButton(
+ onPressed: () {
+ ref.read(jwtProvider.notifier).revokeToken();
+ // Navigator.pushAndRemoveUntil(
+ // context,
+ // MaterialPageRoute(builder: (ctx) => const AccountCreateScreen()),
+ // (_) => false,
+ // );
+ },
+ text: 'Sign Out',
+ ),
+ const SizedBox(height: 20),
],
);
}
diff --git a/lib/global/api.dart b/lib/global/api.dart
index c4e2878..0b6a40d 100644
--- a/lib/global/api.dart
+++ b/lib/global/api.dart
@@ -2,58 +2,104 @@ import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:helpers/helpers/print.dart';
+import 'package:jwt_decoder/jwt_decoder.dart';
+import 'package:rluv/global/store.dart';
-class Api {
- static final Api _instance = Api._internal();
+import '../models/token.dart';
- factory Api() {
- if (_instance.initDone) return _instance;
-
- _instance.initDone = true;
- _instance.dio = Dio();
- _instance.dio.options.baseUrl = "http://localhost:8081/";
- // _instance.dio.options.baseUrl = "https://rluv.fosscat.com/";
- _instance.dio.interceptors.add(
- LoggingInterceptor(),
- // InterceptorsWrapper(
- // onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
- // // Do something before request is sent.
- // // If you want to resolve the request with custom data,
- // // you can resolve a `Response` using `handler.resolve(response)`.
- // // If you want to reject the request with a error message,
- // // you can reject with a `DioException` using `handler.reject(dioError)`.
- // return handler.next(options);
- // },
- // onResponse: (Response response, ResponseInterceptorHandler handler) {
- // if (response.statusCode != null &&
- // response.statusCode! < 500 &&
- // response.statusCode! >= 400) {
- // return handler.reject(DioException.badResponse(
- // requestOptions: RequestOptions(),
- // response: response,
- // statusCode: response.statusCode!));
- // }
- // // Do something with response data.
- // // If you want to reject the request with a error message,
- // // you can reject a `DioException` object using `handler.reject(dioError)`.
- // return handler.next(response);
- // },
- // onError: (DioException e, ErrorInterceptorHandler handler) {
- // printPink(e);
- // // Do something with response error.
- // // If you want to resolve the request with some custom data,
- // // you can resolve a `Response` object using `handler.resolve(response)`.
- // return handler.next(e);
- // },
- // ),
- );
- return _instance;
+final tokenProvider = StateProvider((ref) {
+ final jwt = ref.watch(jwtProvider);
+ printLime('Current token: $jwt');
+ if (jwt == null) return null;
+ try {
+ return Token.fromJson(JwtDecoder.decode(jwt));
+ } catch (_) {
+ return null;
}
- Api._internal();
+});
- bool initDone = false;
- late final Dio dio;
+final jwtProvider = StateNotifierProvider<_JwtNotifier, String?>((ref) {
+ final prefs = ref.watch(prefsProvider);
+ final jwt = prefs?.getString('jwt');
+ return _JwtNotifier(ref, jwt);
+});
+
+class _JwtNotifier extends StateNotifier {
+ _JwtNotifier(this.ref, String? jwt) : super(null) {
+ if (jwt != null) {
+ setToken(jwt);
+ }
+ }
+
+ final StateNotifierProviderRef ref;
+
+ void setToken(String jwt) {
+ state = jwt;
+ printCyan('Loaded jwt into client: $jwt');
+ ref.read(prefsProvider)?.setString('jwt', jwt);
+ }
+
+ void revokeToken() {
+ printCyan('jwt token revoked');
+ state = null;
+ ref.read(prefsProvider)?.remove('jwt');
+ }
+}
+
+final apiProvider = StateNotifierProvider<_ApiNotifier, Dio>((ref) {
+ return _ApiNotifier(ref, Dio());
+});
+
+class _ApiNotifier extends StateNotifier {
+ _ApiNotifier(this.ref, this.dio) : super(dio) {
+ // dio.options.baseUrl = "https://fe7d-136-36-2-234.ngrok-free.app";
+ // dio.options.baseUrl = "http://localhost:8081/";
+ dio.options.baseUrl = "https://rluv.fosscat.com/";
+ dio.interceptors.addAll([
+ InterceptorsWrapper(onRequest:
+ (RequestOptions options, RequestInterceptorHandler handler) {
+ final jwt = ref.read(jwtProvider);
+ if (jwt != null) {
+ options.headers['token'] = jwt;
+ }
+ return handler.next(options);
+ }, onResponse: (Response response, ResponseInterceptorHandler handler) {
+ if (response.statusCode != null) {
+ if (response.statusCode == 200) {
+ try {
+ if ((response.data as Map)
+ .containsKey('success')) {
+ if (!response.data['success']) return handler.next(response);
+ }
+ if ((response.data as Map)
+ .containsKey('token')) {
+ final jwt = response.data['token'];
+ if (jwt != null) {
+ ref.read(jwtProvider.notifier).setToken(jwt);
+ // ref.read(tokenProvider.notifier).state =
+ // Token.fromJson(jwtData);
+ }
+ }
+ } catch (err) {
+ printRed('Error in interceptor for token: $err');
+ return handler.next(response);
+ }
+ }
+ }
+ return handler.next(response);
+ }, onError: (DioException err, ErrorInterceptorHandler handler) {
+ if (err.response?.statusCode == 403) {
+ ref.read(jwtProvider.notifier).revokeToken();
+ }
+ }),
+ _LoggingInterceptor(),
+ ]);
+ }
+
+ final Dio dio;
+ final StateNotifierProviderRef ref;
Future