diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 00000000..20cbbc80 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "myfluttertask" + } +} diff --git a/.metadata b/.metadata index 784ce129..8ca14df4 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3" + revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1" channel: "stable" project_type: app @@ -13,11 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: android + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: ios + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: linux + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: macos + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 - platform: web - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: windows + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 # User provided section diff --git a/README.md b/README.md index 451d649a..0feb4712 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,224 @@ -# get-flutter-fire -This codebase provides a boilerplate code utilizing the following three technologies: -1. Flutter 3.0 - For UX and uses Dart languange. See [https://flutter.dev/] -2. GetX - State management for Flutter. See [https://github.com/jonataslaw/getx/tree/4.6.1] -3. Firebase - For Backend as a Service. See [https://firebase.google.com/] - 1. Easy Authentication flow - 2. Server side functions - 3. Remote Configurations which can be used for A/B testing +# Flutter Firebase Goodies Shopping App -This was created as part of my own learning process and you will find that git commits were made according to the Steps listed below. You can use the git version history to check each commit and learn step by step, as I did. +Welcome to the Flutter Firebase Goodies Shopping App! This app showcases an exciting e-commerce platform built using Flutter and Firebase, with multiple user flows: Guest, Buyer, Seller, and Admin. Follow the instructions below to set up the project and explore its features. -I am also using this codebase as an experiment towards hiring people (freshers especially but not limited to them) for my development team. If you are in Mumbai and are interested to join my team, you can use this codebase in the following manner: +## Getting Started -* Fork the codebase -* Add your own firebase_options.dart (follow steps and see firebase_options.template) -* **Build your own application using this as a base (integrating any existing project of yours also works)**, or complete a TODO or fix a bug (only if you have no other ideas) -* Send me a Pull Request. Mention a way of connecting with you in the commit message along with details of commit. Also modify ReadMe to say what you have changed in detail. -* I will go through the request and then connect with you if I find the entry to be interesting and take an interview round. +### 1. Add the Android Folder -## The Steps -Step 1: Use Get CLI [https://pub.dev/packages/get_cli] -`get create project` +- **Generate the Android Folder in Your Existing Project** + + In your main project directory, you can directly create the `android` folder by running: + ```bash + flutter create . + ``` -Step 2: Copy code from [https://github.com/jonataslaw/getx/tree/4.6.1/example_nav2/lib] +### 2. Firebase Setup -Step 3: Integrate FlutterFire Authentication +To use Firebase features in your app, you need to configure Firebase by following one of these options: -- Tutorials [https://firebase.google.com/codelabs/firebase-auth-in-flutter-apps#0] for inspiration -- Firebase Documentation [https://firebase.google.com/docs/auth/flutter/start] -- Blog [www.medium.com/TBD] -- To compile the code ensure that you generate your own firebase_options.dart by running +1. **Add `firebase.json` file**: Download the `firebase.json` configuration file from the Firebase console and place it in the root directory of your project or Use Firebase Cli and configure accordingly. - `flutterfire configure` +2. **Use Firebase Emulator**: To set up the Firebase Emulator for local testing: -Step 4: Add Google OAuth [https://firebase.google.com/codelabs/firebase-auth-in-flutter-apps#6]. Note ensure you do the steps for Android and iOS as the code for it is not in Github + - Install the Firebase CLI if you haven't already: + ```bash + npm install -g firebase-tools + ``` -Step 5: Add Guest User/Anonymous login with a Cart and Checkout use case [https://firebase.google.com/docs/auth/flutter/anonymous-auth] + - Initialize Firebase in your project: + ```bash + firebase init + ``` -* delete unlinked anonymous user post logout + - Start the Firebase Emulator: + ```bash + firebase emulators:start --import=./emulator-data + ``` -Step 6: Add ImagePicker and Firebase Storage for profile image + - This must start the firebase emulator with such visbile output: +
+ + +
-* Create PopupMenu button for web [https://api.flutter.dev/flutter/material/PopupMenuButton-class.html] -* BottomSheet for phones and single file button for desktops -* GetX and Image Picker [https://stackoverflow.com/questions/66559553/flutter-imagepicker-with-getx] -* Add FilePicker [https://medium.com/@onuaugustine07/pick-any-file-file-picker-flutter-f82c0144e27c] -* Firebase Storage [https://mercyjemosop.medium.com/select-and-upload-images-to-firebase-storage-flutter-6fac855970a9] and [https://firebase.google.com/docs/storage/flutter/start] - Modify the Firebase Rules - `service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow write: if request.auth.uid != null; allow read: if true; } } }` - Fix CORS [https://stackoverflow.com/questions/37760695/firebase-storage-and-access-control-allow-origin] -* PList additions [https://medium.com/unitechie/flutter-tutorial-image-picker-from-camera-gallery-c27af5490b74] -Step 7: Additional Auth flow items +- **Test the App**: Open the app and interact with it to test different user flows while connected to the local emulator. +- Note: just make sure to keep that particular port free or change the port number from firebase,json -1. Add a Change Password Screen. The Flutter Fire package does not have this screen. -2. TODO: Add ReCaptcha to login flow for password authentication for Web only - * Phone Auth on Web has a ReCaptcha already [https://firebase.flutter.dev/docs/auth/phone/]. Tried to use that library but it is very cryptic. - * Use the following instead [https://stackoverflow.com/questions/60675575/how-to-implement-recaptcha-into-a-flutter-app] or [https://medium.com/cloudcraftz/securing-your-flutter-web-app-with-google-recaptcha-b556c567f409] or [https://pub.dev/packages/g_recaptcha_v3] -3. TODO: Ensure Reset Password has Email verification -4. TODO: Add Phone verification [https://firebase.google.com/docs/auth/flutter/phone-auth] - * See [https://github.com/firebase/flutterfire/issues/4189]. -5. TODO: Add 2FA with SMS Pin. This screen is available in the Flutter Fire package -Step 8: Add Firebase Emulator to test on laptop instead of server so that we can use Functions without upgrading the plan to Blaze. See [https://firebase.google.com/docs/emulator-suite/install_and_configure] +The emulator allows you to simulate Firebase services locally, such as Firestore, Authentication, and more. -Step 9: Add User Roles using Custom Claims. This requires upgrade of plan as we need to use Firebase Functions. Instead using Emulator. +# User Flows -1. In Emulator we can add user via http://127.0.0.1:4000/auth and add custom claim via UI as {"role":"admin"}. The effect is shown via Product page in Nav instead of Cart page for admin user. -2. Add Function to add the custom claim to make the first user the admin using the Admin SDK -3. Registeration form to collect some data post signUp and enforce email verification from Client side. +The app includes four distinct user flows: - * Note! for Emulator check the console to verify using the link provided as email is not sent. -4. Enforcing verify email using a button which appears when SignIn fails due to non verification. +## Guest Flow : +1. **Guest**: Explore the app without logging in. Guests can browse products, view product details, and see limited features. +
+ Guest Flow Step 1 + ➡️ + Guest Flow Step 2 + ➡️ + Guest Flow Step 3 + ➡️ + Guest Flow Step 4 +
- * Fixed the error handling message during login. - * Coverted server side to Typescript - * Enabled Resend verification mail button - * Approach 1 - Use Email Link Authentication and signIn, assuming it marks email as verified also. We cannot send the verification mail as is, since that can be sent only if signed in (which was allowed only for first login post signup) - * Refer https://firebase.google.com/docs/auth/flutter/email-link-auth - * TODO Enable Deep Linking: According to https://firebase.google.com/docs/dynamic-links/flutter/receive, the Flutter feature is being deprecated and we should use the AppLinks (Android), UniversalLinks(iOS) instead. Leaving this for future as adding complexity. - * We could use the server side handling instead of deep linking. See [https://firebase.google.com/docs/auth/custom-email-handler?hl=en&authuser=0#web]. However, this requires changing the email template for the URL which is not possible in Emulator. Using the continueURL instead does not work as oobCode is already expired. This handling also uses the web client sdk. Thus it is better to go with the below method instead. - * Approach 2 - (Hack) send a create request with suffix ".verify" added in email when button clicked. Use the server side beforeCreate to catch this and send verification mail - * Note that the Server side beforeCreate function can also bypass user creation till verification but the user record (esp password) needs to be stored anyways, so bypassing user creation is not a good idea. Instead, we should use the verified tag in subsequent processing - * Sending emails from server side is possible but by using the web client SDK. -5. TODO: Other Items - * TODO: Using autocomplete for emails is throwing error log in terminal. No impact but need to fix when all is done. - * TODO: Add a job that removes all unverified users after a while automatically. This could be useful if you were victim of bot attacks, but adding the Recaptcha is better precaution. See [https://stackoverflow.com/questions/67148672/how-to-delete-unverified-e-mail-addresses-in-firebase-authentication-flutter/67150606#67150606] -6. Added Roles of Buyer and Seller. - 1. Added Access level in increasing order of role order => Buyer then Seller then Admin - 2. Created Navigation for each of Admin, Buyer, Seller screens - 3. Allowed switch from lower role Navigation to Navigation view till the given role of the user -Step 10: Firebase Remote Config for A/B testing. See [https://firebase.google.com/docs/remote-config] -1. Complete the Screen enum based Navigation framework -2. Config useBottomSheetForProfileOptions for Navigation element to be one of the following - * False: Drawer for Account, Settings, Sign Out - * True: Hamburger that opens BottomSheet (Context Menu in larger screen) for the same options -3. TODO: Config for adding Search Bar at the Top vs a Bottom Navigation button -Step 11: TODO: CRUD -* Users request role upgrade -* Add this request data to Firebase Datastore -* Create ListView with slidable tiles for approvals -* Admin SDK used by admin user via workflow on this request data and is approved from app - * Allow a Plan attribute via Custome Claims (e.g. Premium user flag) for Buyer and Seller, to add features which are not Navigation linked. Add a button Upgrade to Plan in Drawer that leads to Payment screen. Also certain aspects of premium plan can be visible that leads to upgrade plan screen via middleware -* Nested Category, Sub-Category tree creation -Step 12: TODO: Theming and Custom Settings -* Add Persona (like that in Netflix) and create a Persona selection only for Buyer Role -* Add Minimal (Three Color Gradient Pallette) Material Theme. Align it with Persona Templates (like Kids Template in Netflix) -* Dark theme toggle setting based on each Persona of the logged in User +## Buyer Flow : +2. **Buyer**: Registered users who can add products to their cart, make purchases, and track orders. +Sure, here's how you can display the images in a Markdown format with three images in a row, along with proper text captions below each image: + + + +### Result + +| ![Enter Number](https://github.com/user-attachments/assets/5a7925c1-c41c-4627-9006-115b36cba4f8) | ![Save Address](https://github.com/user-attachments/assets/f3dab607-82b1-48a7-91b6-9e8e1df2a0b1) | ![Add Address](https://github.com/user-attachments/assets/ec0d40eb-2f2c-452d-b58f-518dca7671be) | +|:---:|:---:|:---:| +| **Enter Number** | **Enter Otp** | **Home Screen** | + +| ![Multiple Addresses](https://github.com/user-attachments/assets/384205c5-3a2a-4c4a-b8b7-853d1d5d1fa7) | ![Checkout on Cart](https://github.com/user-attachments/assets/3b527eb7-4cfd-4279-b6b7-4e383517cd48) | ![Select Address on Cart](https://github.com/user-attachments/assets/3bb179f0-2d2b-4688-b9c1-2adae56ef477) | +|:---:|:---:|:---:| +| **Categories Screen** | **Products based on category** | **All Products** | + +| ![Select Payment Method](https://github.com/user-attachments/assets/2ff0921c-e77a-4d48-8a16-4ba96ce0bdb4) | ![Order Confirm Screen](https://github.com/user-attachments/assets/b0d2f159-aa17-4f45-b755-c385536b1b29) | ![Order Placed Notification](https://github.com/user-attachments/assets/32af3f70-8687-443d-a149-1f1ceeac9251) | +|:---:|:---:|:---:| +| **Products added to cart** | **Product detail Page** | **Add address to go on cart** | + +| ![Order Section](https://github.com/user-attachments/assets/c22a6e77-3a33-4232-bd18-8c248bdb4ede) | ![Profile Section](https://github.com/user-attachments/assets/05c906fb-58e3-4655-a186-b4edfee4aa11) | ![Account Details Section](https://github.com/user-attachments/assets/ba2f173c-3994-422b-b469-71fbd91bb229) | +|:---:|:---:|:---:| +| **Add address** | **Manage Address** | **Checkout Page** | + +| ![Support Section](https://github.com/user-attachments/assets/898cb342-9a19-464c-8e93-59ba92b06a9a) | ![Past Queries](https://github.com/user-attachments/assets/95f9f48c-e88b-4538-b9e1-3c138e5f936c) | ![Search from Home](https://github.com/user-attachments/assets/515a4901-f998-4b52-889b-948f7c58607d) | +|:---:|:---:|:---:| +| **Select Address** | **Select Payment Mwthod** | **Order Confirm Screen** | + + +| ![Order Confirm Screen](https://github.com/user-attachments/assets/0ca3a516-e7f5-44d4-8c5d-d224075413ce) | ![Notification on Order Place](https://github.com/user-attachments/assets/65600110-6ffb-48f9-a7d8-7f70d844f8bf) | ![Order Section with Search and Filter Working](https://github.com/user-attachments/assets/3bb04557-98a1-49a4-9173-0a4bee2b7a53) | +|:---:|:---:|:---:| +| **Notification on Order Place** | **Order Section with Search and Filter Working** | **Order Section Example 1** | + +| ![Order Section Example 1](https://github.com/user-attachments/assets/3457586f-6fe9-4ca1-89a5-7d27f40ab942) | ![Order Section Example 2](https://github.com/user-attachments/assets/f57aff96-1b9a-443b-a3d5-093c2c01f99e) | ![Profile Section](https://github.com/user-attachments/assets/88eb3632-7ecb-4dc6-be3f-e3677a38bcfe) | +|:---:|:---:|:---:| +| **Order Section Search** | **Profile Section** | **Account details edit** | +| ![Account Details Section](https://github.com/user-attachments/assets/507f8df1-1ea1-4a74-91fb-90b2abc37047) | ![Support Section](https://github.com/user-attachments/assets/d3c3f492-8ab5-401f-9c4d-af2e51fc7cb4) | ![Past Queries Section](https://github.com/user-attachments/assets/6e454140-52e0-4dcf-b119-e7617a57c9e1) | +|:---:|:---:|:---:| +| **Suport Section** | **Past Queries Section** | **Search for Home** | + + +## Seller Flow : +3. **Seller**: Users who can list products for sale, manage inventory, and fulfill orders and he can also act as Buyer + + + + + + + + + + + + + + + + + + + + + +
Seller Profile PageSeller Section of AppAdd Product Section
Seller Profile PageSeller Section of AppAdd Product Section
Edit Product SectionApproval Tick on Appbar
Edit Product SectionApproval Tick on Appbar
+ + + + + + + + + + + +## Admin Flow : +4. **Admin**: Admins have access to manage users, oversee transactions, and monitor app activities and He can also do stuff as a Buyer flow + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
User Added Business DetailsRequest Went to AdminAdmin Screen
User Added Business DetailsRequest Went to AdminAdmin Screen
Upload BannersMake Banners Active/InactiveAdd Categories
Upload BannersMake Banners Active/InactiveAdd Categories
Manage CategoriesHome Screen and Rest Features
Manage CategoriesHome Screen and Rest Features
+ + + + + + + + + + + + + +### Optional: Use Your Data or Start Fresh + +To run the app with your data using the Firebase Emulator, or to set up a fresh Firebase project: + +- **Option A: Use Emulator with Your Data** + - If you have a pre-existing set of data you want to use, you can import it by running: + ```bash + firebase emulators:start --import=./emulator-data + ``` + - Make sure your data files are in the `./emulator-data` directory to be loaded by the emulator. + +- **Option B: Set Up a Fresh Firebase Project** + - If you prefer to set up a fresh Firebase project: + 1. Go to the [Firebase Console](https://console.firebase.google.com/) and create a new project. + 2. Add your desired Firebase services (Firestore, Authentication, etc.). + 3. Download the `google-services.json` file and place it in the `android/app` directory. + 4. Manually add initial data to your Firebase project using the Firebase Console. -Step 13: TODO: Large vs Small screen responsiveness -* Drawer: Triggered by Top Left Icon (App Logo). For iOS this icon changes to back button when required. Contains allowed Role List, Screens specified as Drawer. Becomes Left Side Navigation for Horizontal Screens. Can have additional extreme left vertical Navigation Strip. Bottom Navigation Bar also folds into this strip in Horizontal Screens. -* Top Right Icon: used for Login and post Login triggers BottomSheet/Context Menu for Persona Change, Profile, Settings, Change Password, Logout -* Search Bar (Toggle Button for phones) on Top Center with Title -* Status Bottom Bar for desktops only instead of SnackBars -* FAB vs Main Menu -Step 14: TODO: Make own login flow screens. Remove firebase library reference from all but auth_service diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 00000000..8745d084 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "457578932874", + "project_id": "myfluttertask", + "storage_bucket": "myfluttertask.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:457578932874:android:c9bb9ed77dbb59d9a29b73", + "android_client_info": { + "package_name": "com.example.get_flutter_fire" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDd_koc2RlY4YAfR3QuHoF7oI336ZrNrTQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/get_flutter_fire/MainActivity.kt b/android/app/src/main/kotlin/com/example/get_flutter_fire/MainActivity.kt new file mode 100644 index 00000000..018e286b --- /dev/null +++ b/android/app/src/main/kotlin/com/example/get_flutter_fire/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.get_flutter_fire + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..c704f54b --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + 10.0.2.2 + + diff --git a/assets/animations/loader.gif b/assets/animations/loader.gif new file mode 100644 index 00000000..afeb11b6 Binary files /dev/null and b/assets/animations/loader.gif differ diff --git a/assets/icons/icon_arrow_go.png b/assets/icons/icon_arrow_go.png new file mode 100644 index 00000000..8264361b Binary files /dev/null and b/assets/icons/icon_arrow_go.png differ diff --git a/assets/icons/icon_cart.png b/assets/icons/icon_cart.png new file mode 100644 index 00000000..36aa375f Binary files /dev/null and b/assets/icons/icon_cart.png differ diff --git a/assets/icons/icon_cart_checkout.png b/assets/icons/icon_cart_checkout.png new file mode 100644 index 00000000..4f4f3c08 Binary files /dev/null and b/assets/icons/icon_cart_checkout.png differ diff --git a/assets/icons/icon_category.png b/assets/icons/icon_category.png new file mode 100644 index 00000000..88135a73 Binary files /dev/null and b/assets/icons/icon_category.png differ diff --git a/assets/icons/icon_check_circle.png b/assets/icons/icon_check_circle.png new file mode 100644 index 00000000..63b5681d Binary files /dev/null and b/assets/icons/icon_check_circle.png differ diff --git a/assets/icons/icon_chevron_left.png b/assets/icons/icon_chevron_left.png new file mode 100644 index 00000000..44dfd9cb Binary files /dev/null and b/assets/icons/icon_chevron_left.png differ diff --git a/assets/icons/icon_chevron_right.png b/assets/icons/icon_chevron_right.png new file mode 100644 index 00000000..e54751cc Binary files /dev/null and b/assets/icons/icon_chevron_right.png differ diff --git a/assets/icons/icon_file.png b/assets/icons/icon_file.png new file mode 100644 index 00000000..d5fa5032 Binary files /dev/null and b/assets/icons/icon_file.png differ diff --git a/assets/icons/icon_history.png b/assets/icons/icon_history.png new file mode 100644 index 00000000..7869a36c Binary files /dev/null and b/assets/icons/icon_history.png differ diff --git a/assets/icons/icon_home.png b/assets/icons/icon_home.png new file mode 100644 index 00000000..99c3889e Binary files /dev/null and b/assets/icons/icon_home.png differ diff --git a/assets/icons/icon_location.png b/assets/icons/icon_location.png new file mode 100644 index 00000000..bd7b4b1f Binary files /dev/null and b/assets/icons/icon_location.png differ diff --git a/assets/icons/icon_mail.png b/assets/icons/icon_mail.png new file mode 100644 index 00000000..4bd2d309 Binary files /dev/null and b/assets/icons/icon_mail.png differ diff --git a/assets/icons/icon_notification.png b/assets/icons/icon_notification.png new file mode 100644 index 00000000..3ba638dc Binary files /dev/null and b/assets/icons/icon_notification.png differ diff --git a/assets/icons/icon_order.png b/assets/icons/icon_order.png new file mode 100644 index 00000000..4235462c Binary files /dev/null and b/assets/icons/icon_order.png differ diff --git a/assets/icons/icon_order_delivered.png b/assets/icons/icon_order_delivered.png new file mode 100644 index 00000000..9ac6d7ab Binary files /dev/null and b/assets/icons/icon_order_delivered.png differ diff --git a/assets/icons/icon_order_dispatched.png b/assets/icons/icon_order_dispatched.png new file mode 100644 index 00000000..613bf845 Binary files /dev/null and b/assets/icons/icon_order_dispatched.png differ diff --git a/assets/icons/icon_order_placed.png b/assets/icons/icon_order_placed.png new file mode 100644 index 00000000..d69a3e32 Binary files /dev/null and b/assets/icons/icon_order_placed.png differ diff --git a/assets/icons/icon_order_shipped.png b/assets/icons/icon_order_shipped.png new file mode 100644 index 00000000..d76ad83c Binary files /dev/null and b/assets/icons/icon_order_shipped.png differ diff --git a/assets/icons/icon_payment.png b/assets/icons/icon_payment.png new file mode 100644 index 00000000..26a072d1 Binary files /dev/null and b/assets/icons/icon_payment.png differ diff --git a/assets/icons/icon_phone.png b/assets/icons/icon_phone.png new file mode 100644 index 00000000..f3667bf7 Binary files /dev/null and b/assets/icons/icon_phone.png differ diff --git a/assets/icons/icon_profile.png b/assets/icons/icon_profile.png new file mode 100644 index 00000000..b39cbbd9 Binary files /dev/null and b/assets/icons/icon_profile.png differ diff --git a/assets/icons/icon_search.png b/assets/icons/icon_search.png new file mode 100644 index 00000000..57158337 Binary files /dev/null and b/assets/icons/icon_search.png differ diff --git a/assets/icons/icon_share.png b/assets/icons/icon_share.png new file mode 100644 index 00000000..83afc77f Binary files /dev/null and b/assets/icons/icon_share.png differ diff --git a/assets/icons/icon_signout.png b/assets/icons/icon_signout.png new file mode 100644 index 00000000..f0a3f22a Binary files /dev/null and b/assets/icons/icon_signout.png differ diff --git a/assets/icons/icon_support.png b/assets/icons/icon_support.png new file mode 100644 index 00000000..2a9d83bc Binary files /dev/null and b/assets/icons/icon_support.png differ diff --git a/assets/icons/icon_whatsapp.png b/assets/icons/icon_whatsapp.png new file mode 100644 index 00000000..0696fa9b Binary files /dev/null and b/assets/icons/icon_whatsapp.png differ diff --git a/assets/icons/loader.gif b/assets/icons/loader.gif new file mode 100644 index 00000000..afeb11b6 Binary files /dev/null and b/assets/icons/loader.gif differ diff --git a/assets/icons/sheru.png b/assets/icons/sheru.png new file mode 100644 index 00000000..f6ba199c Binary files /dev/null and b/assets/icons/sheru.png differ diff --git a/assets/images/main_image.jpg b/assets/images/main_image.jpg new file mode 100644 index 00000000..c5c8fd0e Binary files /dev/null and b/assets/images/main_image.jpg differ diff --git a/database.rules.json b/database.rules.json new file mode 100644 index 00000000..d525e391 --- /dev/null +++ b/database.rules.json @@ -0,0 +1,7 @@ +{ + "rules": { + ".read": "auth != null", + ".write": "auth != null" + } + } + \ No newline at end of file diff --git a/emulator-data/auth_export/accounts.json b/emulator-data/auth_export/accounts.json new file mode 100644 index 00000000..104d41d5 --- /dev/null +++ b/emulator-data/auth_export/accounts.json @@ -0,0 +1 @@ +{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"kWYb11Imn6UOEDReGKFqpJK9L3eu","phoneNumber":"+919876543210","lastLoginAt":"1725098006915","createdAt":"1725098006915","providerUserInfo":[{"providerId":"phone","phoneNumber":"+919876543210","rawId":"+919876543210"}],"lastRefreshAt":"2024-08-31T09:53:26.919Z"},{"localId":"ujWYgsLa2Z7nEyJ9198Z65Czspzz","phoneNumber":"+919324366823","lastLoginAt":"1725101161868","createdAt":"1725095059287","providerUserInfo":[{"providerId":"phone","phoneNumber":"+919324366823","rawId":"+919324366823"}],"lastRefreshAt":"2024-08-31T10:46:01.869Z"}]} \ No newline at end of file diff --git a/emulator-data/auth_export/config.json b/emulator-data/auth_export/config.json new file mode 100644 index 00000000..8f77af98 --- /dev/null +++ b/emulator-data/auth_export/config.json @@ -0,0 +1 @@ +{"signIn":{"allowDuplicateEmails":false}} \ No newline at end of file diff --git a/emulator-data/firebase-export-metadata.json b/emulator-data/firebase-export-metadata.json new file mode 100644 index 00000000..04aaf83c --- /dev/null +++ b/emulator-data/firebase-export-metadata.json @@ -0,0 +1,16 @@ +{ + "version": "13.0.2", + "firestore": { + "version": "1.18.2", + "path": "firestore_export", + "metadata_file": "firestore_export/firestore_export.overall_export_metadata" + }, + "auth": { + "version": "13.0.2", + "path": "auth_export" + }, + "storage": { + "version": "13.0.2", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata b/emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata new file mode 100644 index 00000000..fe1295b6 Binary files /dev/null and b/emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata differ diff --git a/emulator-data/firestore_export/all_namespaces/all_kinds/output-0 b/emulator-data/firestore_export/all_namespaces/all_kinds/output-0 new file mode 100644 index 00000000..1aac7f4a Binary files /dev/null and b/emulator-data/firestore_export/all_namespaces/all_kinds/output-0 differ diff --git a/emulator-data/firestore_export/firestore_export.overall_export_metadata b/emulator-data/firestore_export/firestore_export.overall_export_metadata new file mode 100644 index 00000000..853a60f5 Binary files /dev/null and b/emulator-data/firestore_export/firestore_export.overall_export_metadata differ diff --git a/emulator-data/storage_export/buckets.json b/emulator-data/storage_export/buckets.json new file mode 100644 index 00000000..04e6a7af --- /dev/null +++ b/emulator-data/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "myfluttertask.appspot.com" + } + ] +} \ No newline at end of file diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..0df57a44 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"emulators":{"auth":{"port":9099},"functions":{"port":5001},"ui":{"enabled":true},"singleProjectMode":true,"firestore":{"port":8080},"storage":{"port":9199}},"storage":{"rules":"y"},"firestore":{"rules":"firestore.rules","indexes":"firestore.indexes.json"},"flutter":{"platforms":{"android":{"default":{"projectId":"myfluttertask","appId":"1:457578932874:android:c9bb9ed77dbb59d9a29b73","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"myfluttertask","configurations":{"android":"1:457578932874:android:c9bb9ed77dbb59d9a29b73","ios":"1:457578932874:ios:ca308e3a48d00b48a29b73","macos":"1:457578932874:ios:ca308e3a48d00b48a29b73","web":"1:457578932874:web:8620f40ec57e379ca29b73","windows":"1:457578932874:web:4fc793749a5ebd5ca29b73"}}}}},"functions":[{"source":"functions","codebase":"default","ignore":["node_modules",".git","firebase-debug.log","firebase-debug.*.log"],"predeploy":["npm --prefix \"$RESOURCE_DIR\" run build"]}]} \ No newline at end of file diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 00000000..ba401f4f --- /dev/null +++ b/firestore.rules @@ -0,0 +1,9 @@ +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if true; + } + } +} diff --git a/functions/.gitignore b/functions/.gitignore new file mode 100644 index 00000000..65b4c06e --- /dev/null +++ b/functions/.gitignore @@ -0,0 +1,9 @@ +# Compiled JavaScript files +lib/**/*.js +lib/**/*.js.map + +# TypeScript v1 declaration files +typings/ + +# Node.js dependency directory +node_modules/ diff --git a/functions/package-lock.json b/functions/package-lock.json new file mode 100644 index 00000000..95557e4f --- /dev/null +++ b/functions/package-lock.json @@ -0,0 +1,5929 @@ +{ + "name": "functions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^12.4.0", + "firebase-functions": "^4.9.0" + }, + "devDependencies": { + "firebase-functions-test": "^3.1.0", + "typescript": "^4.9.0" + }, + "engines": { + "node": "18" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/@babel/generator": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "peer": true + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "peer": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", + "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "peer": true + }, + "node_modules/@fastify/busboy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.0.0.tgz", + "integrity": "sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==" + }, + "node_modules/@firebase/component": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.8.tgz", + "integrity": "sha512-LcNvxGLLGjBwB0dJUsBGCej2fqAepWyBubs4jt1Tiuns7QLbXHuyObZ4aMeBjZjWx4m8g1LoVI9QFpSaq/k4/g==", + "dependencies": { + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.7.tgz", + "integrity": "sha512-wjXr5AO8RPxVVg7rRCYffT7FMtBjHRfJ9KMwi19MbOf0vBf0H9YqW3WCgcnLpXI6ehiUcU3z3qgPnnU0nK6SnA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.7.tgz", + "integrity": "sha512-R/3B+VVzEFN5YcHmfWns3eitA8fHLTL03io+FIoMcTYkajFnrBdS3A+g/KceN9omP7FYYYGTQWF9lvbEx6eMEg==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/database": "1.0.7", + "@firebase/database-types": "1.0.4", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.4.tgz", + "integrity": "sha512-mz9ZzbH6euFXbcBo+enuJ36I5dR5w+enJHHjy9Y5ThCdKUseqfDjW3vCp1YxE9zygFCSjJJ/z1cQ+zodvUcwPQ==", + "dependencies": { + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.9.7" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.7.tgz", + "integrity": "sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.9.0.tgz", + "integrity": "sha512-c4ALHT3G08rV7Zwv8Z2KG63gZh66iKdhCBeDfCpIkLrjX6EAjTD/szMdj14M+FnQuClZLFfW5bAgoOjfNmLtJg==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.12.1.tgz", + "integrity": "sha512-Z3ZzOnF3YKLuvpkvF+TjQ6lztxcAyTILp+FjKonmVpEwPa9vFvxpZjubLR4sB6bf19i/8HL2AXRjA0YFgHFRmQ==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.1.tgz", + "integrity": "sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "peer": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "peer": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "peer": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "optional": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "peer": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", + "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "peer": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "optional": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "peer": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "optional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "optional": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "peer": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "peer": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "peer": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "peer": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001655", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz", + "integrity": "sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.0.tgz", + "integrity": "sha512-N1NGmowPlGBLsOZLPvm48StN04V4YvQRL0i6b7ctrVY3epjP/ct7hFLOItz6pDIvRjwpfPxi52a2UWV2ziir8g==", + "dev": true, + "peer": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "peer": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "peer": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "peer": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "peer": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", + "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "dev": true, + "peer": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "optional": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "peer": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-admin": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.4.0.tgz", + "integrity": "sha512-3HOHqJxNmFv0JgK3voyMQgmcibhJN4LQfZfhnZGb6pcONnZxejki4nQ1twsoJlGaIvgQWBtO7rc5mh/cqlOJNA==", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^1.0.2", + "@firebase/database-types": "^1.0.0", + "@types/node": "^22.0.1", + "farmhash-modern": "^1.1.0", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.7.0", + "@google-cloud/storage": "^7.7.0" + } + }, + "node_modules/firebase-functions": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-4.9.0.tgz", + "integrity": "sha512-IqxOEsVAWGcRv9KRGzWQR5mOFuNsil3vsfkRPPiaV1U/ATC27/jbahh4z8I4rW8Xqa6cQE5xqnw0ueyMH7i7Ag==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^10.0.0 || ^11.0.0 || ^12.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-3.3.0.tgz", + "integrity": "sha512-X+OOA34MGrsTimFXTDnWT0psAqnmBkJ85bGCoLMwjgei5Prfkqh3bv5QASnXC/cmIVBSF2Qw9uW1+mF/t3kFlw==", + "dev": true, + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5", + "ts-deepmerge": "^2.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "firebase-functions": ">=4.9.0", + "jest": ">=28.0.0" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "peer": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "optional": true + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/google-auth-library": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.0.tgz", + "integrity": "sha512-Y/eq+RWVs55Io/anIsm24sDS8X79Tq948zVLGaa7+KlJYYqaGwp1YI37w48nzrNi12RgnzMrQD4NzdmCowT90g==", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.0.tgz", + "integrity": "sha512-4fkXSbNy85ikO7mkD5lChLL5UfLnRBvg6z3s3THUJKI6OSbISbufMDE4S/ZH+J3mB9A2FdMXBT/hh7wTvpGAow==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "peer": true + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "optional": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "peer": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "optional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "peer": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "peer": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "peer": true + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "peer": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "peer": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "peer": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "peer": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "peer": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "peer": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "peer": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "peer": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "peer": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "peer": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "peer": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "peer": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "peer": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "peer": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "peer": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "peer": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "peer": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true, + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "peer": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "peer": true + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "peer": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "peer": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "peer": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "peer": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "peer": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "peer": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "peer": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "peer": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/ts-deepmerge": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", + "integrity": "sha512-3phiGcxPSSR47RBubQxPoZ+pqXsEsozLo4G4AlSrsMKTFg9TA3l+3he5BqpUi9wiuDbaHWXH/amlzQ49uEdXtg==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/functions/package.json b/functions/package.json new file mode 100644 index 00000000..b75f118c --- /dev/null +++ b/functions/package.json @@ -0,0 +1,25 @@ +{ + "name": "functions", + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "serve": "npm run build && firebase emulators:start --only functions", + "shell": "npm run build && firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "18" + }, + "main": "lib/index.js", + "dependencies": { + "firebase-admin": "^12.4.0", + "firebase-functions": "^4.9.0" + }, + "devDependencies": { + "firebase-functions-test": "^3.1.0", + "typescript": "^4.9.0" + }, + "private": true +} diff --git a/functions/src/config.ts b/functions/src/config.ts new file mode 100644 index 00000000..d758aa29 --- /dev/null +++ b/functions/src/config.ts @@ -0,0 +1,8 @@ + +import * as admin from "firebase-admin"; + +if (!admin.apps.length) { + admin.initializeApp(); +} + +export { admin }; diff --git a/functions/src/helper/firebaseHelper.ts b/functions/src/helper/firebaseHelper.ts new file mode 100644 index 00000000..8b67a793 --- /dev/null +++ b/functions/src/helper/firebaseHelper.ts @@ -0,0 +1,13 @@ +import { admin } from "../config"; + +export const ordersRef = () => { + return admin.firestore().collection("orders"); +}; + +export const usersRef = () => { + return admin.firestore().collection("users"); +}; + +export const notificationsRef = () => { + return admin.firestore().collection("notifications"); +}; diff --git a/functions/src/helper/getUserHelper.ts b/functions/src/helper/getUserHelper.ts new file mode 100644 index 00000000..7b12d268 --- /dev/null +++ b/functions/src/helper/getUserHelper.ts @@ -0,0 +1,6 @@ +import { usersRef } from "./firebaseHelper"; + +export async function getUser(userID: string) { + const userSnapshot = await usersRef().doc(userID).get(); + return userSnapshot.exists ? userSnapshot.data() : null; +} diff --git a/functions/src/helper/messageHelpers.ts b/functions/src/helper/messageHelpers.ts new file mode 100644 index 00000000..fff55670 --- /dev/null +++ b/functions/src/helper/messageHelpers.ts @@ -0,0 +1,68 @@ +export function createOrderUpdateMessage( + status: string, + username: string, + orderID: string +): string { + const messages: { [key: string]: string } = { + placed: `Exciting news, ${username}! Your order (ID: ${orderID}) has been placed!`, + processed: `Exciting news, ${username}! Your order (ID: ${orderID}) has been processed and is getting ready!`, + shipped: `Great, ${username}! Your order (ID: ${orderID}) is on its way!`, + delivered: `Hooray, ${username}! Your order (ID: ${orderID}) has been delivered! Enjoy your purchase!`, + cancelled: `We're sorry, ${username}. Your order (ID: ${orderID}) has been cancelled. If you have any questions, please contact us.`, + }; + + return ( + messages[status] || + `Hello, ${username}. Your order (ID: ${orderID}) status has changed to ${status}. If you have any questions, please contact us.` + ); +} + +export function createNotificationMessage( + status: string, + tokens: string[], + routeName: string, + parameterID: string +) { + let title = "Order Update!"; + let body = ""; + + switch (status) { + case "placed": + title = "Order Placed!"; + body = "Exciting news! Your order has been placed!"; + break; + case "processed": + title = "Order Processed!"; + body = + "Exciting news! Your order has been processed and is getting ready!"; + break; + case "shipped": + title = "Order Shipped!"; + body = "Great! Your order is on its way!"; + break; + case "delivered": + title = "Order Delivered!"; + body = "Hooray! Your order has been delivered! Enjoy your purchase!"; + break; + case "cancelled": + title = "Order Cancelled"; + body = + "We're sorry. Your order has been cancelled. If you have any questions, please contact us."; + break; + default: + body = `Your order status has changed to ${status}.`; + } + + return { + notification: { + title: title, + body: body, + }, + data: { + routeName: routeName, + parameterID: parameterID, + click_action: "FLUTTER_NOTIFICATION_CLICK", + }, + tokens: tokens, + }; +} diff --git a/functions/src/helper/typeHelper.ts b/functions/src/helper/typeHelper.ts new file mode 100644 index 00000000..9473afa6 --- /dev/null +++ b/functions/src/helper/typeHelper.ts @@ -0,0 +1,34 @@ + +//order type +export interface Order { + id: string; + userID: string; + currentStatus: string; + } + //user type + export interface User { + id: string; + phoneNumber: string; + fcmTokens: string[]; + fullName: string; + } + //notification type + export interface NotificationData { + id: string; + userID: string; + title: string; + body: string; + isRead: boolean; + imageUrl: string | null; + notificationType: string; + url: string; + } + //product type + export interface Product { + id: string; + name_en: string; + images: string[]; + } + + + \ No newline at end of file diff --git a/functions/src/index.ts b/functions/src/index.ts new file mode 100644 index 00000000..c3b9c340 --- /dev/null +++ b/functions/src/index.ts @@ -0,0 +1,13 @@ + +import { onOrderStatusUpdate } from "./onOrderStatusChange"; +import { onProductCreate } from "./onProductCreate"; + +import { onOrderCreate } from "./onOrderCreate"; + +export { + + onOrderCreate, + onOrderStatusUpdate, + onProductCreate, + +}; diff --git a/functions/src/onOrderCreate.ts b/functions/src/onOrderCreate.ts new file mode 100644 index 00000000..2b835f66 --- /dev/null +++ b/functions/src/onOrderCreate.ts @@ -0,0 +1,67 @@ +import * as functions from "firebase-functions"; +import * as admin from "firebase-admin"; + +// Check if the Firebase Admin SDK is already initialized +if (!admin.apps.length) { + admin.initializeApp(); +} + +import { createNotificationMessage } from "./helper/messageHelpers"; +import { getUser } from "./helper/getUserHelper"; +import { notificationsRef } from "./helper/firebaseHelper"; +import { Order, User, NotificationData } from "./helper/typeHelper"; + +export const onOrderCreate = functions.firestore + .document("orders/{orderId}") + .onCreate(async (snapshot, context) => { + const order = snapshot.data() as Order; + const user = (await getUser(order.userID)) as User; + + if (!user) { + console.error(`User with ID ${order.userID} not found`); + return; + } + + await Promise.all([ + sendOrderPlacedNotification(order, context.eventId, user), + ]); + }); + +async function sendOrderPlacedNotification( + order: Order, + eventID: string, + user: User +) { + try { + // Check if fcmTokens is defined and is an array + if (Array.isArray(user.fcmTokens) && user.fcmTokens.length > 0) { + const message = createNotificationMessage( + order.currentStatus, + user.fcmTokens, + "orderDetail", + order.id + ); + + const notificationData: NotificationData = { + id: eventID, + userID: user.id, + title: message.notification.title, + body: message.notification.body, + isRead: false, + imageUrl: null, + notificationType: "order", + url: `/orderDetail?id=${order.id}`, + }; + + await notificationsRef().doc(eventID).set(notificationData); + console.log("Notification data stored in Firestore."); + + const response = await admin.messaging().sendEachForMulticast(message); + console.log("Notification sent successfully:", response); + } else { + console.log("User does not have any FCM tokens."); + } + } catch (error) { + console.error("Error sending order placed notification:", error); + } +} diff --git a/functions/src/onOrderStatusChange.ts b/functions/src/onOrderStatusChange.ts new file mode 100644 index 00000000..7c6a4ac7 --- /dev/null +++ b/functions/src/onOrderStatusChange.ts @@ -0,0 +1,66 @@ +import * as functions from "firebase-functions"; +import { admin } from "./config"; +import { + createNotificationMessage, +} from "./helper/messageHelpers"; +import { getUser } from "./helper/getUserHelper"; +import { notificationsRef } from "./helper/firebaseHelper"; +import { Order, User, NotificationData } from "./helper/typeHelper"; + +export const onOrderStatusUpdate = functions.firestore + .document("orders/{orderId}") + .onUpdate(async (change, context) => { + const newValue = change.after.data() as Order; + const oldValue = change.before.data() as Order; + + if (newValue.currentStatus !== oldValue.currentStatus) { + const user = (await getUser(newValue.userID)) as User; + + if (!user) { + console.error(`User with ID ${newValue.userID} not found`); + return; + } + + await Promise.all([ + sendOrderStatusNotification(newValue, user, context.eventId), + + ]); + } + }); + +async function sendOrderStatusNotification( + order: Order, + user: User, + eventID: string +): Promise { + try { + if (user.fcmTokens.length > 0) { + const message = createNotificationMessage( + order.currentStatus, + user.fcmTokens, + "orderDetail", + order.id + ); + + const notificationData: NotificationData = { + id: eventID, + userID: user.id, + title: message.notification.title, + body: message.notification.body, + isRead: false, + imageUrl: null, + notificationType: "order", + url: `/orderDetail?id=${order.id}`, + }; + + await notificationsRef().doc(eventID).set(notificationData); + console.log("Notification data stored in Firestore."); + + const response = await admin.messaging().sendEachForMulticast(message); + console.log("Notification sent successfully:", response); + } + } catch (error) { + console.error("Error sending order status notification:", error); + } +} + diff --git a/functions/src/onProductCreate.ts b/functions/src/onProductCreate.ts new file mode 100644 index 00000000..55d3b957 --- /dev/null +++ b/functions/src/onProductCreate.ts @@ -0,0 +1,56 @@ +import * as functions from "firebase-functions"; +import { admin } from "./config"; +import { notificationsRef } from "./helper/firebaseHelper"; +import { Product, NotificationData } from "./helper/typeHelper"; + +export const onProductCreate = functions.firestore + .document("products/{productId}") + .onCreate(async (snapshot, context) => { + const product = snapshot.data() as Product; + try { + + await sendProductNotification(product, context.eventId); + } catch (error) { + console.error("Error creating product link:", error); + } + }); + +async function sendProductNotification( + product: Product, + eventId: string +): Promise { + try { + const notificationPayload: admin.messaging.Message = { + notification: { + title: "Exciting News!", + body: `Introducing ${product.name_en} - Your new must-have! Check it out now!`, + imageUrl: product.images[0], + }, + data: { + routeName: "product", + parameterID: product.id, + click_action: "FLUTTER_NOTIFICATION_CLICK", + }, + topic: "new-product", + }; + + const response = await admin.messaging().send(notificationPayload); + console.log("Notification sent:", response); + + const notificationData: NotificationData = { + id: eventId, + userID: "all", + title: "Exciting News!", + body: `Introducing ${product.name_en} - Your new must-have! Check it out now!`, + isRead: false, + imageUrl: product.images[0], + notificationType: "product", + url: `/product?id=${product.id}`, + }; + + await notificationsRef().doc(eventId).set(notificationData); + console.log("Notification data stored in Firestore."); + } catch (error) { + console.error("Error sending product notification:", error); + } +} diff --git a/functions/tsconfig.json b/functions/tsconfig.json new file mode 100644 index 00000000..7ce05d03 --- /dev/null +++ b/functions/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitReturns": true, + "noUnusedLocals": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017" + }, + "compileOnSave": true, + "include": [ + "src" + ] +} diff --git a/hostingFiles/index.html b/hostingFiles/index.html new file mode 100644 index 00000000..1d52c3b0 --- /dev/null +++ b/hostingFiles/index.html @@ -0,0 +1,89 @@ + + + + + + Welcome to Firebase Hosting + + + + + + + + + + + + + + + + + + + +
+

Welcome

+

Firebase Hosting Setup Complete

+

You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!

+ Open Hosting Documentation +
+

Firebase SDK Loading…

+ + + + diff --git a/lib/app/middleware/auth_middleware.dart b/lib/app/middleware/auth_middleware.dart deleted file mode 100644 index 827dd96c..00000000 --- a/lib/app/middleware/auth_middleware.dart +++ /dev/null @@ -1,94 +0,0 @@ -// ignore_for_file: avoid_print -import 'package:get/get.dart'; -import 'package:get_flutter_fire/models/role.dart'; -import 'package:get_flutter_fire/services/auth_service.dart'; -import 'package:get_flutter_fire/app/routes/app_pages.dart'; - -Future loginVerify(bool check, GetNavConfig route, - Future Function(GetNavConfig) redirector) async { - final newRoute = route.location == Routes.LOGIN - ? Routes.LOGIN - : Routes.LOGIN_THEN(route.location); - if (check) { - return GetNavConfig.fromRoute(newRoute); - } - - // Below could be used if the login was happening without verification. - // This will never get reached if server is sending error in login due to non verification - // With customClaims status == "creating", it will reach here for SignUp case only - if (!AuthService.to.isEmailVerified && !AuthService.to.registered.value) { - return GetNavConfig.fromRoute(route.location == Routes.REGISTER - ? Routes.REGISTER - : Routes.REGISTER_THEN(route.location)); - } - - return await redirector(route); -} - -// class EnsureAuthMiddleware extends GetMiddleware { -// @override -// Future redirectDelegate(GetNavConfig route) async { -// // you can do whatever you want here -// // but it's preferable to make this method fast - -// return await loginVerify( -// !AuthService.to.isLoggedInValue, route, super.redirectDelegate); -// } -// } - -class EnsureNotAuthedOrGuestMiddleware extends GetMiddleware { - //AccessLevel.notAuthed - @override - Future redirectDelegate(GetNavConfig route) async { - if (AuthService.to.isLoggedInValue && !AuthService.to.isAnon) { - //NEVER navigate to auth screen, when user is already authed - return GetNavConfig.fromRoute( - AuthService.to.registered.value ? Routes.HOME : Routes.REGISTER); - } - return await super.redirectDelegate(route); - } -} - -class EnsureAuthedAndNotGuestMiddleware extends GetMiddleware { - //AccessLevel.authenticated - @override - Future redirectDelegate(GetNavConfig route) async { - return await loginVerify( - !AuthService.to.isLoggedInValue || AuthService.to.isAnon, - route, - super.redirectDelegate); - } -} - -class EnsureRoleMiddleware extends GetMiddleware { - //AccessLevel.roleBased - Role role; - EnsureRoleMiddleware(this.role); - - @override - Future redirectDelegate(GetNavConfig route) async { - if (!AuthService.to.isLoggedInValue || !AuthService.to.hasRole(role)) { - final newRoute = Routes.LOGIN_THEN(route.location); - return GetNavConfig.fromRoute(newRoute); - } - return await super.redirectDelegate(route); - } -} - -class EnsureAuthOrGuestMiddleware extends GetMiddleware { - //AccessLevel.guest - @override - Future redirectDelegate(GetNavConfig route) async { - // you can do whatever you want here - // but it's preferable to make this method fast - // In this case this is taking human input and is not fast - - if (!AuthService.to.isLoggedInValue) { - bool? value = await AuthService.to.guest(); - if (value != true) { - return GetNavConfig.fromRoute(Routes.LOGIN); - } - } - return await super.redirectDelegate(route); - } -} diff --git a/lib/app/modules/admin/controller/approve_seller_controller.dart b/lib/app/modules/admin/controller/approve_seller_controller.dart new file mode 100644 index 00000000..7db40eed --- /dev/null +++ b/lib/app/modules/admin/controller/approve_seller_controller.dart @@ -0,0 +1,51 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; + +class ApproveSellerController extends GetxController { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + var usersToApprove = [].obs; + var isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + fetchUsersToApprove(); // Fetch users on initialization + } + + Future fetchUsersToApprove() async { + try { + isLoading.value = true; + final querySnapshot = await _firestore + .collection('users') + .where('isBusiness', isEqualTo: true) + .where('userType', isEqualTo: 'buyer') + .get(); + + usersToApprove.value = querySnapshot.docs + .map((doc) => UserModel.fromMap(doc.data())) + .toList(); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch users: $e'); + } finally { + isLoading.value = false; + } + } + + Future approveSeller(UserModel user) async { + try { + await _firestore.collection('users').doc(user.id).update({ + 'userType': 'seller', + }); + + user = user.copyWith(userType: UserType.seller); + usersToApprove.removeWhere((u) => u.id == user.id); // Remove from list + + Get.snackbar('Success', 'User ${user.name} approved as a seller'); + } catch (e) { + Get.snackbar('Error', 'Failed to approve seller: $e'); + } + } +} diff --git a/lib/app/modules/admin/controller/banner_controller.dart b/lib/app/modules/admin/controller/banner_controller.dart new file mode 100644 index 00000000..51ea8cfc --- /dev/null +++ b/lib/app/modules/admin/controller/banner_controller.dart @@ -0,0 +1,128 @@ +import 'dart:io'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:get_flutter_fire/models/banner_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class BannerController extends GetxController { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final FirebaseStorage _firebaseStorage = FirebaseStorage.instance; + final ImagePicker _imagePicker = ImagePicker(); + + final TextEditingController imageUrlController = TextEditingController(); + final TextEditingController productIDController = TextEditingController(); + final RxBool isActive = true.obs; + final RxList banners = [].obs; + final Rxn selectedImage = + Rxn(); // Properly initialize as reactive + + @override + void onInit() { + super.onInit(); + fetchBanners(); // Fetch all banners on initialization + } + + Future pickImage() async { + final XFile? pickedFile = await _imagePicker.pickImage( + source: ImageSource.gallery, + ); + if (pickedFile != null) { + selectedImage.value = + File(pickedFile.path); // Correctly update with .value + } + } + + Future uploadBanner() async { + if (selectedImage.value == null && imageUrlController.text.trim().isEmpty) { + Get.snackbar('Error', 'Please provide an image or URL.', + backgroundColor: AppTheme.colorRed, colorText: AppTheme.colorWhite); + return; + } + + String imageUrl = imageUrlController.text.trim(); + if (selectedImage.value != null) { + imageUrl = await _uploadImageToFirebase(selectedImage.value!); + } + + if (imageUrl.isEmpty || productIDController.text.trim().isEmpty) { + Get.snackbar('Error', 'Please fill all fields to continue.', + backgroundColor: AppTheme.colorRed, colorText: AppTheme.colorWhite); + return; + } + + try { + final banner = BannerModel( + id: _firestore.collection('banners').doc().id, + imageUrl: imageUrl, + productID: productIDController.text.trim(), + isActive: isActive.value, + ); + + await _firestore.collection('banners').doc(banner.id).set(banner.toMap()); + + Get.snackbar('Success', 'Banner uploaded successfully!', + backgroundColor: AppTheme.colorBlue, colorText: AppTheme.colorWhite); + clearFields(); + fetchBanners(); // Refresh banners list after upload + } catch (e) { + Get.snackbar('Error', 'Failed to upload banner: $e', + backgroundColor: AppTheme.colorRed, colorText: AppTheme.colorWhite); + } + } + + Future _uploadImageToFirebase(File image) async { + try { + final String fileName = + 'banners/${DateTime.now().millisecondsSinceEpoch}'; + final UploadTask uploadTask = + _firebaseStorage.ref().child(fileName).putFile(image); + final TaskSnapshot taskSnapshot = await uploadTask; + final String downloadUrl = await taskSnapshot.ref.getDownloadURL(); + return downloadUrl; + } catch (e) { + Get.snackbar('Error', 'Failed to upload image: $e', + backgroundColor: AppTheme.colorRed, colorText: AppTheme.colorWhite); + return ''; + } + } + + Future fetchBanners() async { + try { + final QuerySnapshot snapshot = + await _firestore.collection('banners').get(); + banners.value = snapshot.docs + .map((doc) => BannerModel.fromMap(doc.data() as Map)) + .toList(); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch banners: $e', + backgroundColor: AppTheme.colorRed, colorText: AppTheme.colorWhite); + } + } + + Future toggleBannerStatus(BannerModel banner) async { + try { + await _firestore.collection('banners').doc(banner.id).update({ + 'isActive': !banner.isActive, + }); + banner = banner.copyWith(isActive: !banner.isActive); + banners[banners.indexWhere((b) => b.id == banner.id)] = banner; + update(); // Update the UI + Get.snackbar('Success', 'Banner status updated successfully!', + backgroundColor: AppTheme.colorBlue, colorText: AppTheme.colorWhite); + } catch (e) { + Get.snackbar('Error', 'Failed to update banner status: $e', + backgroundColor: AppTheme.colorRed, colorText: AppTheme.colorWhite); + } + } + + void clearFields() { + imageUrlController.clear(); + productIDController.clear(); + selectedImage.value = null; + isActive.value = true; + update(); + } +} diff --git a/lib/app/modules/admin/controller/category_controller.dart b/lib/app/modules/admin/controller/category_controller.dart new file mode 100644 index 00000000..23e3b154 --- /dev/null +++ b/lib/app/modules/admin/controller/category_controller.dart @@ -0,0 +1,66 @@ +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'dart:io'; + +import 'package:get_flutter_fire/models/category_model.dart'; + +class CategoryController extends GetxController { + final FirebaseFirestore firestore = FirebaseFirestore.instance; + final FirebaseStorage storage = FirebaseStorage.instance; + + RxList categories = [].obs; + + // Fetch all categories from Firestore + Future fetchCategories() async { + try { + final querySnapshot = await firestore.collection('categories').get(); + categories.value = querySnapshot.docs + .map((doc) => CategoryModel.fromMap(doc.data())) + .toList(); + } catch (e) { + if (kDebugMode) { + print('Error fetching categories: $e'); + } + } + } + + // Add a new category to Firestore + Future addCategory(CategoryModel category, {File? imageFile}) async { + try { + String? imageUrl; + if (imageFile != null) { + // Upload image to Firebase Storage and get URL + final ref = storage.ref().child('categories/${category.id}'); + await ref.putFile(imageFile); + imageUrl = await ref.getDownloadURL(); + } else { + imageUrl = category.imageUrl; + } + final newCategory = category.copyWith(imageUrl: imageUrl); + await firestore + .collection('categories') + .doc(newCategory.id) + .set(newCategory.toMap()); + categories.add(newCategory); + } catch (e) { + if (kDebugMode) { + print('Error adding category: $e'); + } + rethrow; + } + } + + // Delete a category from Firestore + Future deleteCategory(String id) async { + try { + await firestore.collection('categories').doc(id).delete(); + categories.removeWhere((category) => category.id == id); + } catch (e) { + if (kDebugMode) { + print('Error deleting category: $e'); + } + } + } +} diff --git a/lib/app/modules/admin/views/add_category.dart b/lib/app/modules/admin/views/add_category.dart new file mode 100644 index 00000000..b08e9e1c --- /dev/null +++ b/lib/app/modules/admin/views/add_category.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/admin/controller/category_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/models/category_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; + +class AddCategoryScreen extends StatefulWidget { + const AddCategoryScreen({super.key}); + + @override + AddCategoryScreenState createState() => AddCategoryScreenState(); +} + +class AddCategoryScreenState extends State { + final CategoryController categoryController = Get.find(); + final TextEditingController nameController = TextEditingController(); + final TextEditingController imageUrlController = TextEditingController(); + File? selectedImage; + + Future _pickImage() async { + final picker = ImagePicker(); + final pickedFile = await picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + setState(() { + selectedImage = File(pickedFile.path); + imageUrlController.clear(); + }); + } + } + + Future _addCategory() async { + if (nameController.text.isNotEmpty) { + final category = CategoryModel( + id: DateTime.now().toString(), + name: nameController.text, + imageUrl: imageUrlController.text, + ); + + try { + Get.dialog( + const Center(child: CircularProgressIndicator()), + barrierDismissible: false, + ); + + await categoryController.addCategory(category, + imageFile: selectedImage); + + Get.back(); + } catch (e) { + Get.back(); + Get.snackbar('Error', 'Failed to add category: $e'); + } + } else { + Get.snackbar('Error', 'Category name is required'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Add New Category'), + ), + body: Padding( + padding: AppTheme.paddingDefault, + child: Column( + children: [ + CustomTextField( + labelText: 'Category Name', + controller: nameController, + ), + const SizedBox(height: 20), + CustomTextField( + labelText: 'Image URL (optional)', + controller: imageUrlController, + ), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: _pickImage, + icon: const Icon(Icons.upload), + label: const Text('Upload Image'), + style: + ElevatedButton.styleFrom(backgroundColor: AppTheme.colorRed), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _addCategory, + style: + ElevatedButton.styleFrom(backgroundColor: AppTheme.colorRed), + child: const Text('Add Category'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/modules/admin/views/admin_banner_list_screen.dart b/lib/app/modules/admin/views/admin_banner_list_screen.dart new file mode 100644 index 00000000..e78f7a90 --- /dev/null +++ b/lib/app/modules/admin/views/admin_banner_list_screen.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/admin/controller/banner_controller.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class AdminBannerListScreen extends StatelessWidget { + const AdminBannerListScreen({super.key}); + + @override + Widget build(BuildContext context) { + final BannerController bannerController = Get.put(BannerController()); + + return Scaffold( + appBar: AppBar( + title: const Text('Manage Banners'), + backgroundColor: AppTheme.colorRed, + ), + body: Obx(() { + if (bannerController.banners.isEmpty) { + return const Center(child: Text('No banners available.')); + } + + return ListView.builder( + itemCount: bannerController.banners.length, + itemBuilder: (context, index) { + final banner = bannerController.banners[index]; + return Card( + margin: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: ListTile( + leading: Image.network( + banner.imageUrl, + width: 50, + height: 50, + fit: BoxFit.cover, + ), + title: Text( + banner.productID, + style: AppTheme.fontStyleDefaultBold, + ), + subtitle: Text( + banner.isActive ? 'Active' : 'Inactive', + style: TextStyle( + color: banner.isActive + ? AppTheme.colorBlue + : AppTheme.colorRed, + ), + ), + trailing: Switch( + value: banner.isActive, + onChanged: (value) { + bannerController.toggleBannerStatus(banner); + }, + activeColor: AppTheme.colorRed, + ), + ), + ); + }, + ); + }), + ); + } +} diff --git a/lib/app/modules/admin/views/admin_screen.dart b/lib/app/modules/admin/views/admin_screen.dart new file mode 100644 index 00000000..81f88d53 --- /dev/null +++ b/lib/app/modules/admin/views/admin_screen.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class AdminScreen extends StatelessWidget { + const AdminScreen({super.key}); + + @override + Widget build(BuildContext context) { + Get.find(); + + final List> adminItems = [ + { + 'imagePath': iconProfile, + 'text': 'Approve Sellers', + 'onTap': () { + // Navigate to the screen where the admin can approve sellers + Get.toNamed(Routes.APPROVE_SELLERS); + } + }, + { + 'imagePath': iconLocation, + 'text': 'Upload Banners', + 'onTap': () { + // Navigate to the screen where the admin can upload banners + Get.toNamed(Routes.UPLOAD_BANNERS); + } + }, + { + 'imagePath': iconFile, + 'text': 'View Categories', + 'onTap': () { + // Navigate to the screen where the admin can add categories + Get.toNamed(Routes.VIEW_CATEGORIES); + } + }, + { + 'imagePath': iconProfile, + 'text': 'check Banners', + 'onTap': () { + // Navigate to the screen where the admin can approve sellers + Get.toNamed(Routes.EDIT_BANNER); + } + }, + ]; + + return Scaffold( + backgroundColor: AppTheme.colorRed, + body: Column( + children: [ + const Spacing(size: AppTheme.spacingMedium), + Expanded( + child: Container( + padding: AppTheme.paddingTiny, + decoration: const BoxDecoration( + color: AppTheme.colorWhite, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + child: Column( + children: [ + Expanded( + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: adminItems.length, + itemBuilder: (context, index) { + final item = adminItems[index]; + return Column( + children: [ + if (index == 0) + const Spacing(size: AppTheme.spacingTiny), + AdminListItem( + imagePath: item['imagePath'], + text: item['text'], + onTap: item['onTap'], + ), + ], + ); + }, + ), + ), + Padding( + padding: AppTheme.paddingDefault, + child: RichText( + text: TextSpan( + style: AppTheme.fontStyleDefaultBold + .copyWith(color: AppTheme.greyTextColor), + children: [ + const TextSpan(text: 'Admin Panel Managed by '), + TextSpan( + text: 'BasedHarsh', + style: AppTheme.fontStyleDefaultBold + .copyWith(color: AppTheme.colorRed), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class AdminListItem extends StatelessWidget { + final String imagePath; + final String text; + final VoidCallback onTap; + + const AdminListItem({ + super.key, + required this.imagePath, + required this.text, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + onTap: onTap, + leading: Image.asset( + imagePath, + width: 30, + height: 30, + color: AppTheme.colorRed, + ), + title: Text( + text, + style: + AppTheme.fontStyleDefaultBold.copyWith(color: AppTheme.colorBlack), + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: AppTheme.greyTextColor, + ), + ); + } +} diff --git a/lib/app/modules/admin/views/approve_seller.dart b/lib/app/modules/admin/views/approve_seller.dart new file mode 100644 index 00000000..623b299f --- /dev/null +++ b/lib/app/modules/admin/views/approve_seller.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/admin/controller/approve_seller_controller.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class ApproveSellerScreen extends StatelessWidget { + const ApproveSellerScreen({super.key}); + + @override + Widget build(BuildContext context) { + final ApproveSellerController controller = + Get.put(ApproveSellerController()); + + return Scaffold( + appBar: AppBar( + title: const Text('Approve Sellers'), + backgroundColor: AppTheme.colorRed, + ), + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.usersToApprove.isEmpty) { + return const Center(child: Text('No sellers pending approval.')); + } + + return ListView.builder( + padding: const EdgeInsets.all(AppTheme.spacingSmall), + itemCount: controller.usersToApprove.length, + itemBuilder: (context, index) { + final user = controller.usersToApprove[index]; + return Card( + margin: + const EdgeInsets.symmetric(vertical: AppTheme.spacingTiny), + shape: RoundedRectangleBorder( + borderRadius: AppTheme.borderRadius, + ), + elevation: 2, + child: ListTile( + leading: CircleAvatar( + backgroundColor: AppTheme.colorBlue, + child: Text( + user.name.substring(0, 1).toUpperCase(), + style: AppTheme.fontStyleDefaultBold + .copyWith(color: AppTheme.colorWhite), + ), + ), + title: Text( + user.name, + style: AppTheme.fontStyleMedium, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: AppTheme.spacingTiny), + Text('Phone: ${user.phoneNumber}', + style: AppTheme.fontStyleSmall), + if (user.email != null) + Text('Email: ${user.email}', + style: AppTheme.fontStyleSmall), + if (user.businessName != null) + Text('Business Name: ${user.businessName}', + style: AppTheme.fontStyleSmall), + if (user.businessType != null) + Text('Business Type: ${user.businessType}', + style: AppTheme.fontStyleSmall), + const SizedBox(height: AppTheme.spacingTiny), + ], + ), + trailing: ElevatedButton( + onPressed: () => controller.approveSeller(user), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.colorRed, + ), + child: const Text('Approve'), + ), + ), + ); + }, + ); + }), + ); + } +} diff --git a/lib/app/modules/admin/views/categories.dart b/lib/app/modules/admin/views/categories.dart new file mode 100644 index 00000000..9b5d24e8 --- /dev/null +++ b/lib/app/modules/admin/views/categories.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/admin/controller/category_controller.dart'; +import 'package:get_flutter_fire/app/modules/admin/views/add_category.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CategoryListScreen extends StatelessWidget { + final CategoryController categoryController = Get.put(CategoryController()); + + CategoryListScreen({super.key}); + + @override + Widget build(BuildContext context) { + categoryController.fetchCategories(); + + return Scaffold( + appBar: AppBar( + title: const Text('Manage Categories'), + ), + body: Obx( + () => ListView.builder( + itemCount: categoryController.categories.length, + itemBuilder: (context, index) { + final category = categoryController.categories[index]; + return ListTile( + title: Text(category.name), + // ignore: unnecessary_null_comparison + leading: category.imageUrl != null + ? Image.network(category.imageUrl, width: 30, height: 30) + : const Icon(Icons.image_not_supported), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + categoryController.deleteCategory(category.id); + }, + ), + ); + }, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Get.to(() => const AddCategoryScreen()); + }, + backgroundColor: AppTheme.colorRed, + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/lib/app/modules/admin/views/upload_banner.dart b/lib/app/modules/admin/views/upload_banner.dart new file mode 100644 index 00000000..6ae45d8e --- /dev/null +++ b/lib/app/modules/admin/views/upload_banner.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/admin/controller/banner_controller.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class AdminBannerUploadScreen extends StatelessWidget { + const AdminBannerUploadScreen({super.key}); + + @override + Widget build(BuildContext context) { + final BannerController bannerController = Get.put(BannerController()); + + return Scaffold( + appBar: AppBar( + title: const Text('Upload Banner'), + backgroundColor: AppTheme.colorRed, + ), + body: Padding( + padding: AppTheme.paddingDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: AppTheme.spacingSmall), + TextField( + controller: bannerController.imageUrlController, + decoration: InputDecoration( + labelText: 'Image URL', + border: AppTheme.textfieldBorder, + ), + ), + const SizedBox(height: AppTheme.spacingSmall), + Center( + child: ElevatedButton( + onPressed: bannerController.pickImage, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.colorBlue, + ), + child: const Text( + 'Upload Image', + style: TextStyle(color: AppTheme.colorWhite), + ), + ), + ), + const SizedBox(height: AppTheme.spacingSmall), + Obx(() => bannerController.selectedImage.value != null + ? Text( + 'Selected image: ${bannerController.selectedImage.value!.path}') + : const Text('No image selected')), + const SizedBox(height: AppTheme.spacingSmall), + TextField( + controller: bannerController.productIDController, + decoration: InputDecoration( + labelText: 'Product ID', + border: AppTheme.textfieldBorder, + ), + ), + const SizedBox(height: AppTheme.spacingSmall), + Obx(() => SwitchListTile( + title: const Text('Active'), + value: bannerController.isActive.value, + onChanged: (value) { + bannerController.isActive.value = value; + }, + activeColor: AppTheme.colorRed, + )), + const SizedBox(height: AppTheme.spacingLarge), + Center( + child: ElevatedButton( + onPressed: bannerController.uploadBanner, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.colorRed, + padding: const EdgeInsets.symmetric( + vertical: AppTheme.spacingDefault, + horizontal: AppTheme.spacingLarge), + ), + child: const Text( + 'Submit', + style: TextStyle( + color: AppTheme.colorWhite, + fontSize: AppTheme.fontSizeMedium), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/modules/auth/bindings/auth_bindings.dart b/lib/app/modules/auth/bindings/auth_bindings.dart new file mode 100644 index 00000000..1329b197 --- /dev/null +++ b/lib/app/modules/auth/bindings/auth_bindings.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class AuthBindings extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => AuthController(), + ); + } +} diff --git a/lib/app/modules/auth/controllers/auth_controller.dart b/lib/app/modules/auth/controllers/auth_controller.dart new file mode 100644 index 00000000..6d9c4e37 --- /dev/null +++ b/lib/app/modules/auth/controllers/auth_controller.dart @@ -0,0 +1,131 @@ +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; +import 'package:get_flutter_fire/constants.dart'; +import 'package:get_flutter_fire/services/get_storage_service.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; + +class AuthController extends GetxController { + final GetStorageService _storageService = GetStorageService(); + + var isLoading = false.obs; + final Rxn _user = Rxn(); + set user(UserModel? value) => _user.value = value; // Add setter for user + + UserModel? get user => _user.value; + + Rxn get currentUser => _user; + + @override + void onInit() { + super.onInit(); + _loadUserData(); + _checkForUserRoleUpdate(); + } + + void _loadUserData() { + final storedUser = _storageService.getUserData(); + if (storedUser != null) { + _user.value = storedUser; + } + } + + Future _checkForUserRoleUpdate() async { + if (_user.value == null) return; + + isLoading.value = true; + try { + var doc = await usersRef.doc(_user.value!.id).get(); + if (doc.exists) { + UserModel updatedUser = UserModel.fromMap(doc.data()!); + if (updatedUser.userType != _user.value!.userType) { + _user.value = updatedUser; + _storageService.saveUserData(updatedUser); + + if (updatedUser.userType == UserType.seller) { + final SellerController sellerController = + Get.put(SellerController()); + await sellerController.onUserRoleChanged(updatedUser); + } + } + } + } catch (error) { + _handleError('Failed to check user role'); + } finally { + isLoading.value = false; + } + } + + Future fetchUserData(String userID) async { + isLoading.value = true; + try { + var doc = await usersRef.doc(userID).get(); + if (doc.exists) { + _user.value = UserModel.fromMap(doc.data()!); + _storageService.saveUserData(_user.value!); + + if (_user.value!.userType == UserType.seller) { + final SellerController sellerController = Get.put(SellerController()); + await sellerController.onUserRoleChanged(_user.value!); + } + } + } catch (error) { + _user.value = null; + _handleError('Failed to fetch user data'); + } finally { + isLoading.value = false; + } + } + + Future registerUser(UserModel user) async { + isLoading.value = true; + try { + await usersRef.doc(user.id).set(user.toMap()); + _user.value = user; + _storageService.saveUserData(user); + } catch (error) { + _handleError('Failed to register user'); + } finally { + isLoading.value = false; + } + } + + // Updated method to set default address + Future updateDefaultAddressID(String addressID) async { + if (_user.value == null) return; + if (kDebugMode) { + print("the addressID here on update default is: $addressID"); + } + try { + await usersRef + .doc(_user.value!.id) + .update({'defaultAddressID': addressID}); + _user.value = _user.value!.copyWith(defaultAddressID: addressID); + _storageService.saveUserData(_user.value!); + } catch (e) { + _handleError('Failed to update user address: $e'); + } + } + + // Updated method to handle user address + Future updateUserAddress(UserModel user, String addressID) async { + try { + await usersRef.doc(user.id).update({'defaultAddressID': addressID}); + _user.value = _user.value!.copyWith(defaultAddressID: addressID); + _storageService.saveUserData(_user.value!); + } catch (e) { + _handleError('Failed to update user address: $e'); + } + } + + void _handleError(String message) { + Get.snackbar('Error', message); + } + + void clearUserData() { + _user.value = null; + + _storageService.clearUserData(); + } +} diff --git a/lib/app/modules/auth/controllers/login_controller.dart b/lib/app/modules/auth/controllers/login_controller.dart new file mode 100644 index 00000000..69bcd32d --- /dev/null +++ b/lib/app/modules/auth/controllers/login_controller.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; + +// ui contollers +class LoginController extends GetxController { + final TextEditingController phoneController = TextEditingController(); + + final isDisabled = true.obs; + final isInitialPosition = true.obs; + final phoneNumber = ''.obs; + + @override + void onInit() { + super.onInit(); + phoneController.addListener(_checkRequiredFields); + + Future.delayed(const Duration(milliseconds: 400), () { + isInitialPosition.value = false; + }); + } + + void _checkRequiredFields() { + final phone = phoneController.text; + + isDisabled.value = phone.length != 10; + phoneNumber.value = phone; + } + + void verifyPhoneNumber(AuthService authService) { + authService.verifyPhoneNumber("+91${phoneController.text}"); + } + + @override + void onClose() { + phoneController.dispose(); + super.onClose(); + } +} diff --git a/lib/app/modules/auth/controllers/otp_controller.dart b/lib/app/modules/auth/controllers/otp_controller.dart new file mode 100644 index 00000000..d7e21104 --- /dev/null +++ b/lib/app/modules/auth/controllers/otp_controller.dart @@ -0,0 +1,83 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_toast.dart'; + +class OtpController extends GetxController { + final AuthController authController = Get.find(); + final AuthService authService = Get.find(); + + var otp = ''.obs; + var isDisabled = true.obs; + var timerSeconds = 30.obs; + var isResendButtonEnabled = false.obs; + + Timer? _resendTimer; + + @override + void onInit() { + super.onInit(); + startTimer(); + } + + void startTimer() { + _resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (timerSeconds.value > 0) { + timerSeconds.value--; + } else { + isResendButtonEnabled.value = true; + _resendTimer?.cancel(); + } + }); + } + + void checkOtpFields() { + isDisabled.value = otp.value.length != 6; + } + + Future submitOtp() async { + if (otp.value.isEmpty || otp.value.length < 6) { + showToast('Please enter OTP'); + return; + } + + showLoader(); + if (kDebugMode) { + print("Phone number: ${authService.phoneNumber}"); + } + + bool success = await authService.verifyOTP(otp.value); + dismissLoader(); + + if (success) { + await authController.fetchUserData(authService.userID); + if (authController.user == null) { + Get.offNamed(Routes.REGISTER, + arguments: {'phoneNumber': authService.phoneNumber}); + } else { + Get.offAllNamed(Routes.ROOT); // to remove screens behind home screens + } + } else { + showToast('Invalid OTP'); + } + } + + void resendCode() { + isResendButtonEnabled.value = false; + timerSeconds.value = 30; + startTimer(); + + // Logic to resend OTP + authService.verifyPhoneNumber(authService.phoneNumber); + } + + @override + void onClose() { + _resendTimer?.cancel(); + super.onClose(); + } +} diff --git a/lib/app/modules/auth/controllers/regster_controller.dart b/lib/app/modules/auth/controllers/regster_controller.dart new file mode 100644 index 00000000..b8eb6534 --- /dev/null +++ b/lib/app/modules/auth/controllers/regster_controller.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/services/notification_service.dart'; + +class RegisterController extends GetxController { + final AuthService authService = Get.find(); + + final nameController = TextEditingController(); + final emailController = TextEditingController(); + final businessNameController = TextEditingController(); + final businessTypeController = TextEditingController(); + final gstNumberController = TextEditingController(); + final panNumberController = TextEditingController(); + + final isBusiness = false.obs; + + void toggleBusiness(bool value) { + isBusiness.value = value; + } + + void registerUser(String phoneNumber) async { + String userID = authService.userID; + + UserModel user = UserModel( + id: userID, + name: nameController.text, + phoneNumber: phoneNumber, + email: emailController.text.isEmpty ? null : emailController.text, + isBusiness: isBusiness.value, + businessName: isBusiness.value ? businessNameController.text : null, + businessType: isBusiness.value ? businessTypeController.text : null, + gstNumber: isBusiness.value ? gstNumberController.text : null, + panNumber: isBusiness.value ? panNumberController.text : null, + userType: UserType.buyer, + defaultAddressID: '', + createdAt: DateTime.now(), + lastSeenAt: DateTime.now(), + fcmTokens: [], + ); + + //to store the token + NotificationService notificationService = NotificationService(); + await notificationService.storeToken(userID); + + // Update the AuthController with the new user + final authController = Get.find(); + authController.registerUser(user).then((_) { + authController.user = user; // Ensure user is set in AuthController + Get.offAllNamed(Routes.ROOT); + }); + } + + @override + void onClose() { + nameController.dispose(); + emailController.dispose(); + businessNameController.dispose(); + businessTypeController.dispose(); + gstNumberController.dispose(); + panNumberController.dispose(); + super.onClose(); + } +} diff --git a/lib/app/modules/auth/views/address_screen.dart b/lib/app/modules/auth/views/address_screen.dart new file mode 100644 index 00000000..46016811 --- /dev/null +++ b/lib/app/modules/auth/views/address_screen.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class AddressScreen extends StatelessWidget { + final AddressController controller = Get.put(AddressController()); + + AddressScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: AppTheme.primaryGradient, + ), + padding: AppTheme.paddingDefault, + child: Center( + child: SingleChildScrollView( + child: Card( + shape: AppTheme.rrShape, + elevation: 10, + shadowColor: AppTheme.colorBlack.withOpacity(0.15), + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Address Details', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingMedium), + CustomTextField( + labelText: 'Address Line 1', + controller: controller.line1Controller, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Address Line 2', + controller: controller.line2Controller, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'City', + controller: controller.cityController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'District', + controller: controller.districtController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Latitude (optional)', + keyboardType: TextInputType.number, + controller: controller.latitudeController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Longitude (optional)', + keyboardType: TextInputType.number, + controller: controller.longitudeController, + ), + const Spacing(size: AppTheme.spacingExtraLarge), + CustomButton( + onPressed: controller.saveAddress, + text: 'Save Address', + isDisabled: false, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/auth/views/login_screen.dart b/lib/app/modules/auth/views/login_screen.dart new file mode 100644 index 00000000..0697ee2e --- /dev/null +++ b/lib/app/modules/auth/views/login_screen.dart @@ -0,0 +1,92 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_phone_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/login_controller.dart'; + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + final authService = Get.find(); + final LoginController loginController = Get.put(LoginController()); + + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingDefault, + child: Stack( + children: [ + Obx(() => AnimatedPositioned( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + top: loginController.isInitialPosition.value + ? -MediaQuery.of(context).size.height * 0.628 + : MediaQuery.of(context).size.height * 0.05, + left: 0, + right: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + logo, + height: 64, + width: 64, + ), + const Spacing(size: AppTheme.spacingLarge), + Text('Enter Number', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + )), + const SizedBox(height: 10), + Obx(() => RichText( + text: TextSpan( + text: 'An OTP will be sent to this number: ', + style: AppTheme.fontStyleSmall.copyWith( + color: AppTheme.greyTextColor, + ), + children: [ + TextSpan( + text: loginController.phoneNumber.value, + style: AppTheme.fontStyleSmall.copyWith( + color: AppTheme.colorRed, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + )), + const SizedBox(height: 10), + PhoneTextField( + hintText: 'Phone Number', + readOnly: false, + controller: loginController.phoneController, + ), + const SizedBox(height: 20), + Obx(() => CustomButton( + onPressed: () { + loginController.verifyPhoneNumber(authService); + if (kDebugMode) { + print( + "The phone number is ${loginController.phoneController.text}"); + } + }, + text: 'Get OTP', + isDisabled: loginController.isDisabled.value)), + ], + ), + )), + ], + ), + ), + ); + } +} diff --git a/lib/app/modules/auth/views/otp_screen.dart b/lib/app/modules/auth/views/otp_screen.dart new file mode 100644 index 00000000..f1b63c9a --- /dev/null +++ b/lib/app/modules/auth/views/otp_screen.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/otp_controller.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class OtpScreen extends StatelessWidget { + final String phoneNumber; + OtpScreen({super.key, required this.phoneNumber}); + + final OtpController otpController = Get.put(OtpController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: AppTheme.paddingDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + logo, + height: 64, + width: 64, + ), + const Spacing(size: AppTheme.spacingMedium), + Text( + 'Enter OTP', + style: AppTheme.fontStyleLarge.copyWith( + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const Spacing(size: AppTheme.spacingSmall), + RichText( + textAlign: TextAlign.left, + text: TextSpan( + children: [ + const TextSpan( + text: + 'An OTP has been sent to your registered mobile number ', + style: AppTheme.fontStyleDefault, + ), + TextSpan( + text: phoneNumber, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorRed, + decoration: TextDecoration.underline, + ), + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingDefault), + PinCodeTextField( + length: 6, + appContext: context, + keyboardType: TextInputType.number, + textStyle: AppTheme.fontStyleDefault, + animationType: AnimationType.fade, + pinTheme: PinTheme( + shape: PinCodeFieldShape.underline, + fieldHeight: 50, + fieldWidth: 40, + inactiveColor: AppTheme.greyTextColor, + activeColor: AppTheme.greyTextColor, + selectedColor: AppTheme.colorRed, + ), + onChanged: (value) { + otpController.otp.value = value; + otpController.checkOtpFields(); + }, + ), + const Spacing(size: AppTheme.spacingTiny), + Obx(() => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + otpController.isResendButtonEnabled.value + ? InkWell( + onTap: otpController.resendCode, + child: Text( + 'Resend OTP', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorRed, + decoration: TextDecoration.underline, + decorationColor: AppTheme.colorRed), + ), + ) + : Row( + children: [ + Text( + 'Resend OTP ', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.greyTextColor, + decoration: TextDecoration.underline, + ), + ), + Obx(() => Text( + 'in ${otpController.timerSeconds.value}s', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + )), + ], + ), + ], + )), + const Spacing(size: AppTheme.spacingLarge), + Obx(() => CustomButton( + isDisabled: otpController.isDisabled.value, + onPressed: otpController.submitOtp, + text: 'Submit', + )), + ], + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/auth/views/register_screen.dart b/lib/app/modules/auth/views/register_screen.dart new file mode 100644 index 00000000..fba805c6 --- /dev/null +++ b/lib/app/modules/auth/views/register_screen.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/regster_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_phone_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class RegisterScreen extends StatelessWidget { + final String phoneNumber; + + RegisterScreen({super.key, required this.phoneNumber}); + + final RegisterController controller = Get.put(RegisterController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: AppTheme.primaryGradient, + ), + padding: AppTheme.paddingDefault, + child: Center( + child: SingleChildScrollView( + child: Card( + shape: AppTheme.rrShape, + elevation: 10, + shadowColor: AppTheme.colorBlack.withOpacity(0.15), + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Register', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingMedium), + CustomTextField( + labelText: 'Full Name', + controller: controller.nameController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Email (optional)', + keyboardType: TextInputType.emailAddress, + controller: controller.emailController, + ), + const Spacing(size: AppTheme.spacingSmall), + PhoneTextField( + hintText: 'Enter your 10-digit mobile number', + readOnly: true, + controller: TextEditingController(text: phoneNumber), + ), + const Spacing(size: AppTheme.spacingMedium), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Are you a Business?', + style: AppTheme.fontStyleMedium), + Obx(() => Switch( + activeColor: Colors.white, + inactiveTrackColor: Colors.white, + inactiveThumbColor: Colors.red, + activeTrackColor: AppTheme.colorRed, + trackOutlineColor: null, + value: controller.isBusiness.value, + onChanged: controller.toggleBusiness, + )), + ], + ), + Obx(() => Visibility( + visible: controller.isBusiness.value, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Business Details', + style: AppTheme.fontStyleDefaultBold.copyWith( + fontSize: 16, + color: AppTheme.colorBlack, + ), + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Business Name', + controller: controller.businessNameController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Business Type', + controller: controller.businessTypeController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'GST Number', + controller: controller.gstNumberController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'PAN Number', + controller: controller.panNumberController, + ), + ], + ), + )), + const Spacing(size: AppTheme.spacingLarge), + CustomButton( + onPressed: () { + controller.registerUser(phoneNumber); + }, + text: 'Register', + isDisabled: false, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/auth/views/welcome_screen.dart b/lib/app/modules/auth/views/welcome_screen.dart new file mode 100644 index 00000000..1933e6a9 --- /dev/null +++ b/lib/app/modules/auth/views/welcome_screen.dart @@ -0,0 +1,131 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class WelcomeScreen extends StatefulWidget { + const WelcomeScreen({super.key}); + + @override + State createState() => _WelcomeScreenState(); +} + +class _WelcomeScreenState extends State { + final isInitialPosition = true.obs; + final AuthController authController = Get.find(); + + @override + void initState() { + super.initState(); + + Future.delayed(const Duration(milliseconds: 400), () { + isInitialPosition.value = false; + }); + } + + @override + Widget build(BuildContext context) { + Get.find(); + + return Scaffold( + body: Stack( + children: [ + Obx(() => AnimatedPositioned( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + top: isInitialPosition.value + ? MediaQuery.of(context).size.height * 0.4 + : MediaQuery.of(context).size.height * 0.2, + left: 0, + right: 0, + child: Center( + child: Image.asset( + logo, + height: 144, + width: 144, + ), + ), + )), + Obx(() => AnimatedPositioned( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + top: isInitialPosition.value + ? MediaQuery.of(context).size.height + : MediaQuery.of(context).size.height * 0.74, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacing(size: AppTheme.spacingLarge), + const Spacing(size: AppTheme.spacingLarge), + CustomButton( + onPressed: () { + Get.toNamed(Routes.LOGIN); + }, + text: 'Get Started', + ), + const SizedBox(height: 20), + Center( + child: Text.rich( + TextSpan( + text: 'New to the app and in doubt? ', + style: const TextStyle( + fontSize: 14, + color: Colors.black54, + ), + children: [ + TextSpan( + text: 'Explore Now', + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + // Set user as guest and navigate to RootView + authController.user = UserModel( + id: 'guest', + name: 'Guest User', + phoneNumber: '', + email: null, + isBusiness: false, + businessName: null, + businessType: null, + gstNumber: null, + panNumber: null, + userType: UserType.guest, + defaultAddressID: '', + createdAt: DateTime.now(), + lastSeenAt: DateTime.now(), + fcmTokens: [], + ); + Get.offAllNamed(Routes.ROOT); + }, + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 40), + ], + ), + ), + )), + ], + ), + ); + } +} diff --git a/lib/app/modules/cart/bindings/cart_binding.dart b/lib/app/modules/cart/bindings/cart_binding.dart deleted file mode 100644 index 009c52ae..00000000 --- a/lib/app/modules/cart/bindings/cart_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/cart_controller.dart'; - -class CartBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => CartController(), - ); - } -} diff --git a/lib/app/modules/cart/controllers/cart_controller.dart b/lib/app/modules/cart/controllers/cart_controller.dart index c938ec4c..32727b16 100644 --- a/lib/app/modules/cart/controllers/cart_controller.dart +++ b/lib/app/modules/cart/controllers/cart_controller.dart @@ -1,23 +1,133 @@ import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/constants.dart'; +import 'package:get_flutter_fire/models/cart_model.dart'; +import 'package:get_flutter_fire/models/order_model.dart'; class CartController extends GetxController { - //TODO: Implement CartController + final Rx _cart = CartModel(items: [], id: '').obs; + CartModel get cart => _cart.value; - final count = 0.obs; - @override - void onInit() { - super.onInit(); + int get totalPrice => _cart.value.items + .fold(0, (total, item) => total + item.price * item.quantity); + + final Rx _pageIndex = 0.obs; + int get pageIndex => _pageIndex.value; + + final Rx _selectedAddress = ''.obs; + String get selectedAddress => _selectedAddress.value; + + final Rx _selectedPaymentMethod = 'cash'.obs; + String get selectedPaymentMethod => _selectedPaymentMethod.value; + + void handlePaymentMethodChange(String paymentMethod) { + _selectedPaymentMethod.value = paymentMethod; + } + + void selectAddress(String addressID) { + _selectedAddress.value = addressID; + } + + void changePageIndex(int newIndex) { + if (newIndex == 1 && _cart.value.itemCount == 0) { + Get.snackbar('Cart Empty', 'Please add some items to your cart'); + return; + } + if (newIndex != _pageIndex.value) { + _pageIndex.value = newIndex; + } + } + + Future fetchCartData(String userID) async { + try { + List cartItems = []; + final snapshot = await firestore.collection('carts').doc(userID).get(); + if (snapshot.exists) { + Map cartData = snapshot.data() as Map; + if (cartData.containsKey('cartItems')) { + List cartItemsData = cartData['cartItems']; + cartItems = cartItemsData + .map((itemData) => CartItem.fromMap(itemData)) + .toList(); + } + } + _cart.value = CartModel(items: cartItems, id: userID); + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching cart data'); + } + } + + bool isProductInCart(String productID) { + return _cart.value.items.any((item) => item.id == productID); + } + + void incrementQuantity(CartItem item) { + int index = _cart.value.items.indexWhere((i) => i.id == item.id); + if (index != -1) { + _cart.update((cart) { + cart!.items[index].quantity++; + }); + syncCartwithDB(); + } } - @override - void onReady() { - super.onReady(); + void decrementQuantity(CartItem item) { + int index = _cart.value.items.indexWhere((i) => i.id == item.id); + if (index != -1) { + _cart.update((cart) { + cart!.items[index].quantity--; + }); + syncCartwithDB(); + } } - @override - void onClose() { - super.onClose(); + void addItem(CartItem item) { + int index = _cart.value.items.indexWhere((i) => i.id == item.id); + if (index != -1) { + _cart.update((cart) { + cart!.items[index].quantity += item.quantity; + }); + } else { + _cart.update((cart) { + cart!.items.add(item); + }); + } + syncCartwithDB(); } - void increment() => count.value++; + void removeItem(CartItem item) { + int index = _cart.value.items.indexWhere((i) => i.id == item.id); + if (index != -1) { + _cart.update((cart) { + cart!.items.removeAt(index); + }); + } + syncCartwithDB(); + } + + Future syncCartwithDB() async { + List> cartItemsData = + _cart.value.items.map((item) => item.toMap()).toList(); + await firestore + .collection('carts') + .doc(_cart.value.id) + .set({'cartItems': cartItemsData, 'id': _cart.value.id}); + } + + void clearCart() { + _cart.value = CartModel(items: [], id: cart.id); + syncCartwithDB(); + } + + Future placeOrder(OrderModel order) async { + try { + await firestore.collection('orders').doc(order.id).set(order.toMap()); + clearCart(); + Get.snackbar('Success', 'Order placed successfully'); + + Get.offAllNamed(Routes.ORDER_CONFIRMED, arguments: order.id); + } catch (e) { + Get.snackbar('Error', 'Failed to place order: $e'); + } + } } diff --git a/lib/app/modules/cart/controllers/order_controller.dart b/lib/app/modules/cart/controllers/order_controller.dart new file mode 100644 index 00000000..b7bed958 --- /dev/null +++ b/lib/app/modules/cart/controllers/order_controller.dart @@ -0,0 +1,66 @@ +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/models/order_model.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class OrderController extends GetxController { + var orders = [].obs; + var filteredOrders = [].obs; + var isLoading = false.obs; + var currentOrder = Rxn(); + + final AuthController authController = Get.find(); + + OrderModel getOrderByID(String id) { + return orders.firstWhere((order) => order.id == id); + } + + Future fetchOrders() async { + if (authController.user == null) { + return; + } + + try { + isLoading.value = true; + final snapshot = await FirebaseFirestore.instance + .collection('orders') + .where('userID', isEqualTo: authController.user!.id) + .get(); + final fetchedOrders = + snapshot.docs.map((doc) => OrderModel.fromMap(doc.data())).toList(); + orders.assignAll(fetchedOrders); + filteredOrders.assignAll(fetchedOrders); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch orders: $e'); + } finally { + isLoading.value = false; + } + } + + Future loadOrder(String id) async { + try { + isLoading.value = true; + await fetchOrders(); + currentOrder.value = getOrderByID(id); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch order: $e'); + } finally { + isLoading.value = false; + } + } + + void filterOrders(String searchQuery, List statusFilter) { + filteredOrders.assignAll(orders.where((order) { + final matchesSearch = order.id.contains(searchQuery.toUpperCase()); + final matchesStatus = statusFilter.isEmpty || + statusFilter.contains(order.currentStatus.name); + return matchesSearch && matchesStatus; + }).toList()); + } + + @override + void onInit() { + super.onInit(); + fetchOrders(); + } +} diff --git a/lib/app/modules/cart/controllers/product_controller.dart b/lib/app/modules/cart/controllers/product_controller.dart new file mode 100644 index 00000000..a7d5db41 --- /dev/null +++ b/lib/app/modules/cart/controllers/product_controller.dart @@ -0,0 +1,29 @@ +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; + +class ProductController extends GetxController { + var products = [].obs; + + ProductModel getProductByID(String id) { + return products.firstWhere((product) => product.id == id); + } + + Future fetchProducts() async { + try { + final snapshot = + await FirebaseFirestore.instance.collection('products').get(); + final fetchedProducts = + snapshot.docs.map((doc) => ProductModel.fromMap(doc.data())).toList(); + products.assignAll(fetchedProducts); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch products: $e'); + } + } + + @override + void onInit() { + super.onInit(); + fetchProducts(); + } +} diff --git a/lib/app/modules/cart/views/cart_root_view.dart b/lib/app/modules/cart/views/cart_root_view.dart new file mode 100644 index 00000000..c35d7272 --- /dev/null +++ b/lib/app/modules/cart/views/cart_root_view.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/checkout_view.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/select_address_view.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/select_payment_method_view.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/models/order_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/utils/get_uuid.dart'; + +class CartRootView extends StatelessWidget { + const CartRootView({super.key}); + + @override + Widget build(BuildContext context) { + final List pages = [ + const CheckoutView(), + const SelectAddressView(), + const SelectPaymentMethodView(), + ]; + + final cartController = Get.find(); + + String getButtonLabel() { + if (cartController.pageIndex == 0) return "Proceed To Checkout"; + if (cartController.pageIndex == 1) return "Select Address"; + if (cartController.pageIndex == 2) return "Confirm Order"; + return ''; + } + + void onBottomButtonPressed() async { + switch (cartController.pageIndex) { + case 0: + if (cartController.cart.itemCount == 0) { + Get.snackbar( + 'Cart Empty', + 'Please add some items to your cart', + ); + return; + } + if (cartController.selectedAddress.isEmpty) { + cartController.selectAddress( + Get.find().user!.defaultAddressID); + } + cartController.changePageIndex(1); + break; + case 1: + cartController.changePageIndex(2); + break; + case 2: + final addressController = Get.find(); + final authController = Get.find(); + + String orderID = getUUID(); + AddressModel address = addressController.addresses.firstWhere( + (element) => element.id == cartController.selectedAddress); + List products = cartController.cart.items + .map((e) => ProductData( + id: e.id, + quantity: e.quantity, + price: e.price, + )) + .toList(); + OrderModel order = OrderModel( + id: orderID, + address: address, + totalPrice: cartController.totalPrice, + couponDiscount: 0, + couponID: '', + createdAt: DateTime.now(), + currentStatus: OrderStatus.placed, + paymentMethod: cartController.selectedPaymentMethod, + statusUpdates: [ + OrderStatusUpdate( + status: OrderStatus.placed, timestamp: DateTime.now()) + ], + totalWeight: 0, + userID: authController.user!.id, + products: products, + ); + cartController.placeOrder(order); + break; + default: + break; + } + } + + return Obx(() => DefaultTabController( + length: 3, + child: Scaffold( + body: Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + TabBar( + onTap: (index) { + if (index <= cartController.pageIndex) { + cartController.changePageIndex(index); + } + }, + indicator: const BoxDecoration(), + labelColor: AppTheme.colorRed, + unselectedLabelColor: Colors.grey, + tabs: List.generate(3, (index) { + return Tab( + icon: Icon( + index == 0 + ? Icons.shopping_cart + : index == 1 + ? Icons.location_on + : Icons.payment, + color: index <= cartController.pageIndex + ? AppTheme.colorRed + : Colors.grey, + ), + text: index == 0 + ? 'Checkout' + : index == 1 + ? 'Address' + : 'Payment', + ); + }), + ), + Expanded( + child: Padding( + padding: AppTheme.paddingSmall, + child: IndexedStack( + index: cartController.pageIndex, + children: pages, + ), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: onBottomButtonPressed, + label: Text(getButtonLabel()), + icon: const Icon(Icons.arrow_forward), + backgroundColor: AppTheme.colorRed, + ), + floatingActionButtonLocation: + FloatingActionButtonLocation.centerFloat, + ), + )); + } +} diff --git a/lib/app/modules/cart/views/cart_view.dart b/lib/app/modules/cart/views/cart_view.dart deleted file mode 100644 index 3e048c79..00000000 --- a/lib/app/modules/cart/views/cart_view.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; -import 'package:get_flutter_fire/app/routes/app_pages.dart'; -import '../../../widgets/screen_widget.dart'; -import '../../../../services/auth_service.dart'; -import '../controllers/cart_controller.dart'; - -class CartView extends GetView { - const CartView({super.key}); - @override - Widget build(BuildContext context) { - return ScreenWidget( - appBar: AppBar( - title: Text('${AuthService.to.userName} Cart'), - centerTitle: true, - ), - body: const Center( - child: Text( - 'CartView is working', - style: TextStyle(fontSize: 20), - ), - ), - screen: screen!, - ); - } -} diff --git a/lib/app/modules/cart/views/checkout_view.dart b/lib/app/modules/cart/views/checkout_view.dart new file mode 100644 index 00000000..20bf54fe --- /dev/null +++ b/lib/app/modules/cart/views/checkout_view.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/home/controllers/home_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CheckoutView extends StatelessWidget { + const CheckoutView({super.key}); + + @override + Widget build(BuildContext context) { + final cartController = Get.find(); + final homeController = Get.find(); + return Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text("Total Items", style: AppTheme.fontStyleDefault), + Text(" (${cartController.cart.itemCount})", + style: AppTheme.fontStyleDefaultBold), + const Spacer(), + IconButton( + onPressed: () { + cartController.clearCart(); + }, + icon: const Icon( + Icons.delete_outline, + color: AppTheme.colorRed, + )), + ], + ), + const SizedBox(height: AppTheme.spacingTiny), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: cartController.cart.items.length, + itemBuilder: (context, index) { + final item = cartController.cart.items[index]; + final product = homeController.products.firstWhere( + (element) => element.id == item.id, + ); + return Padding( + padding: const EdgeInsets.only(bottom: AppTheme.spacingSmall), + child: Container( + decoration: AppTheme.cardDecoration, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () {}, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: AppTheme.borderRadius.topLeft, + bottomLeft: AppTheme.borderRadius.bottomLeft, + ), + child: Image.network( + product.images.first, + height: 152, + width: 146, + fit: BoxFit.cover, + ), + ), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.all( + AppTheme.spacingExtraSmall), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: AppTheme.fontStyleDefault, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "Rs.${item.price}", + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + Container( + width: double.infinity, + decoration: AppTheme.cardDecoration.copyWith( + border: Border.all( + color: AppTheme.colorBlue, + ), + ), + child: Row( + children: [ + InkWell( + onTap: () => cartController + .decrementQuantity(item), + child: Container( + padding: AppTheme.paddingTiny, + child: const Center( + child: Icon(Icons.remove), + ), + ), + ), + Expanded( + child: Container( + padding: AppTheme.paddingTiny, + decoration: const BoxDecoration( + border: Border( + left: BorderSide( + color: AppTheme.colorBlue, + ), + right: BorderSide( + color: AppTheme.colorBlue, + ), + ), + ), + child: Center( + child: Text( + item.quantity.toString(), + style: AppTheme.fontStyleDefault, + ), + ), + ), + ), + InkWell( + onTap: () { + cartController + .incrementQuantity(item); + }, + child: Container( + padding: AppTheme.paddingTiny, + child: const Center( + child: Icon(Icons.add), + ), + ), + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingTiny), + // SecondaryButton( + // label: context.loc.removeItem, + // onPressed: () => _removeItem(item), + // ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ], + )); + } +} diff --git a/lib/app/modules/cart/views/order_confirmed.dart b/lib/app/modules/cart/views/order_confirmed.dart new file mode 100644 index 00000000..66737d5c --- /dev/null +++ b/lib/app/modules/cart/views/order_confirmed.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/order_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/product_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/cart/order_detail_card.dart'; +import 'package:get_flutter_fire/app/widgets/cart/order_summary_widget.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_bottom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderConfirmedScreen extends StatelessWidget { + const OrderConfirmedScreen({super.key}); + + @override + Widget build(BuildContext context) { + final String id = Get.arguments ?? ''; + final OrderController orderController = Get.find(); + final ProductController productController = Get.find(); + + return FutureBuilder( + future: orderController.loadOrder(id), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const LoadingWidget(); + } + + if (snapshot.hasError) { + return Scaffold( + body: Center( + child: Text('Error loading order: ${snapshot.error}'), + ), + ); + } + + final order = orderController.currentOrder.value; + + if (order == null) { + return const Scaffold( + body: Center( + child: Text('Order not found', style: AppTheme.fontStyleLarge), + ), + ); + } + + return SafeArea( + child: Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Order Confirmed", + style: AppTheme.fontStyleLarge), + const Spacing(size: AppTheme.spacingDefault), + Text("${order.createdAt}: #${order.id}", + style: AppTheme.fontStyleMedium), + const Spacing(size: AppTheme.spacingSmall), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: order.products.length, + itemBuilder: (context, index) { + final item = order.products[index]; + final product = + productController.getProductByID(item.id); + + return OrderDetailProductCard( + product: product, + productData: item, + ); + }, + ), + OrderSummaryWidget( + couponDiscount: order.couponDiscount, + couponCode: "None", + priceDiscount: 0, + subTotalPrice: 0, + totalPrice: order.totalPrice, + ), + ], + ), + ), + ), + bottomNavigationBar: Padding( + padding: AppTheme.paddingDefault, + child: CustomBottomButton( + label: "Continue Shopping", + onPressed: () { + Get.offAllNamed(Routes.ROOT); + }, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/app/modules/cart/views/select_address_view.dart b/lib/app/modules/cart/views/select_address_view.dart new file mode 100644 index 00000000..eaac0d0b --- /dev/null +++ b/lib/app/modules/cart/views/select_address_view.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/widgets/cart/select_address_card.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SelectAddressView extends StatelessWidget { + const SelectAddressView({super.key}); + + @override + Widget build(BuildContext context) { + final addressController = Get.find(); + final cartController = Get.find(); + final user = Get.find().user; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Select Address", + style: AppTheme.fontStyleDefaultBold, + ), + const SizedBox(height: AppTheme.spacingSmall), + ListView.builder( + itemCount: addressController.addresses.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + AddressModel address = addressController.addresses[index]; + return Padding( + padding: const EdgeInsets.only(bottom: AppTheme.spacingSmall), + child: Obx(() => SelectAddressCard( + isDefault: user!.defaultAddressID == address.id, + selectedAddressID: cartController.selectedAddress, + address: address, + onSelect: (address) { + cartController.selectAddress(address.id); + }, + )), + ); + }, + ), + // Center( + // child: SecondaryButton( + // label: context.loc.addAddress, + // onPressed: () => context.go(Routes.addAddress), + // ), + // ), + ], + ); + } +} diff --git a/lib/app/modules/cart/views/select_payment_method_view.dart b/lib/app/modules/cart/views/select_payment_method_view.dart new file mode 100644 index 00000000..8e1daefe --- /dev/null +++ b/lib/app/modules/cart/views/select_payment_method_view.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/widgets/cart/payment_selection_card.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SelectPaymentMethodView extends StatelessWidget { + const SelectPaymentMethodView({super.key}); + + @override + Widget build(BuildContext context) { + final cartController = Get.find(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Select Payment", style: AppTheme.fontStyleDefaultBold), + const SizedBox(height: AppTheme.spacingSmall), + Obx( + () => PaymentMethodSelectionCard( + paymentMethod: "cash", + selectedPaymentMethod: cartController.selectedPaymentMethod, + displayText: "Cash On Delivery", + onChanged: cartController.handlePaymentMethodChange, + label: "(free)", + ), + ), + const Spacing(size: AppTheme.spacingExtraSmall), + Obx( + () => PaymentMethodSelectionCard( + paymentMethod: "online", + selectedPaymentMethod: cartController.selectedPaymentMethod, + displayText: "Pay Online", + onChanged: cartController.handlePaymentMethodChange, + ), + ), + ], + ); + } +} diff --git a/lib/app/modules/categories/bindings/categories_binding.dart b/lib/app/modules/categories/bindings/categories_binding.dart deleted file mode 100644 index 06e278c8..00000000 --- a/lib/app/modules/categories/bindings/categories_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/categories_controller.dart'; - -class CategoriesBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => CategoriesController(), - ); - } -} diff --git a/lib/app/modules/categories/controllers/categories_controller.dart b/lib/app/modules/categories/controllers/categories_controller.dart deleted file mode 100644 index 6612e511..00000000 --- a/lib/app/modules/categories/controllers/categories_controller.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:get/get.dart'; - -class CategoriesController extends GetxController { - //TODO: Implement CategoriesController - - final count = 0.obs; - @override - void onInit() { - super.onInit(); - } - - @override - void onReady() { - super.onReady(); - } - - @override - void onClose() { - super.onClose(); - } - - void increment() => count.value++; -} diff --git a/lib/app/modules/categories/views/categories_view.dart b/lib/app/modules/categories/views/categories_view.dart deleted file mode 100644 index 97bfef38..00000000 --- a/lib/app/modules/categories/views/categories_view.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - -import '../controllers/categories_controller.dart'; - -class CategoriesView extends GetView { - const CategoriesView({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('CategoriesView'), - centerTitle: true, - ), - body: const Center( - child: Text( - 'CategoriesView is working', - style: TextStyle(fontSize: 20), - ), - ), - ); - } -} diff --git a/lib/app/modules/checkout/bindings/checkout_binding.dart b/lib/app/modules/checkout/bindings/checkout_binding.dart deleted file mode 100644 index 42202b56..00000000 --- a/lib/app/modules/checkout/bindings/checkout_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/checkout_controller.dart'; - -class CheckoutBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => CheckoutController(), - ); - } -} diff --git a/lib/app/modules/checkout/controllers/checkout_controller.dart b/lib/app/modules/checkout/controllers/checkout_controller.dart deleted file mode 100644 index aa1265f6..00000000 --- a/lib/app/modules/checkout/controllers/checkout_controller.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:get/get.dart'; - -class CheckoutController extends GetxController { - //TODO: Implement CheckoutController - - final count = 0.obs; - @override - void onInit() { - super.onInit(); - } - - @override - void onReady() { - super.onReady(); - } - - @override - void onClose() { - super.onClose(); - } - - void increment() => count.value++; -} diff --git a/lib/app/modules/checkout/views/checkout_view.dart b/lib/app/modules/checkout/views/checkout_view.dart deleted file mode 100644 index b8b17072..00000000 --- a/lib/app/modules/checkout/views/checkout_view.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - -import '../controllers/checkout_controller.dart'; - -class CheckoutView extends GetView { - const CheckoutView({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('CheckoutView'), - centerTitle: true, - ), - body: const Center( - child: Text( - 'CheckoutView is working', - style: TextStyle(fontSize: 20), - ), - ), - ); - } -} diff --git a/lib/app/modules/dashboard/bindings/dashboard_binding.dart b/lib/app/modules/dashboard/bindings/dashboard_binding.dart deleted file mode 100644 index da48f13c..00000000 --- a/lib/app/modules/dashboard/bindings/dashboard_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/dashboard_controller.dart'; - -class DashboardBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => DashboardController(), - ); - } -} diff --git a/lib/app/modules/dashboard/controllers/dashboard_controller.dart b/lib/app/modules/dashboard/controllers/dashboard_controller.dart deleted file mode 100644 index 24d91a16..00000000 --- a/lib/app/modules/dashboard/controllers/dashboard_controller.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:async'; - -import 'package:get/get.dart'; - -class DashboardController extends GetxController { - final now = DateTime.now().obs; - @override - void onReady() { - super.onReady(); - Timer.periodic( - const Duration(seconds: 1), - (timer) { - now.value = DateTime.now(); - }, - ); - } -} diff --git a/lib/app/modules/dashboard/views/dashboard_view.dart b/lib/app/modules/dashboard/views/dashboard_view.dart deleted file mode 100644 index f475030f..00000000 --- a/lib/app/modules/dashboard/views/dashboard_view.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../controllers/dashboard_controller.dart'; - -class DashboardView extends GetView { - const DashboardView({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Obx( - () => Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'DashboardView is working', - style: TextStyle(fontSize: 20), - ), - Text('Time: ${controller.now.value.toString()}'), - ], - ), - ), - ), - ); - } -} diff --git a/lib/app/modules/home/bindings/home_binding.dart b/lib/app/modules/home/bindings/home_binding.dart deleted file mode 100644 index d08a80d4..00000000 --- a/lib/app/modules/home/bindings/home_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/home_controller.dart'; - -class HomeBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => HomeController(), - ); - } -} diff --git a/lib/app/modules/home/controllers/category_filter_controller.dart b/lib/app/modules/home/controllers/category_filter_controller.dart new file mode 100644 index 00000000..20a2a33c --- /dev/null +++ b/lib/app/modules/home/controllers/category_filter_controller.dart @@ -0,0 +1,69 @@ +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; + +class CategoryFilterController extends GetxController { + var products = [].obs; + var searchHistory = [].obs; + + @override + void onInit() { + super.onInit(); + fetchProducts(); + } + + Future fetchProductsByCategory(String categoryId) async { + try { + final snapshot = await FirebaseFirestore.instance + .collection('products') + .where('categoryID', isEqualTo: categoryId) + .get(); + + final fetchedProducts = + snapshot.docs.map((doc) => ProductModel.fromMap(doc.data())).toList(); + products.assignAll(fetchedProducts); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch products: $e'); + } + } + + // Fetch all products from Firestore + Future fetchProducts() async { + try { + final snapshot = + await FirebaseFirestore.instance.collection('products').get(); + final fetchedProducts = + snapshot.docs.map((doc) => ProductModel.fromMap(doc.data())).toList(); + products.assignAll(fetchedProducts); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch products: $e'); + } + } + + // Search products based on query + List searchProducts(String query) { + query = query.trim().toLowerCase(); + return products.where((product) { + return product.name.toLowerCase().contains(query); + }).toList(); + } + + void addToSearchHistory(String query) { + if (!searchHistory.contains(query)) { + searchHistory.add(query); + } + } + + void clearSearchHistory() { + searchHistory.clear(); + } + + // Get product by ID + ProductModel? getProductByID(String id) { + try { + return products.firstWhere((product) => product.id == id); + } catch (e) { + return null; + } + } +} diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index f058de2a..f5f7046d 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -1,14 +1,96 @@ +import 'package:carousel_slider/carousel_options.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:get/get.dart'; - -import '../../../../models/role.dart'; -import '../../../../services/auth_service.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/models/banner_model.dart'; +import 'package:get_flutter_fire/models/category_model.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; class HomeController extends GetxController { - final Rx chosenRole = Rx(AuthService.to.maxRole); + RxList banners = [].obs; + RxList categories = [].obs; + RxList products = [].obs; + RxBool isLoading = true.obs; + RxInt currentCarouselIndex = 0.obs; + + @override + void onInit() { + super.onInit(); + _fetchAllData(); + } + + Future _fetchAllData() async { + isLoading(true); + try { + final cartController = Get.put(CartController()); + final authController = Get.put(AuthController()); + await Future.wait([ + fetchBanners(), + fetchCategories(), + fetchProducts(), + cartController.fetchCartData(authController.user!.id), + ]); + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching data'); + } finally { + isLoading(false); + } + } + + Future fetchBanners() async { + try { + QuerySnapshot snapshot = await FirebaseFirestore.instance + .collection('banners') + .where('isActive', isEqualTo: true) + .get(); + + List fetchedBanners = snapshot.docs.map((doc) { + return BannerModel.fromMap(doc.data() as Map); + }).toList(); + + banners.value = fetchedBanners; + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching banners'); + rethrow; + } + } + + Future fetchCategories() async { + try { + QuerySnapshot snapshot = + await FirebaseFirestore.instance.collection('categories').get(); + + List fetchedCategories = snapshot.docs.map((doc) { + final data = doc.data() as Map; + return CategoryModel.fromMap(data); + }).toList(); + + categories.value = fetchedCategories; + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching categories'); + rethrow; + } + } + + Future fetchProducts() async { + try { + QuerySnapshot snapshot = + await FirebaseFirestore.instance.collection('products').get(); - // Role get role => AuthService.to.maxRole; + List fetchedProducts = snapshot.docs.map((doc) { + final data = doc.data() as Map; + return ProductModel.fromMap(data); + }).toList(); - get isBuyer => chosenRole.value == Role.buyer; + products.value = fetchedProducts; + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching products'); + rethrow; + } + } - get isAdmin => chosenRole.value == Role.admin; + void onPageChanged(int index, CarouselPageChangedReason reason) { + currentCarouselIndex.value = index; + } } diff --git a/lib/app/modules/home/controllers/home_product_controller.dart b/lib/app/modules/home/controllers/home_product_controller.dart new file mode 100644 index 00000000..8bd900fc --- /dev/null +++ b/lib/app/modules/home/controllers/home_product_controller.dart @@ -0,0 +1,104 @@ +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; + +class HomeProductController extends GetxController { + final String productID; + Rx product = Rx(null); + RxBool isLoading = true.obs; + RxInt currentCarouselIndex = 0.obs; + late CartController cartController; + var products = [].obs; + var searchHistory = [].obs; + + HomeProductController(this.productID); + + @override + void onInit() { + super.onInit(); + cartController = Get.put(CartController()); + fetchProductDetails(); + fetchProducts(); + } + + Future fetchProductDetails() async { + isLoading(true); + try { + DocumentSnapshot snapshot = await FirebaseFirestore.instance + .collection('products') + .doc(productID) + .get(); + + if (snapshot.exists) { + product.value = + ProductModel.fromMap(snapshot.data() as Map); + } else { + Get.snackbar('Error', 'Product not found'); + } + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching product details'); + } finally { + isLoading(false); + } + } + + void onPageChanged(int index) { + currentCarouselIndex.value = index; + } + + Future fetchProductsByCategory(String categoryId) async { + try { + final snapshot = await FirebaseFirestore.instance + .collection('products') + .where('categoryID', isEqualTo: categoryId) + .get(); + + final fetchedProducts = + snapshot.docs.map((doc) => ProductModel.fromMap(doc.data())).toList(); + products.assignAll(fetchedProducts); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch products: $e'); + } + } + + // Fetch all products from Firestore + Future fetchProducts() async { + try { + final snapshot = + await FirebaseFirestore.instance.collection('products').get(); + final fetchedProducts = + snapshot.docs.map((doc) => ProductModel.fromMap(doc.data())).toList(); + products.assignAll(fetchedProducts); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch products: $e'); + } + } + + // Search products based on query + List searchProducts(String query) { + query = query.trim().toLowerCase(); + return products.where((product) { + return product.name.toLowerCase().contains(query); + }).toList(); + } + + void addToSearchHistory(String query) { + if (!searchHistory.contains(query)) { + searchHistory.add(query); + } + } + + void clearSearchHistory() { + searchHistory.clear(); + } + + // Get product by ID + ProductModel? getProductByID(String id) { + try { + return products.firstWhere((product) => product.id == id); + } catch (e) { + return null; + } + } +} diff --git a/lib/app/modules/home/view/categories/categories.dart b/lib/app/modules/home/view/categories/categories.dart new file mode 100644 index 00000000..fcb5f6da --- /dev/null +++ b/lib/app/modules/home/view/categories/categories.dart @@ -0,0 +1,101 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/home/controllers/home_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CategoriesScreen extends StatefulWidget { + const CategoriesScreen({super.key}); + + @override + State createState() => _CategoriesScreenState(); +} + +class _CategoriesScreenState extends State { + @override + Widget build(BuildContext context) { + final HomeController homeController = Get.put(HomeController()); + final categories = homeController.categories; + + return Scaffold( + backgroundColor: AppTheme.backgroundColor, + appBar: AppBar( + title: const Text( + 'Categories', + style: TextStyle( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + fontSize: AppTheme.fontSizeLarge, + ), + ), + backgroundColor: AppTheme.colorWhite, + elevation: 0, + centerTitle: true, + ), + body: Padding( + padding: AppTheme.paddingDefault, + child: GridView.builder( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + itemCount: categories.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: AppTheme.spacingDefault, + mainAxisSpacing: AppTheme.spacingDefault, + childAspectRatio: 1, + ), + itemBuilder: (context, index) { + final category = categories[index]; + return InkWell( + onTap: () { + if (kDebugMode) { + print('Tapped on category: ${category.id}'); + } + Get.toNamed( + Routes.PRODUCTS_LISTING, + arguments: {'category': category.id}, + ); + }, + child: Container( + decoration: BoxDecoration( + color: AppTheme.colorWhite, + borderRadius: AppTheme.borderRadius, + boxShadow: [ + BoxShadow( + color: AppTheme.colorBlack.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: AppTheme.borderRadiusSmall, + child: Image.network( + category.imageUrl, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + ), + const SizedBox(height: AppTheme.spacingTiny), + Text( + category.name, + style: AppTheme.fontStyleDefaultBold.copyWith( + fontSize: AppTheme.fontSizeMedium, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/app/modules/home/view/home.dart b/lib/app/modules/home/view/home.dart new file mode 100644 index 00000000..7f441d60 --- /dev/null +++ b/lib/app/modules/home/view/home.dart @@ -0,0 +1,275 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/home/controllers/home_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/secondary_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/product/add_to_cart_button.dart'; +import 'package:get_flutter_fire/models/banner_model.dart'; +import 'package:get_flutter_fire/models/category_model.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final HomeController homeController = Get.put(HomeController()); + + return Scaffold( + body: Obx(() { + if (homeController.isLoading.value) { + return const LoadingWidget(); + } else { + return SingleChildScrollView( + child: Column( + children: [ + Hero( + tag: 'searchField', + child: InkWell( + onTap: () { + // context.push(Routes.search); + Get.toNamed(Routes.SEARCH); + }, + child: Padding( + padding: AppTheme.paddingDefault, + child: Container( + padding: AppTheme.paddingTiny, + decoration: BoxDecoration( + color: AppTheme.greyTextColor, + borderRadius: AppTheme.borderRadius, + ), + child: Row( + children: [ + const Icon( + Icons.search, + color: AppTheme.colorWhite, + ), + const Spacing( + size: AppTheme.spacingTiny, isHorizontal: true), + Text( + "search", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorWhite, + ), + ), + ], + ), + ), + ), + ), + ), + _buildCarousel(homeController.banners), + _buildCategories(homeController.categories), + _buildProducts( + homeController.products, MediaQuery.of(context).size), + ], + ), + ); + } + }), + ); + } + + Widget _buildCarousel(List banners) { + return CarouselSlider( + options: CarouselOptions( + height: 200.0, + autoPlay: true, + enlargeCenterPage: true, + aspectRatio: 16 / 9, + viewportFraction: 0.8, + ), + items: banners.map((banner) { + return Builder( + builder: (BuildContext context) { + return Container( + margin: const EdgeInsets.all(5.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: NetworkImage(banner.imageUrl), + fit: BoxFit.cover, + ), + ), + ); + }, + ); + }).toList(), + ); + } + + Widget _buildCategories(List categories) { + return Padding( + padding: AppTheme.paddingDefault, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Categories", + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + SecondaryButton( + label: "See All", + onPressed: () { + Get.toNamed(Routes.CATEGORIES); + }, + ), + ], + ), + const Spacing(size: AppTheme.fontSizeDefault), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + return Column( + children: [ + Container( + width: 70.0, + height: 70.0, + margin: const EdgeInsets.only(right: 10.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: NetworkImage(category.imageUrl), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 5), + Text( + category.name, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildProducts(List products, Size size) { + return Padding( + padding: AppTheme.paddingDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Products", + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + SecondaryButton( + label: "See All", + onPressed: () { + Get.toNamed( + Routes.PRODUCTS_LISTING, + arguments: {'category': 'ALL'}, + ); + }, + ), + ], + ), + const Spacing(size: AppTheme.fontSizeDefault), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: (products.length / 2).ceil(), + itemBuilder: (context, index) { + final productLeft = products[index * 2]; + final productRight = (index * 2 + 1 < products.length) + ? products[index * 2 + 1] + : null; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildProductItem(productLeft), + if (productRight != null) _buildProductItem(productRight), + ], + ); + }, + ), + ], + ), + ); + } + + Widget _buildProductItem(ProductModel product) { + return GestureDetector( + onTap: () { + Get.toNamed('/product/${product.id}'); + }, + child: SizedBox( + width: 150, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 150.0, + height: 150.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: NetworkImage(product.images.first), + fit: BoxFit.cover, + ), + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + product.name, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + product.description, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "Rs. ${product.unitPrice}", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + AddToCartButton(product: product), + const Spacing(size: AppTheme.spacingMedium), + ], + ), + ), + ); + } +} diff --git a/lib/app/modules/home/view/product_bottom.dart b/lib/app/modules/home/view/product_bottom.dart new file mode 100644 index 00000000..0eb9aabb --- /dev/null +++ b/lib/app/modules/home/view/product_bottom.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/product/add_to_cart_button.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class ProductBottomButton extends StatelessWidget { + final VoidCallback onPressed; + final ProductModel product; + final String label; + final String totalPrice; + final String originalPrice; + final bool disabled; + final bool isWholesale; + + const ProductBottomButton({ + super.key, + required this.onPressed, + required this.label, + required this.totalPrice, + required this.originalPrice, + required this.product, + this.disabled = false, + this.isWholesale = false, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: AppTheme.paddingDefault, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + totalPrice, + style: AppTheme.fontStyleMedium.copyWith( + color: AppTheme.colorRed, + fontWeight: FontWeight.w700, + ), + ), + Text( + originalPrice, + style: AppTheme.fontStyleSmall.copyWith( + decoration: TextDecoration.lineThrough, + color: AppTheme.greyTextColor, + ), + ), + ], + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + SizedBox( + width: 100, + child: AddToCartButton( + product: product, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/app/modules/home/view/product_card.dart b/lib/app/modules/home/view/product_card.dart new file mode 100644 index 00000000..344f3117 --- /dev/null +++ b/lib/app/modules/home/view/product_card.dart @@ -0,0 +1,88 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/product/add_to_cart_button.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class ProductCard extends StatefulWidget { + final ProductModel product; + final bool showAddToCart; + const ProductCard( + {super.key, required this.product, this.showAddToCart = true}); + + @override + State createState() => _ProductCardState(); +} + +class _ProductCardState extends State { + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + Get.toNamed('/product/${widget.product.id}'); + }, + child: Container( + width: 160, + height: (widget.showAddToCart) ? 332 : 260, + decoration: BoxDecoration( + color: AppTheme.colorWhite, + borderRadius: AppTheme.borderRadius, + border: AppTheme.cardBorder, + ), + child: Column( + children: [ + ClipRRect( + borderRadius: + BorderRadius.vertical(top: AppTheme.borderRadius.topLeft), + child: CachedNetworkImage( + imageUrl: widget.product.images[0], + fit: BoxFit.fitHeight, + height: 80, + width: double.infinity, + ), + ), + Expanded( + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.product.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTheme.fontStyleDefault, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + widget.product.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + const Spacing(size: AppTheme.spacingSmall), + Text( + widget.product.unitPrice.toString(), + style: AppTheme.fontStyleDefaultBold, + ), + if (widget.showAddToCart) ...[ + const Spacing(size: AppTheme.spacingTiny), + const Spacer(), + AddToCartButton( + product: widget.product, + ), + ], + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/modules/home/view/product_detail_screen.dart b/lib/app/modules/home/view/product_detail_screen.dart new file mode 100644 index 00000000..abf7a311 --- /dev/null +++ b/lib/app/modules/home/view/product_detail_screen.dart @@ -0,0 +1,224 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dots_indicator/dots_indicator.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/home/controllers/home_product_controller.dart'; +import 'package:get_flutter_fire/app/modules/home/view/product_bottom.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/cart_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class ProductDetailScreen extends StatelessWidget { + final String productID; + + const ProductDetailScreen({super.key, required this.productID}); + + @override + Widget build(BuildContext context) { + final HomeProductController productController = Get.put( + HomeProductController(productID), + ); // Pass productID as a named argument + final CartController cartController = Get.find(); + + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Obx(() { + if (productController.isLoading.value) { + return const LoadingWidget(); + } + + final product = productController.product.value; + if (product == null) { + return const Center( + child: Text( + 'Product not found', + style: AppTheme.fontStyleDefaultBold, + ), + ); + } + + // final isInCart = cartController.isProductInCart(product.id); + + return Stack( + children: [ + SingleChildScrollView( + child: Column( + children: [ + const Spacing(size: AppTheme.spacingExtraSmall), + CarouselSlider.builder( + itemCount: product.images.length, + itemBuilder: (context, index, pageViewIndex) { + var imageUrl = product.images[index]; + return CachedNetworkImage( + imageUrl: imageUrl, + fit: BoxFit.cover, + width: double.infinity, + ); + }, + options: CarouselOptions( + height: 320, + viewportFraction: 1, + autoPlay: true, + autoPlayInterval: const Duration(seconds: 5), + enableInfiniteScroll: product.images.length != 1, + onPageChanged: (index, reason) { + productController.onPageChanged(index); + }, + ), + ), + const Spacing(size: 300), + ], + ), + ), + Positioned( + top: 35, + left: 16, + child: InkWell( + onTap: () { + Get.back(); + }, + child: Container( + height: 30, + width: 30, + decoration: const BoxDecoration( + color: AppTheme.colorWhite, + borderRadius: BorderRadius.all(Radius.circular(40.0)), + ), + child: const Icon(Icons.arrow_back_ios_new_sharp, + size: 14, color: AppTheme.greyTextColor), + ), + ), + ), + Positioned( + top: 35, + right: 16, + child: InkWell( + onTap: () { + // TODO: Add share product functionality + }, + child: Container( + height: 30, + width: 30, + decoration: const BoxDecoration( + color: AppTheme.colorWhite, + borderRadius: BorderRadius.all(Radius.circular(40.0)), + ), + child: const Icon(Icons.share, + size: 14, color: AppTheme.greyTextColor), + ), + ), + ), + Positioned( + top: 300, + left: 0, + right: 0, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24.0), + topRight: Radius.circular(24.0), + ), + child: Container( + color: AppTheme.colorWhite, + padding: AppTheme.paddingDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: AppTheme.paddingTiny, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadiusSmall, + border: Border.all( + color: AppTheme.colorRed, + ), + ), + child: Text( + 'RS ${product.unitPrice}', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorRed, + ), + ), + ), + const SizedBox(height: AppTheme.spacingSmall), + Row( + children: [ + Text( + product.name, + style: AppTheme.fontStyleDefault, + ), + const Spacer(), + ], + ), + const SizedBox(height: AppTheme.spacingTiny), + Text( + 'Dimension: ${product.unitWeight} gm', + style: AppTheme.fontStyleDefault, + ), + const SizedBox(height: AppTheme.spacingSmall), + const Text( + 'Product Description', + style: AppTheme.fontStyleDefaultBold, + ), + const SizedBox(height: AppTheme.spacingTiny), + Text( + product.description, + style: AppTheme.fontStyleDefault, + ), + const SizedBox(height: AppTheme.spacingSemiMedium), + ], + ), + ), + ), + ), + Positioned( + bottom: 340, + left: 0, + right: 0, + child: Center( + child: Container( + decoration: BoxDecoration( + color: AppTheme.greyTextColor, + borderRadius: AppTheme.borderRadiusSmall, + ), + child: DotsIndicator( + dotsCount: product.images.length, + position: productController.currentCarouselIndex.toInt(), + decorator: DotsDecorator( + color: AppTheme.colorDisabled, + activeColor: AppTheme.colorBlack, + activeSize: const Size(22.0, 9.0), + activeShape: AppTheme.rrShape, + ), + ), + ), + ), + ), + ], + ); + }), + bottomNavigationBar: Obx(() { + final product = productController.product.value; + if (product == null) return const SizedBox.shrink(); + + final isInCart = cartController.isProductInCart(product.id); + return ProductBottomButton( + product: product, + isWholesale: true, + label: isInCart ? 'Go to Cart' : 'Add to Cart', + totalPrice: product.unitPrice.toString(), + originalPrice: product.unitPrice.toString(), + onPressed: () { + if (isInCart) { + Get.toNamed('/cart'); + } else { + cartController.addItem(product as CartItem); + } + }, + ); + }), + ); + } +} diff --git a/lib/app/modules/home/view/product_listing.dart b/lib/app/modules/home/view/product_listing.dart new file mode 100644 index 00000000..4b7d48a6 --- /dev/null +++ b/lib/app/modules/home/view/product_listing.dart @@ -0,0 +1,141 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/home/view/product_card.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/app/modules/home/controllers/category_filter_controller.dart'; + +class ProductsListingScreen extends StatefulWidget { + const ProductsListingScreen({super.key}); + + @override + State createState() => _ProductsListingScreenState(); +} + +class _ProductsListingScreenState extends State { + String? query; + String? category; + + @override + void initState() { + super.initState(); + + query = Get.arguments['query']; + category = Get.arguments['category']; + + if (kDebugMode) { + print("The category ID is $category"); + } + + final CategoryFilterController productsController = + Get.put(CategoryFilterController()); + + if (category != null) { + productsController.fetchProductsByCategory(category!); + } + } + + @override + Widget build(BuildContext context) { + final CategoryFilterController productsController = + Get.find(); + + return Scaffold( + appBar: AppBar( + title: const Text( + 'Products', + style: TextStyle( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + fontSize: AppTheme.fontSizeLarge, + ), + ), + backgroundColor: AppTheme.colorWhite, + elevation: 0, + centerTitle: true, + ), + body: Obx(() { + final products = productsController.products; + + if (products.isEmpty) { + return const Align( + alignment: Alignment.topCenter, + child: Padding( + padding: AppTheme.paddingSmall, + child: Text('No products found', style: AppTheme.fontStyleMedium), + ), + ); + } else { + return SingleChildScrollView( + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + query != null + ? Padding( + padding: const EdgeInsets.only( + bottom: AppTheme.spacingSmall), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "${products.length}", + style: AppTheme.fontStyleDefaultBold, + ), + TextSpan( + text: " results for ", + style: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + )), + TextSpan( + text: '"$query"', + style: AppTheme.fontStyleDefaultBold + .copyWith( + color: AppTheme.colorBlue, + ), + ), + ], + ), + ), + ) + : Padding( + padding: const EdgeInsets.only( + bottom: AppTheme.spacingSmall), + child: Text( + "${products.length} results", + style: AppTheme.fontStyleDefault, + ), + ), + ], + ), + const Spacing(size: AppTheme.spacingSmall), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 0.7, + ), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + return ProductCard(product: product); + }, + ), + ], + ), + ), + ); + } + }), + ); + } +} diff --git a/lib/app/modules/home/view/search.dart b/lib/app/modules/home/view/search.dart new file mode 100644 index 00000000..59eabc5d --- /dev/null +++ b/lib/app/modules/home/view/search.dart @@ -0,0 +1,226 @@ +import 'dart:async'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/home/controllers/category_filter_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/secondary_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SearchScreen extends StatefulWidget { + const SearchScreen({super.key}); + + @override + State createState() => _SearchScreenState(); +} + +class _SearchScreenState extends State { + Timer? _typingTimer; + List _searchResults = []; + String _query = ""; + + final CategoryFilterController productsController = + Get.put(CategoryFilterController()); + + void _onChange(String text) { + if (_typingTimer?.isActive ?? false) { + _typingTimer?.cancel(); + } + + _typingTimer = Timer(const Duration(milliseconds: 500), () { + if (text.isNotEmpty) { + setState(() { + _query = text; + }); + _searchProducts(text); + } else { + setState(() { + _searchResults = []; + _query = ""; + }); + } + }); + } + + void _searchProducts(String query) { + setState(() { + _searchResults = productsController.searchProducts(query); + }); + } + + void _clearSearchHistory() { + productsController.clearSearchHistory(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, + color: AppTheme.greyTextColor), + onPressed: () => Get.back(), + ), + const Spacing( + size: AppTheme.spacingTiny, isHorizontal: true), + Expanded( + child: Hero( + tag: 'searchField', + child: TextField( + decoration: InputDecoration( + hintText: 'Search...', + hintStyle: const TextStyle( + color: AppTheme.greyTextColor, + ), + border: OutlineInputBorder( + borderRadius: AppTheme.borderRadius, + borderSide: const BorderSide( + color: AppTheme.greyTextColor, + ), + ), + contentPadding: const EdgeInsets.symmetric( + vertical: 12.0, horizontal: 12.0), + ), + onChanged: _onChange, + ), + ), + ), + ], + ), + const Spacing(size: AppTheme.spacingSmall), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _searchResults.length, + itemBuilder: (context, index) { + var product = _searchResults[index]; + return Padding( + padding: const EdgeInsets.only( + bottom: AppTheme.spacingTiny, + ), + child: ListTile( + contentPadding: EdgeInsets.zero, + leading: CachedNetworkImage( + imageUrl: product.images[0], + height: 24, + width: 24, + ), + title: Text(product.name), + trailing: const Icon(Icons.arrow_forward_ios, + color: AppTheme.greyTextColor), + onTap: () async { + productsController.addToSearchHistory(product.id); + Get.toNamed('/product/${product.id}'); + }, + ), + ); + }, + ), + _query.isNotEmpty + ? InkWell( + onTap: () async { + productsController.addToSearchHistory(_query); + Get.toNamed(Routes.PRODUCTS_LISTING, + arguments: {'query': _query}); + }, + child: RichText( + text: TextSpan( + children: [ + const TextSpan( + text: 'Search for ', + style: AppTheme.fontStyleDefault, + ), + TextSpan( + text: ' "$_query"', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorBlue, + )), + ], + ), + ), + ) + : const SizedBox.shrink(), + const Spacing(size: AppTheme.spacingDefault), + Obx(() { + List searchHistory = productsController.searchHistory; + + if (searchHistory.isNotEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Past Searches', + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: AppTheme.colorBlue, + ), + ), + SecondaryButton( + label: 'Clear History', + onPressed: _clearSearchHistory, + ), + ], + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: searchHistory.length, + itemBuilder: (context, index) { + String query = searchHistory[index]; + ProductModel? product = + productsController.getProductByID(query); + return Padding( + padding: const EdgeInsets.only( + bottom: AppTheme.spacingTiny, + ), + child: ListTile( + contentPadding: EdgeInsets.zero, + leading: product != null + ? CachedNetworkImage( + imageUrl: product.images[0], + height: 24, + width: 24, + ) + : const Icon(Icons.history, + color: AppTheme.greyTextColor), + title: Text(product?.name ?? query), + trailing: const Icon(Icons.arrow_forward_ios, + color: AppTheme.greyTextColor), + onTap: () async { + if (product != null) { + Get.toNamed('/product/${product.id}'); + } else { + Get.toNamed(Routes.PRODUCTS_LISTING, + arguments: {'query': query}); + } + }, + ), + ); + }, + ), + ], + ); + } + return const SizedBox.shrink(); + }), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/home/views/home_view.dart b/lib/app/modules/home/views/home_view.dart deleted file mode 100644 index 0cfc040d..00000000 --- a/lib/app/modules/home/views/home_view.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../../../routes/app_pages.dart'; -import '../../../widgets/screen_widget.dart'; -import '../controllers/home_controller.dart'; - -class HomeView extends GetView { - const HomeView({super.key}); - - @override - Widget build(BuildContext context) { - return GetRouterOutlet.builder( - builder: (context, delegate, currentRoute) { - var arg = Get.rootDelegate.arguments(); - if (arg != null) { - controller.chosenRole.value = arg["role"]; - } - var route = controller.chosenRole.value.tabs[0].route; - //This router outlet handles the appbar and the bottom navigation bar - return ScreenWidget( - screen: screen!, - body: GetRouterOutlet( - initialRoute: route, - // anchorRoute: Routes.HOME, - key: Get.nestedKey(route), - ), - role: controller.chosenRole.value, - delegate: delegate, - currentRoute: currentRoute, - ); - }, - ); - } -} diff --git a/lib/app/modules/login/bindings/login_binding.dart b/lib/app/modules/login/bindings/login_binding.dart deleted file mode 100644 index ac119f4a..00000000 --- a/lib/app/modules/login/bindings/login_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/login_controller.dart'; - -class LoginBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => LoginController(), - ); - } -} diff --git a/lib/app/modules/login/controllers/login_controller.dart b/lib/app/modules/login/controllers/login_controller.dart deleted file mode 100644 index 5178fec9..00000000 --- a/lib/app/modules/login/controllers/login_controller.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:get/get.dart'; - -import '../../../../services/auth_service.dart'; - -class LoginController extends GetxController { - static AuthService get to => Get.find(); - - final Rx showReverificationButton = Rx(false); - - bool get isRobot => AuthService.to.robot.value == true; - - set robot(bool v) => AuthService.to.robot.value = v; - - bool get isLoggedIn => AuthService.to.isLoggedInValue; - - bool get isAnon => AuthService.to.isAnon; - - bool get isRegistered => - AuthService.to.registered.value || AuthService.to.isEmailVerified; -} diff --git a/lib/app/modules/login/views/login_view.dart b/lib/app/modules/login/views/login_view.dart deleted file mode 100644 index 00c3af3f..00000000 --- a/lib/app/modules/login/views/login_view.dart +++ /dev/null @@ -1,162 +0,0 @@ -// ignore_for_file: inference_failure_on_function_invocation - -import 'package:firebase_auth/firebase_auth.dart' as fba; -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; -import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../../../../firebase_options.dart'; - -import '../../../../models/screens.dart'; -import '../../../widgets/login_widgets.dart'; -import '../controllers/login_controller.dart'; - -class LoginView extends GetView { - void showReverificationButton( - bool show, fba.EmailAuthCredential? credential) { - // Below is very important. - // See [https://stackoverflow.com/questions/69351845/this-obx-widget-cannot-be-marked-as-needing-to-build-because-the-framework-is-al] - WidgetsBinding.instance.addPostFrameCallback((_) { - controller.showReverificationButton.value = show; - }); - //or Future.delayed(Duration.zero, () { - // We can get the email and password from the controllers either by making the whole screen from scratch - // or probably by add flutter_test find.byKey (hacky) - // tried using AuthStateChangeAction instead which is not getting called - // Finally Subclassed EmailAuthProvider to handle the same, but that also did not work - // So went for server side email sending option - //})); - } - - const LoginView({super.key}); - - @override - Widget build(BuildContext context) { - return Obx(() => loginScreen(context)); - } - - Widget subtitleBuilder(context, action) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: action == AuthAction.signIn - ? const Text('Welcome to Get Flutter Fire, please sign in!') - : const Text('New to Get Flutter Fire, please sign up!'), - ); - } - - Widget footerBuilder(Rx show, Rxn credential) { - return LoginWidgets.footerBuilder(EmailLinkButton(show, credential)); - } - - Widget loginScreen(BuildContext context) { - Widget ui; - if (!controller.isLoggedIn) { - ui = !(GetPlatform.isAndroid || GetPlatform.isIOS) && controller.isRobot - ? recaptcha() - : SignInScreen( - providers: [ - GoogleProvider(clientId: DefaultFirebaseOptions.webClientId), - MyEmailAuthProvider(), - ], - showAuthActionSwitch: !controller.isRegistered, - showPasswordVisibilityToggle: true, - headerBuilder: LoginWidgets.headerBuilder, - subtitleBuilder: subtitleBuilder, - footerBuilder: (context, action) => footerBuilder( - controller.showReverificationButton, - LoginController.to.credential), - sideBuilder: LoginWidgets.sideBuilder, - actions: getActions(), - ); - } else if (controller.isAnon) { - ui = RegisterScreen( - providers: [ - MyEmailAuthProvider(), - ], - showAuthActionSwitch: !controller.isAnon, //if Anon only SignUp - showPasswordVisibilityToggle: true, - headerBuilder: LoginWidgets.headerBuilder, - subtitleBuilder: subtitleBuilder, - footerBuilder: (context, action) => footerBuilder( - controller.showReverificationButton, LoginController.to.credential), - sideBuilder: LoginWidgets.sideBuilder, - actions: getActions(), - ); - } else { - final thenTo = Get - .rootDelegate.currentConfiguration!.currentPage!.parameters?['then']; - Get.rootDelegate.offNamed(thenTo ?? - (controller.isRegistered ? Screen.HOME : Screen.REGISTER).route); - ui = const Scaffold(); - } - return ui; - } - - Widget recaptcha() { - //TODO: Add Recaptcha - return Scaffold( - body: TextButton( - onPressed: () => controller.robot = false, - child: const Text("Are you a Robot?"), - )); - } - - /// The following actions are useful here: - /// - [AuthStateChangeAction] - /// - [AuthCancelledAction] - /// - [EmailLinkSignInAction] - /// - [VerifyPhoneAction] - /// - [SMSCodeRequestedAction] - - List getActions() { - return [ - // AuthStateChangeAction((context, state) { - AuthStateChangeAction((context, state) => LoginController.to - .errorMessage(context, state, showReverificationButton)), - // AuthStateChangeAction((context, state) { - // // This is not required due to the AuthMiddleware - // }), - // EmailLinkSignInAction((context) { - // final thenTo = Get.rootDelegate.currentConfiguration!.currentPage! - // .parameters?['then']; - // Get.rootDelegate.offNamed(thenTo ?? Routes.PROFILE); - // }), - ]; - } -} - -class MyEmailAuthProvider extends EmailAuthProvider { - @override - void onCredentialReceived( - fba.EmailAuthCredential credential, - AuthAction action, - ) { - WidgetsBinding.instance.addPostFrameCallback((_) { - LoginController.to.credential.value = credential; - }); - super.onCredentialReceived(credential, action); - } -} - -class EmailLinkButton extends StatelessWidget { - final Rx show; - final Rxn credential; - - const EmailLinkButton( - this.show, - this.credential, { - super.key, - }); - - @override - Widget build(BuildContext context) { - return Obx(() => Visibility( - visible: show.value, - child: Padding( - padding: const EdgeInsets.only(top: 16), - child: ElevatedButton( - onPressed: () => LoginController.to - .sendVerificationMail(emailAuth: credential.value), - child: const Text('Resend Verification Mail'))))); - } -} diff --git a/lib/app/modules/my_products/bindings/my_products_binding.dart b/lib/app/modules/my_products/bindings/my_products_binding.dart deleted file mode 100644 index a537f047..00000000 --- a/lib/app/modules/my_products/bindings/my_products_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/my_products_controller.dart'; - -class MyProductsBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => MyProductsController(), - ); - } -} diff --git a/lib/app/modules/my_products/controllers/my_products_controller.dart b/lib/app/modules/my_products/controllers/my_products_controller.dart deleted file mode 100644 index 31696ea2..00000000 --- a/lib/app/modules/my_products/controllers/my_products_controller.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:get/get.dart'; - -class MyProductsController extends GetxController { - //TODO: Implement MyProductsController - - final count = 0.obs; - @override - void onInit() { - super.onInit(); - } - - @override - void onReady() { - super.onReady(); - } - - @override - void onClose() { - super.onClose(); - } - - void increment() => count.value++; -} diff --git a/lib/app/modules/my_products/views/my_products_view.dart b/lib/app/modules/my_products/views/my_products_view.dart deleted file mode 100644 index 43793ebb..00000000 --- a/lib/app/modules/my_products/views/my_products_view.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - -import '../controllers/my_products_controller.dart'; - -class MyProductsView extends GetView { - const MyProductsView({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('MyProductsView'), - centerTitle: true, - ), - body: const Center( - child: Text( - 'MyProductsView is working', - style: TextStyle(fontSize: 20), - ), - ), - ); - } -} diff --git a/lib/app/modules/orders/views/order_detail_screen.dart b/lib/app/modules/orders/views/order_detail_screen.dart new file mode 100644 index 00000000..ea012b79 --- /dev/null +++ b/lib/app/modules/orders/views/order_detail_screen.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/order_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/product_controller.dart'; +import 'package:get_flutter_fire/app/widgets/cart/order_detail_card.dart'; +import 'package:get_flutter_fire/app/widgets/cart/order_summary_widget.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/orders/order_status_indicator.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderDetailScreen extends StatelessWidget { + const OrderDetailScreen({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final String orderID = Get.arguments['id']; + final OrderController orderController = Get.find(); + final ProductController productController = Get.find(); + + return Scaffold( + body: Padding( + padding: AppTheme.paddingSmall, + child: SingleChildScrollView( + child: Obx(() { + if (orderController.isLoading.value) { + return const LoadingWidget(); + } + + final order = orderController.getOrderByID(orderID); + final coupon = orderController.orders + .firstWhere((o) => o.id == orderID) + .couponID; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacing(size: AppTheme.spacingLarge), + OrderStatusIndicator(currentStatus: order.currentStatus), + const Spacing(size: AppTheme.spacingSmall), + Center( + child: Text( + "Order ${order.currentStatus.name.capitalizeFirst}", + style: AppTheme.fontStyleLarge, + ), + ), + const SizedBox(height: AppTheme.spacingDefault), + const Text( + 'Item Information', + style: AppTheme.fontStyleDefaultBold, + ), + const SizedBox(height: AppTheme.spacingSmall), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: order.products.length, + itemBuilder: (context, index) { + final item = order.products[index]; + final product = productController.getProductByID(item.id); + + return OrderDetailProductCard( + product: product, + productData: order.products[index], + ); + }, + ), + const SizedBox(height: AppTheme.spacingTiny), + const Text( + 'Payment Information', + style: AppTheme.fontStyleDefaultBold, + ), + const SizedBox(height: AppTheme.spacingSmall), + Container( + decoration: AppTheme.cardDecoration, + padding: AppTheme.paddingSmall, + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + (order.paymentMethod == "cash") ? 'Cash' : 'Paid', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorBlue, + fontWeight: FontWeight.bold, + ), + ), + Text( + (order.paymentMethod == "cash") + ? 'Cash on Delivery' + : 'Online Payment', + style: AppTheme.fontStyleDefault, + ), + ], + ), + ], + ), + ), + const SizedBox(height: AppTheme.spacingDefault), + const Text( + 'Delivery Address', + style: AppTheme.fontStyleDefaultBold, + ), + const SizedBox(height: AppTheme.spacingSmall), + Container( + decoration: AppTheme.cardDecoration, + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${order.address.name} (${order.address.phoneNumber})", + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingSmall), + Text( + "${order.address.line1}\n${order.address.line2}", + style: AppTheme.fontStyleDefault, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "${order.address.district}, ${order.address.city}", + style: AppTheme.fontStyleDefault, + ), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + const Text( + "Full Name: ", + style: AppTheme.fontStyleDefault, + ), + Text( + order.address.name, + style: AppTheme.fontStyleDefaultBold, + ), + ], + ), + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + const Text( + "Phone Number: ", + style: AppTheme.fontStyleDefault, + ), + Text( + order.address.phoneNumber, + style: AppTheme.fontStyleDefaultBold, + ), + ], + ), + const Spacing(size: AppTheme.spacingTiny), + ], + ), + ), + const SizedBox(height: AppTheme.spacingDefault), + const Text( + 'Order Summary', + style: AppTheme.fontStyleDefaultBold, + ), + const SizedBox(height: AppTheme.spacingSmall), + OrderSummaryWidget( + couponDiscount: order.couponDiscount, + couponCode: coupon, + priceDiscount: 0, + subTotalPrice: order.totalPrice, + totalPrice: order.totalPrice, + ), + const SizedBox(height: AppTheme.spacingDefault), + ], + ); + }), + ), + ), + // bottomNavigationBar: Obx(() { + // return BottomButton( + // onPressed: () => orderController.createPDF( + // order: orderController.currentOrder.value!, + // user: Get.find().user!, + // products: productController.products), + // label: 'Download Invoice', + // disabled: false, + // ); + // }), + ); + } +} diff --git a/lib/app/modules/orders/views/orders.dart b/lib/app/modules/orders/views/orders.dart new file mode 100644 index 00000000..3af36610 --- /dev/null +++ b/lib/app/modules/orders/views/orders.dart @@ -0,0 +1,287 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/order_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/product_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/orders/primary_button.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrdersScreen extends StatefulWidget { + const OrdersScreen({super.key}); + + @override + OrdersScreenState createState() => OrdersScreenState(); +} + +class OrdersScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + final List _orderStatusFilter = []; + + final OrderController orderController = Get.find(); + final ProductController productController = Get.find(); + + @override + void initState() { + super.initState(); + _searchController.addListener(_onSearchChanged); + } + + void _onSearchChanged() { + orderController.filterOrders(_searchController.text, _orderStatusFilter); + } + + void _toggleFilter(String status) { + setState(() { + if (_orderStatusFilter.contains(status)) { + _orderStatusFilter.remove(status); + } else { + _orderStatusFilter.add(status); + } + orderController.filterOrders(_searchController.text, _orderStatusFilter); + }); + } + + @override + void dispose() { + _searchController.removeListener(_onSearchChanged); + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Obx(() { + if (orderController.isLoading.value) { + return const LoadingWidget(); + } + + final filteredOrders = orderController.filteredOrders; + + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Container( + decoration: AppTheme.cardDecoration, + child: TextField( + controller: _searchController, + decoration: InputDecoration( + prefixIcon: const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Icon(Icons.search, + color: AppTheme.greyTextColor), + ), + hintText: 'Search for Orders', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: AppTheme.spacingTiny), + ), + textAlignVertical: TextAlignVertical.center, + ), + ), + ), + const Spacing( + isHorizontal: true, size: AppTheme.spacingTiny), + _filterByOrderWidget(), + ], + ), + filteredOrders.isEmpty + ? const Column( + children: [ + Spacing(size: AppTheme.spacingSmall), + Text( + 'No Orders Found', + style: AppTheme.fontStyleDefault, + ), + ], + ) + : const SizedBox.shrink(), + const Spacing(size: AppTheme.spacingSmall), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: filteredOrders.length, + itemBuilder: (context, index) { + final order = filteredOrders[index]; + final products = order.products + .map((product) => + productController.getProductByID(product.id)) + .toList(); + final productNames = products.map((p) { + return p.name; + }).toList(); + + return Padding( + padding: + const EdgeInsets.only(bottom: AppTheme.spacingSmall), + child: Container( + decoration: AppTheme.cardDecoration, + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + children: [ + const TextSpan( + text: "Order ID: ", + style: AppTheme.fontStyleDefault, + ), + TextSpan( + text: "#${order.id}", + style: AppTheme.fontStyleDefaultBold + .copyWith( + color: AppTheme.colorDarkBlue, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingTiny), + RichText( + text: TextSpan( + children: [ + const TextSpan( + text: "Status: ", + style: AppTheme.fontStyleDefault, + ), + TextSpan( + text: order.currentStatus.name, + style: AppTheme.fontStyleDefaultBold + .copyWith( + color: AppTheme.colorDarkBlue, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingTiny), + if (productNames.length == 1) + Text( + productNames[0], + style: AppTheme.fontStyleDefault, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + else if (productNames.length == 2) + Text( + productNames.join(', '), + style: AppTheme.fontStyleDefault, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + else if (productNames.length > 2) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + productNames.take(2).join(', '), + style: AppTheme.fontStyleDefault, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + '${productNames.length - 2} more products', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor), + ), + ], + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + 'Total Products: ${order.products.length}', + style: AppTheme.fontStyleDefault, + ), + const Spacing(size: AppTheme.spacingSmall), + PrimaryButton( + onPressed: () => Get.toNamed( + Routes.ORDER_DETAILS, + arguments: {'id': order.id}), + label: 'View Details', + ), + ], + ), + ), + ), + ); + }, + ), + ], + ), + ), + ), + ); + }); + } + + Widget _filterByOrderWidget() { + return InkWell( + onTap: () { + Get.dialog( + Dialog( + child: Container( + padding: AppTheme.paddingSmall, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + maxHeight: MediaQuery.of(context).size.height * 0.4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.white, + ), + child: _buildFilterDialog(), + ), + ), + ); + }, + child: Container( + height: 50, + padding: AppTheme.paddingTiny, + decoration: AppTheme.cardDecoration, + child: const Row( + children: [ + Text("Filter By", style: AppTheme.fontStyleDefault), + Spacing(size: AppTheme.spacingTiny, isHorizontal: true), + Icon(Icons.chevron_right), + ], + ), + ), + ); + } + + Widget _buildFilterDialog() { + final statusItems = ['Pending', 'Delivered', 'Cancelled', 'placed']; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: statusItems.map((status) { + return CheckboxListTile( + title: Text(status.capitalizeFirst!), + value: _orderStatusFilter.contains(status), + onChanged: (bool? value) { + _toggleFilter(status); + Get.back(); + }, + controlAffinity: ListTileControlAffinity.leading, + ); + }).toList(), + ); + } +} diff --git a/lib/app/modules/product_details/bindings/product_details_binding.dart b/lib/app/modules/product_details/bindings/product_details_binding.dart deleted file mode 100644 index 624d55ac..00000000 --- a/lib/app/modules/product_details/bindings/product_details_binding.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/product_details_controller.dart'; - -class ProductDetailsBinding extends Bindings { - @override - void dependencies() { - Get.create( - () => ProductDetailsController( - Get.parameters['productId'] ?? '', - ), - ); - } -} diff --git a/lib/app/modules/product_details/controllers/product_details_controller.dart b/lib/app/modules/product_details/controllers/product_details_controller.dart deleted file mode 100644 index d894e10c..00000000 --- a/lib/app/modules/product_details/controllers/product_details_controller.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:get/get.dart'; - -class ProductDetailsController extends GetxController { - final String productId; - - ProductDetailsController(this.productId); - @override - void onInit() { - super.onInit(); - Get.log('ProductDetailsController created with id: $productId'); - } - - @override - void onClose() { - Get.log('ProductDetailsController close with id: $productId'); - super.onClose(); - } -} diff --git a/lib/app/modules/product_details/views/product_details_view.dart b/lib/app/modules/product_details/views/product_details_view.dart deleted file mode 100644 index c9290724..00000000 --- a/lib/app/modules/product_details/views/product_details_view.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - -import '../controllers/product_details_controller.dart'; - -class ProductDetailsView extends GetWidget { - const ProductDetailsView({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'ProductDetailsView is working', - style: TextStyle(fontSize: 20), - ), - Text('ProductId: ${controller.productId}') - ], - ), - ), - ); - } -} diff --git a/lib/app/modules/products/bindings/products_binding.dart b/lib/app/modules/products/bindings/products_binding.dart deleted file mode 100644 index e7c762db..00000000 --- a/lib/app/modules/products/bindings/products_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/products_controller.dart'; - -class ProductsBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => ProductsController(), - ); - } -} diff --git a/lib/app/modules/products/controllers/products_controller.dart b/lib/app/modules/products/controllers/products_controller.dart deleted file mode 100644 index 118c7dc8..00000000 --- a/lib/app/modules/products/controllers/products_controller.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:get/get.dart'; - -import '../../../../models/product.dart'; - -class ProductsController extends GetxController { - final products = [].obs; - - void loadDemoProductsFromSomeWhere() { - products.add( - Product( - name: 'Product added on: ${DateTime.now().toString()}', - id: DateTime.now().millisecondsSinceEpoch.toString(), - ), - ); - } - - @override - void onReady() { - super.onReady(); - loadDemoProductsFromSomeWhere(); - } - - @override - void onClose() { - Get.printInfo(info: 'Products: onClose'); - super.onClose(); - } -} diff --git a/lib/app/modules/products/views/products_view.dart b/lib/app/modules/products/views/products_view.dart deleted file mode 100644 index 5b190a6a..00000000 --- a/lib/app/modules/products/views/products_view.dart +++ /dev/null @@ -1,58 +0,0 @@ -// ignore_for_file: inference_failure_on_function_invocation - -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../../../models/role.dart'; -import '../../../routes/app_pages.dart'; -import '../controllers/products_controller.dart'; - -class ProductsView extends GetView { - const ProductsView({super.key}); - - @override - Widget build(BuildContext context) { - var arg = Get.rootDelegate.arguments(); - return Scaffold( - floatingActionButton: - (arg != null && Get.rootDelegate.arguments()["role"] == Role.seller) - ? FloatingActionButton.extended( - onPressed: controller.loadDemoProductsFromSomeWhere, - label: const Text('Add'), - ) - : null, - body: Column( - children: [ - const Hero( - tag: 'heroLogo', - child: FlutterLogo(), - ), - Expanded( - child: Obx( - () => RefreshIndicator( - onRefresh: () async { - controller.products.clear(); - controller.loadDemoProductsFromSomeWhere(); - }, - child: ListView.builder( - itemCount: controller.products.length, - itemBuilder: (context, index) { - final item = controller.products[index]; - return ListTile( - onTap: () { - Get.rootDelegate.toNamed(Routes.PRODUCT_DETAILS( - item.id)); //we could use Get Parameters - }, - title: Text(item.name), - subtitle: Text(item.id), - ); - }, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/app/modules/profile/bindings/profile_binding.dart b/lib/app/modules/profile/bindings/profile_binding.dart deleted file mode 100644 index 5eb3b2bd..00000000 --- a/lib/app/modules/profile/bindings/profile_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/profile_controller.dart'; - -class ProfileBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => ProfileController(), - ); - } -} diff --git a/lib/app/modules/profile/controllers/address_controller.dart b/lib/app/modules/profile/controllers/address_controller.dart new file mode 100644 index 00000000..034dcc02 --- /dev/null +++ b/lib/app/modules/profile/controllers/address_controller.dart @@ -0,0 +1,163 @@ +import 'dart:developer'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:uuid/uuid.dart'; + +class AddressController extends GetxController { + final AuthController authController = Get.find(); + + final line1Controller = TextEditingController(); + final line2Controller = TextEditingController(); + final cityController = TextEditingController(); + final districtController = TextEditingController(); + + final latitudeController = TextEditingController(text: ''); + final longitudeController = TextEditingController(text: ''); + + final CollectionReference addressesRef = + FirebaseFirestore.instance.collection('addresses'); + + var addresses = [].obs; + var isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + + if (authController.currentUser.value == null) { + Get.snackbar('Error', 'User data is not available'); + return; + } + + fetchAddresses(); // Initial fetch + } + + @override + void onReady() { + super.onReady(); + fetchAddresses(); // Fetch addresses every time the screen becomes visible + } + + Future fetchAddresses() async { + try { + isLoading.value = true; + + if (authController.currentUser.value == null) { + Get.snackbar('Error', 'User is not authenticated'); + return; + } + + if (kDebugMode) { + print( + 'Fetching addresses for user: ${authController.currentUser.value!.id}'); + } + + final querySnapshot = await addressesRef + .where('userID', isEqualTo: authController.currentUser.value!.id) + .get(); + + if (kDebugMode) { + print('Fetched ${querySnapshot.docs.length} addresses'); + } + + addresses.assignAll(querySnapshot.docs + .map( + (doc) => AddressModel.fromMap(doc.data() as Map)) + .toList()); + + if (addresses.isEmpty) { + log('No address found. Please add an address.', level: 1000); + } + } catch (e) { + if (kDebugMode) { + print('Error fetching addresses: $e'); + } + Get.snackbar('Error', 'Failed to load addresses: $e'); + } finally { + isLoading.value = false; // End loading + } + } + + Future saveAddress() async { + if (authController.currentUser.value == null) { + Get.snackbar('Error', 'User is not authenticated'); + return; + } + + const uuid = Uuid(); + String addressID = uuid.v4(); + if (kDebugMode) { + print("The addressID is: $addressID"); + } + + AddressModel address = AddressModel( + name: authController.currentUser.value!.name, + phoneNumber: authController.currentUser.value!.phoneNumber, + line1: line1Controller.text, + line2: line2Controller.text, + city: cityController.text, + district: districtController.text, + latitude: latitudeController.text.isEmpty + ? 0.0 + : double.parse(latitudeController.text), + longitude: longitudeController.text.isEmpty + ? 0.0 + : double.parse(longitudeController.text), + id: addressID, + userID: authController.currentUser.value!.id, + ); + + try { + await addressesRef.doc(addressID).set(address.toMap()); + + if (authController.currentUser.value!.defaultAddressID.isEmpty) { + await authController.updateDefaultAddressID(addressID); + authController.currentUser.value = authController.currentUser.value! + .copyWith(defaultAddressID: addressID); + } + + Get.offAllNamed(Routes.ROOT); + } catch (e) { + Get.snackbar('Error', 'Failed to save address: $e'); + } + } + + Future setDefaultAddress(AddressModel address) async { + if (authController.currentUser.value == null) { + Get.snackbar('Error', 'User is not authenticated'); + return; + } + + showLoader(); + await authController.updateDefaultAddressID(address.id); + await fetchAddresses(); + dismissLoader(); + } + + Future deleteAddress(String id) async { + try { + await addressesRef.doc(id).delete(); + addresses.removeWhere((address) => address.id == id); + } catch (e) { + Get.snackbar('Error', 'Failed to delete address: $e'); + } + } + + @override + void onClose() { + line1Controller.dispose(); + line2Controller.dispose(); + cityController.dispose(); + districtController.dispose(); + latitudeController.dispose(); + longitudeController.dispose(); + super.onClose(); + } +} diff --git a/lib/app/modules/profile/controllers/contact_controller.dart b/lib/app/modules/profile/controllers/contact_controller.dart new file mode 100644 index 00000000..88d36c18 --- /dev/null +++ b/lib/app/modules/profile/controllers/contact_controller.dart @@ -0,0 +1,77 @@ +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/contact_enquiry_model.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class ContactController extends GetxController { + final AuthController authController = Get.find(); + final CollectionReference supportRef = + FirebaseFirestore.instance.collection('support'); + + var enquiries = [].obs; + var filteredEnquiries = [].obs; + var isLoading = false.obs; + var selectedTab = 'Completed'.obs; + + @override + void onInit() { + super.onInit(); + getEnquiries(); + } + + Future getEnquiries() async { + if (authController.user == null) return; + + isLoading(true); + try { + final snapshot = await supportRef + .where('userID', isEqualTo: authController.user!.id) + .orderBy('timestamp', descending: true) + .get(); + + enquiries.assignAll(snapshot.docs + .map((doc) => + ContactEnquiryModel.fromMap(doc.data() as Map)) + .toList()); + + filterEnquiries(); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch enquiries'); + } finally { + isLoading(false); + } + } + + void filterEnquiries() { + EnquiryStatus status; + switch (selectedTab.value) { + case 'Pending': + status = EnquiryStatus.pending; + break; + case 'In-Progress': + status = EnquiryStatus.inProgress; + break; + default: + status = EnquiryStatus.completed; + } + + filteredEnquiries.value = + enquiries.where((enquiry) => enquiry.status == status).toList(); + } + + void changeTab(String tab) { + selectedTab.value = tab; + filterEnquiries(); + } + + Future addEnquiry(ContactEnquiryModel enquiry) async { + try { + await supportRef.doc(enquiry.id).set(enquiry.toMap()); + enquiries.insert(0, enquiry); + filterEnquiries(); + } catch (e) { + Get.snackbar('Error', 'Failed to submit enquiry'); + } + } +} diff --git a/lib/app/modules/profile/controllers/profile_controller.dart b/lib/app/modules/profile/controllers/profile_controller.dart deleted file mode 100644 index 0c1e059e..00000000 --- a/lib/app/modules/profile/controllers/profile_controller.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:io'; - -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:firebase_storage/firebase_storage.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; - -import 'package:path/path.dart'; -import '../../../../services/auth_service.dart'; - -class ProfileController extends GetxController { - FirebaseStorage storage = FirebaseStorage.instance; - User? currentUser = AuthService.to.user; - final Rxn _photoURL = Rxn(); - - File? _photo; - - String? get photoURL => _photoURL.value; - - @override - onInit() { - super.onInit(); - _photoURL.value = currentUser!.photoURL; - _photoURL.bindStream(currentUser!.photoURL.obs.stream); - } - - Future uploadFile(String path) async { - try { - var byt = GetStorage().read(path); - if (byt != null) { - final fileName = path; - final destination = 'profilePics/${currentUser!.uid}'; - - final ref = storage.ref(destination).child(fileName); - await ref.putData(byt); - return "$destination/$fileName"; - } else { - _photo = File(path); - if (_photo == null) return null; - final fileName = basename(_photo!.path); - final destination = 'profilePics/${currentUser!.uid}'; - - final ref = storage.ref(destination).child(fileName); - await ref.putFile(_photo!); - return "$destination/$fileName"; - } - } catch (e) { - Get.snackbar('Error', 'Image Not Uploaded as ${e.toString()}'); - } - return null; - } - - void logout() { - AuthService.to.logout(); - } - - Future updatePhotoURL(String dest) async { - _photoURL.value = await storage.ref().child(dest).getDownloadURL(); - await currentUser?.updatePhotoURL(_photoURL.value); - Get.snackbar('Success', 'Picture stored and linked'); - } -} diff --git a/lib/app/modules/profile/views/account_detail.dart b/lib/app/modules/profile/views/account_detail.dart new file mode 100644 index 00000000..609f2e84 --- /dev/null +++ b/lib/app/modules/profile/views/account_detail.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_bottom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_phone_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class AccountDetailsScreen extends StatefulWidget { + const AccountDetailsScreen({super.key}); + + @override + AccountDetailsScreenState createState() => AccountDetailsScreenState(); +} + +class AccountDetailsScreenState extends State { + final _fullNameController = TextEditingController(); + final _emailController = TextEditingController(); + final _phoneNumberController = TextEditingController(); + final _businessNameController = TextEditingController(); + final _businessTINController = TextEditingController(); + final _businessVATController = TextEditingController(); + String? selectedBusinessType; + + final List businessTypes = ['Fashion', 'Electronics', 'Groceries']; + + @override + void initState() { + super.initState(); + init(); + } + + void init() { + final authController = Get.find(); + final user = authController.user; + if (user == null) return; + _fullNameController.text = user.name; + _emailController.text = user.email ?? ''; + _phoneNumberController.text = user.phoneNumber; + _businessNameController.text = user.businessName ?? ''; + _businessTINController.text = user.gstNumber ?? ''; + _businessVATController.text = user.panNumber ?? ''; + selectedBusinessType = + businessTypes.contains(user.businessType) ? user.businessType : null; + } + + void _updateProfile() { + showLoader(); + final authController = Get.find(); + final user = authController.user; + + if (user == null) return; + + final newUser = user.copyWith( + name: _fullNameController.text, + email: _emailController.text.isNotEmpty ? _emailController.text : null, + businessName: _businessNameController.text, + businessType: selectedBusinessType, + gstNumber: _businessTINController.text, + panNumber: _businessVATController.text, + ); + + authController.registerUser(newUser); + + dismissLoader(); + Get.back(); + } + + @override + void dispose() { + _fullNameController.dispose(); + _emailController.dispose(); + _phoneNumberController.dispose(); + _businessNameController.dispose(); + _businessTINController.dispose(); + _businessVATController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_sharp, + color: AppTheme.greyTextColor), + onPressed: () { + Get.back(); + }, + ), + title: Text( + 'Account Details', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.greyTextColor, + ), + ), + ), + body: Padding( + padding: const EdgeInsets.all(AppTheme.spacingDefault), + child: GetBuilder( + builder: (authController) { + if (authController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + return ListView( + children: [ + const Text( + 'Personal Details', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Name', + controller: _fullNameController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Email (Optional)', + controller: _emailController, + ), + const Spacing(size: AppTheme.spacingSmall), + PhoneTextField( + hintText: 'Enter 9-digit mobile number', + readOnly: true, + controller: _phoneNumberController, + ), + const Spacing(size: AppTheme.spacingDefault), + const Text( + 'Business Details', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Business Name', + controller: _businessNameController, + ), + const Spacing(size: AppTheme.spacingSmall), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Business Type', + border: AppTheme.textfieldUnderlineBorder, + ), + value: selectedBusinessType, + items: businessTypes + .map((label) => DropdownMenuItem( + value: label, + child: Text(label), + )) + .toList(), + onChanged: (value) { + setState(() { + selectedBusinessType = value; + }); + }, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'TIN Number (Optional)', + controller: _businessTINController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'VRN Number (Optional)', + controller: _businessVATController, + ), + const Spacing(size: AppTheme.spacingMedium), + ], + ); + }, + ), + ), + bottomNavigationBar: BottomAppBar( + elevation: 0, + color: AppTheme.colorWhite, + child: CustomBottomButton( + label: 'Save Changes', + onPressed: _updateProfile, + ), + ), + ); + } +} diff --git a/lib/app/modules/profile/views/add_addresses.dart b/lib/app/modules/profile/views/add_addresses.dart new file mode 100644 index 00000000..93aac1d1 --- /dev/null +++ b/lib/app/modules/profile/views/add_addresses.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class AddAddressScreen extends StatelessWidget { + final AddressController controller = Get.put(AddressController()); + + AddAddressScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: AppTheme.primaryGradient, + ), + padding: AppTheme.paddingDefault, + child: Center( + child: SingleChildScrollView( + child: Card( + shape: AppTheme.rrShape, + elevation: 10, + shadowColor: AppTheme.colorBlack.withOpacity(0.15), + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Address Details', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingMedium), + CustomTextField( + labelText: 'Address Line 1', + controller: controller.line1Controller, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Address Line 2', + controller: controller.line2Controller, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'City', + controller: controller.cityController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'District', + controller: controller.districtController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Latitude (optional)', + keyboardType: TextInputType.number, + controller: controller.latitudeController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Longitude (optional)', + keyboardType: TextInputType.number, + controller: controller.longitudeController, + ), + const Spacing(size: AppTheme.spacingExtraLarge), + CustomButton( + onPressed: controller.saveAddress, // Use saveAddress here + text: 'Save Address', + isDisabled: false, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/profile/views/contact_support.dart b/lib/app/modules/profile/views/contact_support.dart new file mode 100644 index 00000000..155b663d --- /dev/null +++ b/lib/app/modules/profile/views/contact_support.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/contact_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_bottom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_dropdown.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_toast.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/contact_enquiry_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; +import 'package:get_flutter_fire/utils/get_reference.dart'; +import 'package:get_flutter_fire/utils/get_uuid.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class SupportScreen extends StatelessWidget { + final ContactController contactController = Get.put(ContactController()); + final AuthController authController = Get.find(); + + final TextEditingController _messageController = TextEditingController(); + final Rx _selectedQueryType = Rx(null); + + SupportScreen({super.key}); + + @override + Widget build(BuildContext context) { + final List> supportOptions = [ + { + 'imagePath': iconPhone, + 'text': '+91 9324366823', + 'onTap': () {}, + }, + { + 'imagePath': iconMail, + 'text': 'basedharsh@gmail.com', + 'onTap': () {}, + }, + { + 'imagePath': iconWhatsapp, + 'text': '+91 9324366823', + 'onTap': () {}, + }, + { + 'imagePath': iconLocation, + 'text': 'Mumbai, India...', + 'onTap': () {}, + }, + ]; + + void submitEnquiry() async { + if (_selectedQueryType.value == null) { + showToast("Please select a query type"); + return; + } + + if (_messageController.text.trim().isEmpty) { + showToast("Please enter a message"); + return; + } + + showLoader(); + + try { + String successMessage = "Enquiry submitted successfully"; + String id = getUUID(); + + ContactEnquiryModel contactEnquiry = ContactEnquiryModel( + id: id, + message: _messageController.text.trim(), + timestamp: DateTime.now(), + userID: authController.user!.id, + queryType: _selectedQueryType.value!, + reference: getReference(), + status: EnquiryStatus.pending, + ); + + await contactController.addEnquiry(contactEnquiry); + + _messageController.clear(); + _selectedQueryType.value = null; + + showToast(successMessage); + } catch (e) { + showToast("Failed to submit enquiry. Please try again."); + } finally { + dismissLoader(); + } + } + + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_sharp, + color: AppTheme.greyTextColor, size: 18), + onPressed: () { + Get.back(); + }, + ), + title: Text( + 'Support', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.greyTextColor, + ), + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: AppTheme.paddingDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListView.builder( + shrinkWrap: true, + itemCount: supportOptions.length, + itemBuilder: (context, index) { + final option = supportOptions[index]; + return Column( + children: [ + ListTile( + leading: Image.asset(option['imagePath'] as String, + height: 24.0, width: 24.0), + title: Text(option['text'] as String, + style: AppTheme.fontStyleDefault), + trailing: const Icon(Icons.arrow_forward_ios, + size: 16, color: AppTheme.greyTextColor), + onTap: option['onTap'] as VoidCallback, + ), + if (index != supportOptions.length - 1) + const Spacing(size: AppTheme.spacingTiny), + const Divider(height: 0.1, color: AppTheme.greyTextColor), + ], + ); + }, + ), + const Spacing(size: AppTheme.spacingLarge), + Text('Send Query', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + )), + const Spacing(size: AppTheme.spacingSmall), + Obx(() { + return CustomDropdown( + hintText: 'Select Query Type', + items: QueryType.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.toString().split('.').last), + )) + .toList(), + onChanged: (QueryType? val) { + _selectedQueryType.value = val; + }, + value: _selectedQueryType.value, + ); + }), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Write your query', + controller: _messageController, + ), + const Spacing(size: AppTheme.spacingDefault), + CustomBottomButton( + label: 'Submit', + onPressed: submitEnquiry, + ), + const Spacing(size: AppTheme.spacingSmall), + Center( + child: TextButton( + onPressed: () { + Get.toNamed(Routes.PAST_QUERIES); + }, + child: Text( + 'Visit Past Queries', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorRed, + decoration: TextDecoration.underline, + ), + ), + ), + ), + const Spacing(size: AppTheme.spacingSmall), + ], + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/profile/views/manage_address.dart b/lib/app/modules/profile/views/manage_address.dart new file mode 100644 index 00000000..b6f0937f --- /dev/null +++ b/lib/app/modules/profile/views/manage_address.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_bottom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/app/widgets/profile/address_container.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class ManageAddressScreen extends StatelessWidget { + ManageAddressScreen({super.key}); + + final AddressController addressController = Get.put(AddressController()); + final AuthController authController = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_sharp, + color: AppTheme.greyTextColor, size: 18), + onPressed: () { + Get.toNamed(Routes.ROOT); + }, + ), + title: Text( + 'Manage Address', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.greyTextColor, + ), + ), + ), + body: Obx(() { + if (addressController.isLoading.value) { + return const LoadingWidget(); + } + + if (addressController.addresses.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'No address found.', + style: TextStyle(fontSize: 18, color: Colors.grey), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + Get.toNamed(Routes.ADD_ADDRESS); + }, + child: const Text('Add Address'), + ), + ], + ), + ); + } + + return Padding( + padding: AppTheme.paddingSmall, + child: SingleChildScrollView( + child: Column( + children: [ + ListView.builder( + padding: EdgeInsets.zero, + itemCount: addressController.addresses.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + AddressModel address = addressController.addresses[index]; + bool isDefault = address.id == + authController.currentUser.value?.defaultAddressID; + + return Column( + children: [ + if (index != 0) + const SizedBox(height: AppTheme.spacingTiny), + AddressContainer( + address: address, + onDelete: (id) { + showLoader(); + addressController.deleteAddress(id).then((_) { + dismissLoader(); + }); + }, + isDefault: isDefault, + onSetAsDefault: (id) { + showLoader(); + addressController + .setDefaultAddress(address) + .then((_) { + dismissLoader(); + }); + }, + ), + const Divider( + color: AppTheme.greyTextColor, + height: 0.1, + ), + ], + ); + }, + ), + ], + ), + ), + ); + }), + bottomNavigationBar: BottomAppBar( + elevation: 0, + color: AppTheme.colorWhite, + child: CustomBottomButton( + label: 'Add New Address', + onPressed: () { + Get.toNamed(Routes.ADD_ADDRESS); + }, + ), + ), + ); + } +} diff --git a/lib/app/modules/profile/views/past_queries_screen.dart b/lib/app/modules/profile/views/past_queries_screen.dart new file mode 100644 index 00000000..f07f8b31 --- /dev/null +++ b/lib/app/modules/profile/views/past_queries_screen.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/contact_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/utils/months.dart'; + +class PastQueriesScreen extends StatelessWidget { + final ContactController contactController = Get.put(ContactController()); + + PastQueriesScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_sharp, + color: AppTheme.greyTextColor, size: 18), + onPressed: () { + Get.back(); + }, + ), + title: Text( + 'Past Queries', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + ), + body: Padding( + padding: AppTheme.paddingDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + _buildTabButton('Pending'), + const Spacing( + size: AppTheme.spacingDefault, + isHorizontal: true, + ), + _buildTabButton('In-Progress'), + const Spacing( + size: AppTheme.spacingDefault, + isHorizontal: true, + ), + _buildTabButton('Completed'), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Expanded( + child: Obx(() { + if (contactController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (contactController.filteredEnquiries.isEmpty) { + return const Center(child: Text('No queries found.')); + } + + return ListView.builder( + itemCount: contactController.filteredEnquiries.length, + itemBuilder: (context, index) { + final enquiry = contactController.filteredEnquiries[index]; + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.insert_drive_file, + color: AppTheme.greyTextColor), + const Spacing( + size: AppTheme.spacingSmall, + isHorizontal: true), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(enquiry.message, + style: AppTheme.fontStyleDefaultBold), + const Spacing(size: AppTheme.spacingTiny), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 4.0, horizontal: 8.0), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(6.0), + border: Border.all( + color: AppTheme.colorRed), + ), + child: Text(enquiry.queryType.name, + style: AppTheme.fontStyleDefault + .copyWith( + color: AppTheme.colorRed, + )), + ), + const Spacing( + size: AppTheme.spacingSmall), + Text( + '${enquiry.timestamp.day} ${monthString(enquiry.timestamp.month)}, ${enquiry.timestamp.year}, ${enquiry.timestamp.hour}:${enquiry.timestamp.minute} ${enquiry.timestamp.hour >= 12 ? "PM" : "AM"}', + style: AppTheme.fontStyleDefault, + ), + ], + ), + ], + ), + ), + ], + ), + const Divider( + height: 32, color: AppTheme.greyTextColor), + ], + ); + }, + ); + }), + ), + ], + ), + ), + ); + } + + Widget _buildTabButton(String tab) { + return Expanded( + child: Obx(() { + final isSelected = contactController.selectedTab.value == tab; + return InkWell( + onTap: () { + contactController.changeTab(tab); + }, + child: Container( + padding: AppTheme.paddingSmall, + decoration: BoxDecoration( + color: isSelected ? AppTheme.colorRed : AppTheme.colorWhite, + borderRadius: BorderRadius.circular(12.0), + border: Border.all( + color: isSelected ? AppTheme.colorRed : AppTheme.greyTextColor, + ), + ), + child: Center( + child: Text( + tab, + style: AppTheme.fontStyleDefaultBold.copyWith( + fontSize: 12, + color: isSelected ? AppTheme.colorWhite : AppTheme.colorBlack, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ), + ); + }), + ); + } +} diff --git a/lib/app/modules/profile/views/profile.dart b/lib/app/modules/profile/views/profile.dart new file mode 100644 index 00000000..de378bd4 --- /dev/null +++ b/lib/app/modules/profile/views/profile.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/profile/profile_list_widget.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + final AuthController authController = Get.find(); + + String getInitials(String name) { + List nameParts = name.split(' '); + String initials = ''; + if (nameParts.isNotEmpty) { + initials = nameParts.map((part) => part[0].toUpperCase()).join(); + } + return initials; + } + + final List> profileItems = [ + { + 'imagePath': iconProfile, + 'text': 'Account Details', + 'onTap': () { + Get.toNamed(Routes.ACCOUNT_DETAILS); + } + }, + { + 'imagePath': iconLocation, + 'text': 'Manage Address', + 'onTap': () { + Get.toNamed(Routes.MANAGE_ADDRESS, + arguments: {'user': authController.currentUser.value}); + } + }, + { + 'imagePath': iconFile, + 'text': 'Terms and Conditions', + 'onTap': () {}, + }, + { + 'imagePath': iconFile, + 'text': 'Privacy Policy', + 'onTap': () {}, + }, + { + 'imagePath': iconSupport, + 'text': 'Support', + 'onTap': () { + Get.toNamed(Routes.CONTACT); + }, + }, + { + 'imagePath': iconSignout, + 'text': 'Sign Out', + 'onTap': () async { + showLoader(); // Show the loader + authController.clearUserData(); // Clear user data + Get.offAllNamed(Routes.WELCOME); // Navigate to welcome screen + dismissLoader(); // Dismiss the loader + }, + }, + ]; + + return Scaffold( + backgroundColor: AppTheme.colorRed, + body: Column( + children: [ + const Spacing(size: AppTheme.spacingMedium), + Container( + padding: AppTheme.paddingDefault, + child: Row( + children: [ + Container( + decoration: BoxDecoration( + color: AppTheme.colorWhite, + borderRadius: BorderRadius.circular(6), + ), + width: 60, + height: 60, + child: Center( + child: Obx(() { + final user = authController.user; + return Text( + user != null ? getInitials(user.name) : 'JD', + style: AppTheme.fontStyleLarge + .copyWith(color: AppTheme.colorBlack), + ); + }), + ), + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + final user = authController.user; + return Text( + user?.name ?? 'Unknown User', + style: AppTheme.fontStyleMedium + .copyWith(color: AppTheme.colorWhite), + ); + }), + const SizedBox(height: AppTheme.spacingTiny), + Obx(() { + final user = authController.user; + return Text( + user?.phoneNumber ?? '+225 123 456 789', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorWhite), + ); + }), + ], + ), + ], + ), + ), + Expanded( + child: Container( + padding: AppTheme.paddingTiny, + decoration: const BoxDecoration( + color: AppTheme.colorWhite, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + child: Column( + children: [ + Expanded( + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: profileItems.length, + itemBuilder: (context, index) { + final item = profileItems[index]; + return Column( + children: [ + if (index == 0) + const Spacing(size: AppTheme.spacingTiny), + ProfileListItem( + imagePath: item['imagePath'], + text: item['text'], + onTap: item['onTap'], + ), + ], + ); + }, + ), + ), + Padding( + padding: AppTheme.paddingDefault, + child: RichText( + text: TextSpan( + style: AppTheme.fontStyleDefaultBold + .copyWith(color: AppTheme.greyTextColor), + children: [ + const TextSpan(text: 'Designed & Developed by '), + TextSpan( + text: 'BasedHarsh', + style: AppTheme.fontStyleDefaultBold + .copyWith(color: AppTheme.colorRed), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/app/modules/profile/views/profile_view.dart b/lib/app/modules/profile/views/profile_view.dart deleted file mode 100644 index c26d11c1..00000000 --- a/lib/app/modules/profile/views/profile_view.dart +++ /dev/null @@ -1,124 +0,0 @@ -// ignore_for_file: inference_failure_on_function_invocation - -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../../../services/auth_service.dart'; -import '../../../../models/screens.dart'; -import '../../../widgets/change_password_dialog.dart'; -import '../../../widgets/image_picker_button.dart'; -import '../controllers/profile_controller.dart'; - -class ProfileView extends GetView { - const ProfileView({super.key}); - ShapeBorder get shape => const CircleBorder(); - double get size => 120; - Color get placeholderColor => Colors.grey; - - Widget _imageFrameBuilder( - BuildContext context, - Widget? child, - int? frame, - bool? _, - ) { - if (frame == null) { - return Container(color: placeholderColor); - } - - return child!; - } - - @override - Widget build(BuildContext context) { - return Obx(() => profileScreen()); - } - - Widget profileScreen() { - return AuthService.to.isLoggedInValue - ? ProfileScreen( - // We are using the Flutter Fire Profile Screen now but will change in subsequent steps. - // The issues are highlighted in comments here - - // appBar: AppBar( - // title: const Text('User Profile'), - // ), - avatar: SizedBox( - //null will give the profile image component but it does not refresh the pic when changed - height: size, - width: size, - child: ClipPath( - clipper: ShapeBorderClipper(shape: shape), - clipBehavior: Clip.hardEdge, - child: controller.photoURL != null - ? Image.network( - controller.photoURL!, - width: size, - height: size, - cacheWidth: size.toInt(), - cacheHeight: size.toInt(), - fit: BoxFit.contain, - frameBuilder: _imageFrameBuilder, - ) - : Center( - child: Image.asset( - 'assets/images/dash.png', - width: size, - fit: BoxFit.contain, - ), - ), - ), - ), - // showDeleteConfirmationDialog: true, //this does not work properly. Possibly a bug in FlutterFire - actions: [ - SignedOutAction((context) { - Get.back(); - controller.logout(); - Get.rootDelegate.toNamed(Screen.PROFILE.route); - // Navigator.of(context).pop(); - }), - AccountDeletedAction((context, user) { - //If we don't include this the button is still shown but no action gets done. Ideally the button should also not be shown. Its a bug in FlutterFire - Get.defaultDialog( - //this is only called after the delete is done and not useful for confirmation of the delete action - title: 'Deleted Account of ${user.displayName}', - barrierDismissible: true, - navigatorKey: Get.nestedKey(Screen.HOME.route), - ); - }) - ], - children: [ - //This is to show that we can add custom content here - const Divider(), - controller.currentUser?.email != null - ? TextButton.icon( - onPressed: callChangePwdDialog, - label: const Text('Change Password'), - icon: const Icon(Icons.password_rounded), - ) - : const SizedBox.shrink(), - ImagePickerButton(callback: (String? path) async { - if (path != null) { - //Upload to Store - String? dest = await controller.uploadFile(path); - //attach it to User imageUrl - if (dest != null) { - await controller.updatePhotoURL(dest); - } - } - }) - ], - ) - : const Scaffold(); - } - - void callChangePwdDialog() { - var dlg = ChangePasswordDialog(controller.currentUser!); - Get.defaultDialog( - title: "Change Password", - content: dlg, - textConfirm: "Submit", - textCancel: "Cancel", - onConfirm: dlg.onSubmit); - } -} diff --git a/lib/app/modules/register/bindings/register_binding.dart b/lib/app/modules/register/bindings/register_binding.dart deleted file mode 100644 index 1089ecbd..00000000 --- a/lib/app/modules/register/bindings/register_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/register_controller.dart'; - -class RegisterBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => RegisterController(), - ); - } -} diff --git a/lib/app/modules/register/controllers/register_controller.dart b/lib/app/modules/register/controllers/register_controller.dart deleted file mode 100644 index 96cb2cb0..00000000 --- a/lib/app/modules/register/controllers/register_controller.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:get/get.dart'; - -import '../../../../services/auth_service.dart'; - -class RegisterController extends GetxController { - @override - void onInit() { - super.onInit(); - // Send email verification and logout - AuthService.to - .sendVerificationMail(); //if we use the EmailVerificationScreen then no need to call this - } - - // @override - // void onReady() { - // super.onReady(); - // } - - // @override - // void onClose() { - // super.onClose(); - // } -} diff --git a/lib/app/modules/register/views/register_view.dart b/lib/app/modules/register/views/register_view.dart deleted file mode 100644 index 01f73e88..00000000 --- a/lib/app/modules/register/views/register_view.dart +++ /dev/null @@ -1,53 +0,0 @@ -// import 'package:firebase_ui_auth/firebase_ui_auth.dart'; -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - -import '../../../../services/auth_service.dart'; -// import '../../../widgets/login_widgets.dart'; -import '../controllers/register_controller.dart'; - -//ALso add a form to take additional info such as display name of other customer details mapped with uid in Firestore -class RegisterView extends GetView { - const RegisterView({super.key}); - - @override - Widget build(BuildContext context) { - // Add pre verification Form if any. Mostly it can be post verification and can be the Profile or Setting screens - try { - // using this is causing an error when we send verification mail from server side - // if it was initiated once, even when no visible. So we need to dispose when not visible - var w = - // EmailVerificationScreen( - // headerBuilder: LoginWidgets.headerBuilder, - // sideBuilder: LoginWidgets.sideBuilder, - // actions: [ - // EmailVerifiedAction(() { - // AuthService.to.register(); - // }), - // ], - // ); - Scaffold( - appBar: AppBar( - title: const Text('Registeration'), - centerTitle: true, - ), - body: Center( - child: Column(children: [ - const Text( - 'Please verify your email (check SPAM folder), and then relogin', - style: TextStyle(fontSize: 20), - ), - TextButton( - onPressed: () => AuthService.to.register(), - child: const Text("Verification Done. Relogin"), - ) - ])), - ); - return w; - } catch (e) { - // TODO - } - return const Scaffold(); - } -} diff --git a/lib/app/modules/root/bindings/root_binding.dart b/lib/app/modules/root/bindings/root_binding.dart deleted file mode 100644 index e1e94d1d..00000000 --- a/lib/app/modules/root/bindings/root_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/root_controller.dart'; - -class RootBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => RootController(), - ); - } -} diff --git a/lib/app/modules/root/controllers/my_drawer_controller.dart b/lib/app/modules/root/controllers/my_drawer_controller.dart deleted file mode 100644 index f45ef122..00000000 --- a/lib/app/modules/root/controllers/my_drawer_controller.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; - -import '../../../../models/screens.dart'; - -class MyDrawerController extends GetxController { - MyDrawerController(Iterable iter) - : values = Rx>(iter); - - final Rx> values; -} diff --git a/lib/app/modules/root/controllers/root_controller.dart b/lib/app/modules/root/controllers/root_controller.dart index 7f160fc6..3c534f09 100644 --- a/lib/app/modules/root/controllers/root_controller.dart +++ b/lib/app/modules/root/controllers/root_controller.dart @@ -1,14 +1,23 @@ -import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; class RootController extends GetxController { - GlobalKey scaffoldKey = GlobalKey(); + var selectedIndex = 0.obs; - void openDrawer() { - scaffoldKey.currentState!.openDrawer(); + final AuthController authController = Get.find(); + + @override + void onInit() { + super.onInit(); + + if (authController.user?.userType == UserType.seller) { + Get.put(SellerController()); + } } - void closeDrawer() { - scaffoldKey.currentState!.openEndDrawer(); + void changeTabIndex(int index) { + selectedIndex.value = index; } } diff --git a/lib/app/modules/root/root_view.dart b/lib/app/modules/root/root_view.dart new file mode 100644 index 00000000..5d6f67f5 --- /dev/null +++ b/lib/app/modules/root/root_view.dart @@ -0,0 +1,278 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/admin/views/admin_screen.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/home/view/home.dart'; +import 'package:get_flutter_fire/app/modules/orders/views/orders.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/profile.dart'; +import 'package:get_flutter_fire/app/modules/root/controllers/root_controller.dart'; +import 'package:get_flutter_fire/app/modules/seller/views/seller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_toast.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class RootView extends StatelessWidget { + final AuthController authController; + final RootController rootController; + + RootView({super.key}) + : authController = Get.find(), + rootController = Get.put(RootController()); + + @override + Widget build(BuildContext context) { + return Obx(() { + if (authController.user == null) { + Future.microtask(() => Get.offAllNamed(Routes.WELCOME)); + return const SizedBox.shrink(); + } + + final showAppBar = rootController.selectedIndex.value != 2; + + return Scaffold( + appBar: showAppBar ? _buildAppBar() : null, + body: _getTabContent(rootController.selectedIndex.value), + bottomNavigationBar: _buildBottomNavigationBar(), + ); + }); + } + + AppBar _buildAppBar() { + return AppBar( + automaticallyImplyLeading: false, + title: _buildAppBarTitle(), + backgroundColor: AppTheme.colorRed, + actions: _buildAppBarActions(), + ); + } + + Widget _buildAppBarTitle() { + final user = authController.user!; + return Padding( + padding: const EdgeInsets.only(bottom: AppTheme.spacingTiny), + child: Row( + children: [ + _getUserRoleIcon(user.userType), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Welcome,', + style: TextStyle(fontSize: 14, color: Colors.white)), + Text( + user.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + if (user.userType == UserType.seller) const SizedBox(width: 10), + if (user.userType == UserType.seller) + const Icon(Icons.verified, color: Colors.yellowAccent), + ], + ), + ); + } + + List _buildAppBarActions() { + return [ + IconButton( + icon: const Icon(Icons.shopping_cart, color: Colors.white), + onPressed: () {}, + ), + if (authController.user!.userType == UserType.seller) + IconButton( + icon: const Icon(Icons.store, color: Colors.white), + onPressed: () { + // Navigate to seller-specific screens + }, + ), + IconButton( + icon: const Icon(Icons.logout, color: Colors.white), + onPressed: () { + authController.clearUserData(); + Get.offAllNamed(Routes.WELCOME); + }, + ), + ]; + } + + Widget _getUserRoleIcon(UserType userType) { + IconData iconData; + if (userType == UserType.seller) { + iconData = Icons.storefront; + } else if (userType == UserType.admin) { + iconData = Icons.admin_panel_settings; + } else { + iconData = Icons.shopping_bag; + } + + return CircleAvatar( + backgroundColor: Colors.white, + child: Icon( + iconData, + color: AppTheme.colorRed, + size: 28, + ), + ); + } + + Widget _getTabContent(int index) { + final guestTabs = [ + const HomeScreen(), // Allow guest to view only the HomeScreen + ]; + + final buyerTabs = [ + const HomeScreen(), + const OrdersScreen(), + const ProfileScreen(), + ]; + + final sellerTabs = [ + const HomeScreen(), + const OrdersScreen(), + const ProfileScreen(), + SellerPage(), + ]; + + final adminTabs = [ + const HomeScreen(), + const OrdersScreen(), + const ProfileScreen(), + const AdminScreen(), + ]; + + final userType = authController.user!.userType; + + // Restrict access for guest users + if (userType == UserType.guest) { + return guestTabs[0]; + } else if (userType == UserType.admin) { + return adminTabs[index]; + } else if (userType == UserType.seller) { + return sellerTabs[index]; + } else { + return buyerTabs[index]; + } + } + + Widget _buildBottomNavigationBar() { + return Obx(() { + final items = _buildBottomNavItems(); + + return Container( + padding: const EdgeInsets.only(bottom: AppTheme.spacingTiny), + decoration: BoxDecoration( + color: AppTheme.colorWhite, + boxShadow: AppTheme.bottomBoxShadow, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: items.map((item) { + return Expanded( + child: CustomBottomNavigationBarItem( + icon: item.icon, + label: item.label!, + index: items.indexOf(item), + currentIndex: rootController.selectedIndex.value, + onTap: (index) { + if (authController.user!.userType == UserType.guest && + index != 0) { + showToast( + 'Please log in or register to access this feature.'); + Get.offAllNamed(Routes.WELCOME); + } else { + rootController.changeTabIndex(index); + } + }, + ), + ); + }).toList(), + ), + const SizedBox(height: AppTheme.spacingTiny), + ], + ), + ); + }); + } + + List _buildBottomNavItems() { + final items = [ + const BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), + const BottomNavigationBarItem( + icon: Icon(Icons.shopping_cart), label: 'Orders'), + const BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), + ]; + + if (authController.user!.userType == UserType.seller) { + items.add(const BottomNavigationBarItem( + icon: Icon(Icons.store), label: 'Sell')); + } else if (authController.user!.userType == UserType.admin) { + items.add(const BottomNavigationBarItem( + icon: Icon(Icons.admin_panel_settings), label: 'Admin')); + } + + return items; + } +} + +class CustomBottomNavigationBarItem extends StatelessWidget { + final Widget icon; + final String label; + final int index; + final int currentIndex; + final Function(int) onTap; + + const CustomBottomNavigationBarItem({ + super.key, + required this.icon, + required this.label, + required this.index, + required this.currentIndex, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final isSelected = currentIndex == index; + + return GestureDetector( + onTap: () => onTap(index), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isSelected) + Container( + height: 4, + width: 40, + color: AppTheme.colorRed, + ), + const SizedBox(height: AppTheme.spacingTiny), + IconTheme( + data: IconThemeData( + color: isSelected ? AppTheme.colorRed : AppTheme.greyTextColor, + size: 28, + ), + child: icon, + ), + const SizedBox(height: AppTheme.spacingTiny), + Text( + label, + style: TextStyle( + color: isSelected ? AppTheme.colorRed : AppTheme.greyTextColor, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ), + ); + } +} diff --git a/lib/app/modules/root/views/drawer.dart b/lib/app/modules/root/views/drawer.dart deleted file mode 100644 index 908d0223..00000000 --- a/lib/app/modules/root/views/drawer.dart +++ /dev/null @@ -1,118 +0,0 @@ -// ignore_for_file: inference_failure_on_function_invocation - -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../../../models/role.dart'; -import '../../../../services/auth_service.dart'; - -import '../../../../models/screens.dart'; -import '../controllers/my_drawer_controller.dart'; - -class DrawerWidget extends StatelessWidget { - const DrawerWidget({ - super.key, - }); - - @override - Widget build(BuildContext context) { - MyDrawerController controller = Get.put(MyDrawerController([]), - permanent: true); //must make true else gives error - Screen.drawer().then((v) => {controller.values.value = v}); - return Obx(() => Drawer( - //changing the shape of the drawer - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(0), bottomRight: Radius.circular(20)), - ), - width: 200, - child: Column( - children: drawerItems(context, controller.values), - ), - )); - } - - List drawerItems(BuildContext context, Rx> values) { - List list = [ - Container( - height: 100, - color: Colors.red, - //adding content in the highlighted part of the drawer - child: Align( - alignment: Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.only(left: 15), - child: const Text('User Name', //Profile Icon also - style: TextStyle(fontWeight: FontWeight.bold)))), - ) - ]; - - if (AuthService.to.maxRole.index > 1) { - for (var i = 0; i <= AuthService.to.maxRole.index; i++) { - Role role = Role.values[i]; - list.add(ListTile( - title: Text( - role.name, - style: const TextStyle( - color: Colors.blue, - ), - ), - onTap: () { - Get.rootDelegate - .toNamed(Screen.HOME.route, arguments: {'role': role}); - //to close the drawer - Navigator.of(context).pop(); - }, - )); - } - } - - for (Screen screen in values.value) { - list.add(ListTile( - title: Text(screen.label ?? ''), - onTap: () { - Get.rootDelegate.toNamed(screen.route); - //to close the drawer - - Navigator.of(context).pop(); - }, - )); - } - - if (AuthService.to.isLoggedInValue) { - list.add(ListTile( - title: const Text( - 'Logout', - style: TextStyle( - color: Colors.red, - ), - ), - onTap: () { - AuthService.to.logout(); - Get.rootDelegate.toNamed(Screen.LOGIN.route); - //to close the drawer - - Navigator.of(context).pop(); - }, - )); - } - if (!AuthService.to.isLoggedInValue) { - list.add(ListTile( - title: const Text( - 'Login', - style: TextStyle( - color: Colors.blue, - ), - ), - onTap: () { - Get.rootDelegate.toNamed(Screen.LOGIN.route); - //to close the drawer - - Navigator.of(context).pop(); - }, - )); - } - - return list; - } -} diff --git a/lib/app/modules/root/views/root_view.dart b/lib/app/modules/root/views/root_view.dart deleted file mode 100644 index 2bbf228c..00000000 --- a/lib/app/modules/root/views/root_view.dart +++ /dev/null @@ -1,64 +0,0 @@ -// ignore_for_file: inference_failure_on_function_invocation - -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:get_flutter_fire/services/auth_service.dart'; -import '../../../routes/app_pages.dart'; -import '../../../../models/screens.dart'; -import '../controllers/root_controller.dart'; -import 'drawer.dart'; - -class RootView extends GetView { - const RootView({super.key}); - - @override - Widget build(BuildContext context) { - return GetRouterOutlet.builder( - builder: (context, delegate, current) { - final title = current!.currentPage!.title; - return Scaffold( - key: controller.scaffoldKey, - drawer: const DrawerWidget(), - appBar: AppBar( - title: Text(title ?? ''), - centerTitle: true, - leading: GetPlatform.isIOS // Since Web and Android have back button - && - current.locationString.contains(RegExp(r'(\/[^\/]*){3,}')) - ? BackButton( - onPressed: () => - Get.rootDelegate.popRoute(), //Navigator.pop(context), - ) - : IconButton( - icon: ImageIcon( - const AssetImage("icons/logo.png"), - color: Colors.grey.shade800, - ), - onPressed: () => AuthService.to.isLoggedInValue - ? controller.openDrawer() - : {Screen.HOME.doAction()}, - ), - actions: topRightMenuButtons(current), - // automaticallyImplyLeading: false, //removes drawer icon - ), - body: GetRouterOutlet( - initialRoute: AppPages.INITIAL, - // anchorRoute: '/', - // filterPages: (afterAnchor) { - // return afterAnchor.take(1); - // }, - ), - ); - }, - ); - } - -//This could be used to add icon buttons in expanded web view instead of the context menu - List topRightMenuButtons(GetNavConfig current) { - return [ - Container( - margin: const EdgeInsets.only(right: 15), - child: Screen.LOGIN.widget(current)) - ]; //TODO add seach button - } -} diff --git a/lib/app/modules/seller/controllers/seller_controller.dart b/lib/app/modules/seller/controllers/seller_controller.dart new file mode 100644 index 00000000..c0fcee11 --- /dev/null +++ b/lib/app/modules/seller/controllers/seller_controller.dart @@ -0,0 +1,181 @@ +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/category_model.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/models/seller_model.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:uuid/uuid.dart'; + +class SellerController extends GetxController { + final AuthService authService = Get.find(); + var products = [].obs; + var isLoading = false.obs; + + String get sellerId { + final user = authService.userID; + return "seller_$user"; + } + + Future assignSellerRole(UserModel user) async { + if (user.userType != UserType.seller) { + return; + } + + SellerModel seller = SellerModel( + id: user.id, + name: user.name, + phoneNumber: user.phoneNumber, + email: user.email, + isBusiness: user.isBusiness, + businessName: user.businessName, + businessType: user.businessType, + gstNumber: user.gstNumber, + panNumber: user.panNumber, + userType: UserType.seller, + defaultAddressID: user.defaultAddressID, + createdAt: user.createdAt, + lastSeenAt: user.lastSeenAt, + sellerId: sellerId, + products: [], + ); + + await FirebaseFirestore.instance + .collection('sellers') + .doc(sellerId) + .set(seller.toMap()); + } + + Future onUserRoleChanged(UserModel user) async { + if (user.userType == UserType.seller) { + await assignSellerRole(user); + } + } + + Future fetchSellerProducts() async { + isLoading.value = true; + try { + FirebaseFirestore.instance + .collection('products') + .where('sellerId', isEqualTo: sellerId) + .snapshots() + .listen((event) { + final fetchedProducts = + event.docs.map((doc) => ProductModel.fromMap(doc.data())).toList(); + products.assignAll(fetchedProducts); + }); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch products: $e'); + } finally { + isLoading.value = false; + } + } + + Future addSellerProduct({ + required String name, + required String description, + required int unitPrice, + required int remainingQuantity, + required int unitWeight, + required String categoryID, + required List images, + }) async { + isLoading.value = true; + try { + final productId = const Uuid().v4(); + final newProduct = ProductModel( + id: productId, + categoryID: categoryID, + name: name, + description: description, + unitPrice: unitPrice, + remainingQuantity: remainingQuantity, + unitWeight: unitWeight, + images: images, + isActive: true, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + sellerId: sellerId, + ); + + await FirebaseFirestore.instance + .collection('products') + .doc(productId) + .set(newProduct.toMap()); + + Get.snackbar('Success', 'Product added successfully'); + } catch (e) { + Get.snackbar('Error', 'Failed to add product: $e'); + } finally { + isLoading.value = false; + } + } + + Future> fetchCategories() async { + try { + final querySnapshot = + await FirebaseFirestore.instance.collection('categories').get(); + return querySnapshot.docs + .map((doc) => CategoryModel.fromMap(doc.data())) + .toList(); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch categories: $e'); + return []; + } + } + + Future updateSellerProduct({ + required String id, + required String name, + required String description, + required int unitPrice, + required int remainingQuantity, + required int unitWeight, + required List images, + }) async { + isLoading.value = true; + try { + final updatedProduct = ProductModel( + id: id, + categoryID: 'default_category', + name: name, + description: description, + unitPrice: unitPrice, + remainingQuantity: remainingQuantity, + unitWeight: unitWeight, + images: images, + isActive: true, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + sellerId: sellerId, + ); + + await FirebaseFirestore.instance + .collection('products') + .doc(id) + .update(updatedProduct.toMap()); + + Get.snackbar('Success', 'Product updated successfully'); + } catch (e) { + Get.snackbar('Error', 'Failed to update product: $e'); + } finally { + isLoading.value = false; + } + } + + Future deleteSellerProduct(String productId) async { + isLoading.value = true; + try { + await FirebaseFirestore.instance + .collection('products') + .doc(productId) + .delete(); + Get.snackbar('Success', 'Product deleted successfully'); + } catch (e) { + Get.snackbar('Error', 'Failed to delete product: $e'); + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/app/modules/seller/views/add_product.dart b/lib/app/modules/seller/views/add_product.dart new file mode 100644 index 00000000..f9eecae2 --- /dev/null +++ b/lib/app/modules/seller/views/add_product.dart @@ -0,0 +1,227 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; +import 'package:get_flutter_fire/models/category_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; + +class AddProductPage extends StatefulWidget { + const AddProductPage({super.key}); + + @override + AddProductPageState createState() => AddProductPageState(); +} + +class AddProductPageState extends State { + final SellerController sellerController = Get.find(); + final TextEditingController nameController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController priceController = TextEditingController(); + final TextEditingController quantityController = TextEditingController(); + final TextEditingController weightController = TextEditingController(); + final TextEditingController imageUrlController = TextEditingController(); + final RxList images = [].obs; + final RxBool isUploading = false.obs; + final RxString selectedCategory = ''.obs; + final RxList categories = [].obs; + + @override + void initState() { + super.initState(); + fetchCategories(); + } + + Future fetchCategories() async { + final fetchedCategories = await sellerController.fetchCategories(); + categories.assignAll(fetchedCategories); + if (fetchedCategories.isNotEmpty) { + selectedCategory.value = fetchedCategories.first.id; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Add Product'), + backgroundColor: AppTheme.colorRed, + ), + body: SafeArea( + child: Padding( + padding: AppTheme.paddingDefault, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextField( + labelText: 'Product Name', + controller: nameController, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Description', + controller: descriptionController, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Price', + controller: priceController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Quantity', + controller: quantityController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Unit Weight (grams)', + controller: weightController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + Obx(() => DropdownButton( + value: selectedCategory.value, + onChanged: (value) { + if (value != null) { + selectedCategory.value = value; + } + }, + items: categories + .map((category) => DropdownMenuItem( + value: category.id, + child: Text(category.name), + )) + .toList(), + )), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Image URL (optional)', + controller: imageUrlController, + ), + const SizedBox(height: AppTheme.spacingDefault), + _buildImagePicker(images, isUploading), + const SizedBox(height: AppTheme.spacingDefault), + ElevatedButton( + onPressed: () { + if (nameController.text.isEmpty || + descriptionController.text.isEmpty || + priceController.text.isEmpty || + quantityController.text.isEmpty || + weightController.text.isEmpty || + (images.isEmpty && imageUrlController.text.isEmpty)) { + Get.snackbar('Error', + 'Please fill all fields and add at least one image or provide an image URL.'); + return; + } + + // Add image URL to the list if it is provided + if (imageUrlController.text.isNotEmpty) { + images.add(imageUrlController.text); + } + + sellerController.addSellerProduct( + name: nameController.text, + description: descriptionController.text, + unitPrice: int.parse(priceController.text), + remainingQuantity: int.parse(quantityController.text), + unitWeight: int.parse(weightController.text), + images: images.toList(), + categoryID: selectedCategory.value, + ); + + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.colorRed, + minimumSize: const Size.fromHeight(50), + ), + child: const Text('Add Product'), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildImagePicker(RxList images, RxBool isUploading) { + return Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Product Images', style: AppTheme.fontStyleMedium), + const SizedBox(height: AppTheme.spacingSmall), + Wrap( + spacing: 10, + children: [ + ...images.map((image) => _buildImagePreview(image, images)), + _buildAddImageButton(images, isUploading), + ], + ), + if (isUploading.value) + const Padding( + padding: EdgeInsets.only(top: 16), + child: Center( + child: LoadingWidget(), + ), + ), + ], + )); + } + + Widget _buildImagePreview(String imagePath, RxList images) { + return Stack( + children: [ + Image.file( + File(imagePath), + width: 100, + height: 100, + fit: BoxFit.cover, + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => images.remove(imagePath), + child: Container( + color: Colors.black54, + child: const Icon(Icons.close, color: Colors.white, size: 20), + ), + ), + ), + ], + ); + } + + Widget _buildAddImageButton(RxList images, RxBool isUploading) { + return GestureDetector( + onTap: () async { + final ImagePicker picker = ImagePicker(); + isUploading.value = true; + try { + final pickedFile = + await picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + images.add(pickedFile.path); + } + } finally { + isUploading.value = false; + } + }, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: AppTheme.colorDisabled, + borderRadius: AppTheme.borderRadius, + ), + child: const Icon(Icons.add_a_photo, color: Colors.white), + ), + ); + } +} diff --git a/lib/app/modules/seller/views/edit_product_page.dart b/lib/app/modules/seller/views/edit_product_page.dart new file mode 100644 index 00000000..d1fffe2b --- /dev/null +++ b/lib/app/modules/seller/views/edit_product_page.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:image_picker/image_picker.dart'; +// import 'dart:io'; + +class EditProductPage extends StatelessWidget { + final ProductModel product; + final SellerController sellerController = Get.find(); + + EditProductPage({super.key, required this.product}); + + final TextEditingController nameController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController priceController = TextEditingController(); + final TextEditingController quantityController = TextEditingController(); + final TextEditingController weightController = TextEditingController(); + final RxList images = [].obs; + final RxBool isUploading = false.obs; + + @override + Widget build(BuildContext context) { + nameController.text = product.name; + descriptionController.text = product.description; + priceController.text = product.unitPrice.toString(); + quantityController.text = product.remainingQuantity.toString(); + weightController.text = product.unitWeight.toString(); + images.assignAll(product.images); + + return Scaffold( + appBar: AppBar( + title: const Text('Edit Product'), + backgroundColor: AppTheme.colorRed, + ), + body: SafeArea( + child: Padding( + padding: AppTheme.paddingDefault, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextField( + labelText: 'Product Name', + controller: nameController, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Description', + controller: descriptionController, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Price', + controller: priceController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Quantity', + controller: quantityController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Unit Weight (grams)', + controller: weightController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + _buildImagePicker(images, isUploading), + const SizedBox(height: AppTheme.spacingDefault), + ElevatedButton( + onPressed: () { + if (nameController.text.isEmpty || + descriptionController.text.isEmpty || + priceController.text.isEmpty || + quantityController.text.isEmpty || + weightController.text.isEmpty || + images.isEmpty) { + Get.snackbar('Error', + 'Please fill all fields and add at least one image.'); + return; + } + + sellerController.updateSellerProduct( + id: product.id, + name: nameController.text, + description: descriptionController.text, + unitPrice: int.parse(priceController.text), + remainingQuantity: int.parse(quantityController.text), + unitWeight: int.parse(weightController.text), + images: images.toList(), + ); + + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.colorRed, + minimumSize: const Size.fromHeight(50), + ), + child: const Text('Update Product'), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildImagePicker(RxList images, RxBool isUploading) { + return Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Product Images', style: AppTheme.fontStyleMedium), + const SizedBox(height: AppTheme.spacingSmall), + Wrap( + spacing: 10, + children: [ + ...images.map((image) => _buildImagePreview(image, images)), + _buildAddImageButton(images, isUploading), + ], + ), + if (isUploading.value) + const Padding( + padding: EdgeInsets.only(top: 16), + child: Center( + child: LoadingWidget(), + ), + ), + ], + )); + } + + Widget _buildImagePreview(String imagePath, RxList images) { + return Stack( + children: [ + Image.network( + imagePath, + width: 100, + height: 100, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ), + // Image.file( + // File(imagePath), + // width: 100, + // height: 100, + // fit: BoxFit.cover, + // ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => images.remove(imagePath), + child: Container( + color: Colors.black54, + child: const Icon(Icons.close, color: Colors.white, size: 20), + ), + ), + ), + ], + ); + } + + Widget _buildAddImageButton(RxList images, RxBool isUploading) { + return GestureDetector( + onTap: () async { + final ImagePicker picker = ImagePicker(); + isUploading.value = true; + try { + final pickedFile = + await picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + images.add(pickedFile.path); + } + } finally { + isUploading.value = false; + } + }, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: AppTheme.colorDisabled, + borderRadius: AppTheme.borderRadius, + ), + child: const Icon(Icons.add_a_photo, color: Colors.white), + ), + ); + } +} diff --git a/lib/app/modules/seller/views/seller.dart b/lib/app/modules/seller/views/seller.dart new file mode 100644 index 00000000..5f91a7f3 --- /dev/null +++ b/lib/app/modules/seller/views/seller.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; +import 'package:get_flutter_fire/app/modules/seller/views/add_product.dart'; +import 'package:get_flutter_fire/app/modules/seller/views/edit_product_page.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SellerPage extends StatelessWidget { + final AuthController authController = Get.find(); + final SellerController sellerController = Get.put(SellerController()); + + SellerPage({super.key}); + + @override + Widget build(BuildContext context) { + sellerController.fetchSellerProducts(); + + return Scaffold( + body: Obx(() { + if (sellerController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + if (sellerController.products.isEmpty) { + return const Center( + child: Text( + 'No Products Available', + style: TextStyle(color: Colors.grey, fontSize: 18), + ), + ); + } + return _buildProductList(sellerController.products, context); + }), + floatingActionButton: FloatingActionButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const AddProductPage()), //TODO: AddProductPage + ), + backgroundColor: Colors.black, + child: const Icon(Icons.add, color: Colors.white), + ), + ); + } + + Widget _buildProductList(List products, BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text( + "Manage Products", + style: AppTheme.fontStyleLarge.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + return _buildProductCard(product, context); + }, + ), + ], + ), + ); + } + + Widget _buildProductCard(ProductModel product, BuildContext context) { + return GestureDetector( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditProductPage(product: product)), + ), + child: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: Image.network( + product.images.isNotEmpty ? product.images.first : '', + width: double.infinity, + height: 150, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: double.infinity, + height: 150, + color: Colors.grey[200], + child: const Icon( + Icons.image_not_supported, + size: 80, + color: Colors.grey, + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + product.description, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + '₹${product.unitPrice}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(height: 6), + Text( + 'Quantity Left: ${product.remainingQuantity}', + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + ElevatedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EditProductPage(product: product)), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + ), + child: const Text( + 'Edit', + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () => + sellerController.deleteSellerProduct(product.id), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + ), + child: const Text( + 'Delete', + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/modules/settings/bindings/settings_binding.dart b/lib/app/modules/settings/bindings/settings_binding.dart deleted file mode 100644 index fb567f07..00000000 --- a/lib/app/modules/settings/bindings/settings_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/settings_controller.dart'; - -class SettingsBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => SettingsController(), - ); - } -} diff --git a/lib/app/modules/settings/controllers/settings_controller.dart b/lib/app/modules/settings/controllers/settings_controller.dart deleted file mode 100644 index 265e54b1..00000000 --- a/lib/app/modules/settings/controllers/settings_controller.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:get/get.dart'; - -class SettingsController extends GetxController { - //TODO: Implement SettingsController - - final count = 0.obs; - @override - void onInit() { - super.onInit(); - } - - @override - void onReady() { - super.onReady(); - } - - @override - void onClose() {} - void increment() => count.value++; -} diff --git a/lib/app/modules/settings/views/settings_view.dart b/lib/app/modules/settings/views/settings_view.dart deleted file mode 100644 index 2bb244b6..00000000 --- a/lib/app/modules/settings/views/settings_view.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - -import '../controllers/settings_controller.dart'; - -class SettingsView extends GetView { - const SettingsView({super.key}); - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: Text( - 'SettingsView is working', - style: TextStyle(fontSize: 20), - ), - ), - ); - } -} diff --git a/lib/app/modules/splash/splash_screen.dart b/lib/app/modules/splash/splash_screen.dart new file mode 100644 index 00000000..870fa1fe --- /dev/null +++ b/lib/app/modules/splash/splash_screen.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_toast.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/services/notification_service.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + late final AuthController _authController; + + @override + void initState() { + super.initState(); + _authController = Get.put(AuthController()); + + Future.delayed(const Duration(seconds: 2), () async { + if (_authController.user != null) { + showToast("Welcome", isShort: true); + + await _storeUserToken(_authController.user!.id); + + Get.offNamed(Routes.ROOT); + } else { + showToast("Welcome", isShort: true); + Get.offNamed(Routes.WELCOME); + } + }); + } + + // Function to store user token + Future _storeUserToken(String userID) async { + try { + NotificationService notificationService = NotificationService(); + await notificationService.storeToken(userID); + } catch (e) { + // Handle any errors while storing the token + print("Error storing token: $e"); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + Column( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.4, + ), + const Spacing(size: AppTheme.spacingDefault), + Image.asset( + logo, + height: 144, + width: 144, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/app/modules/task_details/bindings/task_details_binding.dart b/lib/app/modules/task_details/bindings/task_details_binding.dart deleted file mode 100644 index 1e017283..00000000 --- a/lib/app/modules/task_details/bindings/task_details_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/task_details_controller.dart'; - -class TaskDetailsBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => TaskDetailsController(), - ); - } -} diff --git a/lib/app/modules/task_details/controllers/task_details_controller.dart b/lib/app/modules/task_details/controllers/task_details_controller.dart deleted file mode 100644 index e0a6a0d9..00000000 --- a/lib/app/modules/task_details/controllers/task_details_controller.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:get/get.dart'; - -class TaskDetailsController extends GetxController { - //TODO: Implement TaskDetailsController - - final count = 0.obs; - @override - void onInit() { - super.onInit(); - } - - @override - void onReady() { - super.onReady(); - } - - @override - void onClose() { - super.onClose(); - } - - void increment() => count.value++; -} diff --git a/lib/app/modules/task_details/views/task_details_view.dart b/lib/app/modules/task_details/views/task_details_view.dart deleted file mode 100644 index c21dbc0e..00000000 --- a/lib/app/modules/task_details/views/task_details_view.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - -import '../controllers/task_details_controller.dart'; - -class TaskDetailsView extends GetView { - const TaskDetailsView({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('TaskDetailsView'), - centerTitle: true, - ), - body: const Center( - child: Text( - 'TaskDetailsView is working', - style: TextStyle(fontSize: 20), - ), - ), - ); - } -} diff --git a/lib/app/modules/tasks/bindings/tasks_binding.dart b/lib/app/modules/tasks/bindings/tasks_binding.dart deleted file mode 100644 index 9d836c2b..00000000 --- a/lib/app/modules/tasks/bindings/tasks_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/tasks_controller.dart'; - -class TasksBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => TasksController(), - ); - } -} diff --git a/lib/app/modules/tasks/controllers/tasks_controller.dart b/lib/app/modules/tasks/controllers/tasks_controller.dart deleted file mode 100644 index 4d4196e4..00000000 --- a/lib/app/modules/tasks/controllers/tasks_controller.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:get/get.dart'; - -class TasksController extends GetxController { - //TODO: Implement TasksController - - final count = 0.obs; - @override - void onInit() { - super.onInit(); - } - - @override - void onReady() { - super.onReady(); - } - - @override - void onClose() { - super.onClose(); - } - - void increment() => count.value++; -} diff --git a/lib/app/modules/tasks/views/tasks_view.dart b/lib/app/modules/tasks/views/tasks_view.dart deleted file mode 100644 index 2103103b..00000000 --- a/lib/app/modules/tasks/views/tasks_view.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - -import '../controllers/tasks_controller.dart'; - -class TasksView extends GetView { - const TasksView({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('TasksView'), - centerTitle: true, - ), - body: const Center( - child: Text( - 'TasksView is working', - style: TextStyle(fontSize: 20), - ), - ), - ); - } -} diff --git a/lib/app/modules/users/bindings/users_binding.dart b/lib/app/modules/users/bindings/users_binding.dart deleted file mode 100644 index 7d8efdb0..00000000 --- a/lib/app/modules/users/bindings/users_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/users_controller.dart'; - -class UsersBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => UsersController(), - ); - } -} diff --git a/lib/app/modules/users/controllers/users_controller.dart b/lib/app/modules/users/controllers/users_controller.dart deleted file mode 100644 index 871467c4..00000000 --- a/lib/app/modules/users/controllers/users_controller.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:get/get.dart'; - -class UsersController extends GetxController { - //TODO: Implement UsersController - - final count = 0.obs; - @override - void onInit() { - super.onInit(); - } - - @override - void onReady() { - super.onReady(); - } - - @override - void onClose() { - super.onClose(); - } - - void increment() => count.value++; -} diff --git a/lib/app/modules/users/views/users_view.dart b/lib/app/modules/users/views/users_view.dart deleted file mode 100644 index 702f32da..00000000 --- a/lib/app/modules/users/views/users_view.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:get/get.dart'; - -import '../controllers/users_controller.dart'; - -class UsersView extends GetView { - const UsersView({super.key}); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('UsersView'), - centerTitle: true, - ), - body: const Center( - child: Text( - 'UsersView is working', - style: TextStyle(fontSize: 20), - ), - ), - ); - } -} diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 7269755d..402775ed 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -1,154 +1,145 @@ -import 'package:flutter/material.dart'; import 'package:get/get.dart'; - -import '../../models/access_level.dart'; -import '../../models/role.dart'; -import '../middleware/auth_middleware.dart'; -import '../modules/cart/bindings/cart_binding.dart'; -import '../modules/cart/views/cart_view.dart'; -import '../modules/categories/bindings/categories_binding.dart'; -import '../modules/categories/views/categories_view.dart'; -import '../modules/checkout/bindings/checkout_binding.dart'; -import '../modules/checkout/views/checkout_view.dart'; -import '../modules/dashboard/bindings/dashboard_binding.dart'; -import '../modules/dashboard/views/dashboard_view.dart'; -import '../modules/home/bindings/home_binding.dart'; -import '../modules/home/views/home_view.dart'; -import '../modules/login/bindings/login_binding.dart'; -import '../modules/login/views/login_view.dart'; -import '../modules/my_products/bindings/my_products_binding.dart'; -import '../modules/my_products/views/my_products_view.dart'; -import '../modules/product_details/bindings/product_details_binding.dart'; -import '../modules/product_details/views/product_details_view.dart'; -import '../modules/products/bindings/products_binding.dart'; -import '../modules/products/views/products_view.dart'; -import '../modules/profile/bindings/profile_binding.dart'; -import '../modules/profile/views/profile_view.dart'; -import '../modules/register/bindings/register_binding.dart'; -import '../modules/register/views/register_view.dart'; -import '../modules/root/bindings/root_binding.dart'; -import '../modules/root/views/root_view.dart'; -import '../modules/settings/bindings/settings_binding.dart'; -import '../modules/settings/views/settings_view.dart'; -import '../modules/task_details/bindings/task_details_binding.dart'; -import '../modules/task_details/views/task_details_view.dart'; -import '../modules/tasks/bindings/tasks_binding.dart'; -import '../modules/tasks/views/tasks_view.dart'; -import '../modules/users/bindings/users_binding.dart'; -import '../modules/users/views/users_view.dart'; -import '../../models/screens.dart'; - -part 'app_routes.dart'; -part 'screen_extension.dart'; +import 'package:get_flutter_fire/app/modules/admin/views/add_category.dart'; +import 'package:get_flutter_fire/app/modules/admin/views/admin_banner_list_screen.dart'; +import 'package:get_flutter_fire/app/modules/admin/views/approve_seller.dart'; +import 'package:get_flutter_fire/app/modules/admin/views/categories.dart'; +import 'package:get_flutter_fire/app/modules/admin/views/upload_banner.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/address_screen.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/login_screen.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/otp_screen.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/register_screen.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/welcome_screen.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/cart_root_view.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/order_confirmed.dart'; +import 'package:get_flutter_fire/app/modules/home/view/categories/categories.dart'; +import 'package:get_flutter_fire/app/modules/home/view/product_detail_screen.dart'; +import 'package:get_flutter_fire/app/modules/home/view/product_listing.dart'; +import 'package:get_flutter_fire/app/modules/home/view/search.dart'; +import 'package:get_flutter_fire/app/modules/orders/views/order_detail_screen.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/account_detail.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/add_addresses.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/contact_support.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/manage_address.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/past_queries_screen.dart'; +import 'package:get_flutter_fire/app/modules/root/root_view.dart'; +import 'package:get_flutter_fire/app/modules/splash/splash_screen.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; class AppPages { AppPages._(); - static const INITIAL = Routes.HOME; + static const INITIAL = Routes.SPLASH; - //TODO create this using the information from Screen and Role data - //can use https://pub.dev/packages/freezed static final routes = [ GetPage( - name: '/', - page: () => const RootView(), - binding: RootBinding(), - participatesInRootNavigator: true, - preventDuplicates: true, - children: [ - Screen.LOGIN.getPage( - page: () => const LoginView(), - binding: LoginBinding(), - ), - Screen.REGISTER.getPage( - page: () => const RegisterView(), - binding: RegisterBinding(), - ), - Screen.PROFILE.getPage( - page: () => const ProfileView(), - binding: ProfileBinding(), - ), - Screen.SETTINGS.getPage( - page: () => const SettingsView(), - binding: SettingsBinding(), - ), - Screen.HOME.getPage( - page: () => const HomeView(), - bindings: [ - HomeBinding(), - ], - children: [ - Screen.DASHBOARD.getPage( - page: () => const DashboardView(), - binding: DashboardBinding(), - ), - Screen.USERS.getPage( - role: Role.admin, - page: () => const UsersView(), - binding: UsersBinding(), - children: [ - Screen.USER_PROFILE.getPage( - page: () => const ProfileView(), - binding: ProfileBinding(), - ) - ], - ), - Screen.PRODUCTS.getPage( - page: () => const ProductsView(), - binding: ProductsBinding(), - children: [ - Screen.PRODUCT_DETAILS.getPages( - page: () => const ProductDetailsView(), - binding: ProductDetailsBinding(), - ), - ], - ), - Screen.CATEGORIES.getPage( - role: Role.admin, - page: () => const CategoriesView(), - binding: CategoriesBinding(), - ), - Screen.CART.getPage( - page: () => const CartView(), - binding: CartBinding(), - role: Role.buyer, - children: [ - Screen.CHECKOUT.getPage( - //if this is after cart details, it never gets reached - page: () => const CheckoutView(), - binding: CheckoutBinding(), - ), - Screen.CART_DETAILS.getPages( - page: () => const ProductDetailsView(), - binding: ProductDetailsBinding(), - ), - ], - ), - Screen.MY_PRODUCTS.getPage( - page: () => const MyProductsView(), - binding: MyProductsBinding(), - role: Role.seller, - children: [ - Screen.MY_PRODUCT_DETAILS.getPages( - page: () => const ProductDetailsView(), - binding: ProductDetailsBinding(), - ), - ], - ), - Screen.TASKS.getPage( - role: Role.admin, - page: () => const TasksView(), - binding: TasksBinding(), - children: [ - Screen.TASK_DETAILS.getPage( - page: () => const TaskDetailsView(), - binding: TaskDetailsBinding(), - ), - ], - ), - ], - ) - ], + name: Routes.SPLASH, + page: () => const SplashScreen(), + ), + GetPage( + name: Routes.WELCOME, + page: () => const WelcomeScreen(), + ), + GetPage( + name: Routes.LOGIN, + page: () => const LoginScreen(), + ), + GetPage( + name: Routes.OTP, + page: () => OtpScreen(phoneNumber: Get.arguments['phoneNumber']), + ), + GetPage( + name: Routes.REGISTER, + page: () => RegisterScreen( + phoneNumber: Get.arguments['phoneNumber'], + ), + ), + GetPage( + name: Routes.ADDRESS, + page: () => AddressScreen(), + ), + GetPage( + name: Routes.ROOT, + page: () => RootView(), + ), + + //home screen + GetPage( + name: Routes.CATEGORIES, + page: () => const CategoriesScreen(), + ), + GetPage( + name: Routes.SEARCH, + page: () => const SearchScreen(), + ), + + // Profile Routes + GetPage( + name: Routes.ACCOUNT_DETAILS, + page: () => const AccountDetailsScreen(), + ), + GetPage( + name: Routes.MANAGE_ADDRESS, + page: () => ManageAddressScreen(), + ), + GetPage( + name: Routes.ADD_ADDRESS, + page: () => AddAddressScreen(), + ), + GetPage( + name: Routes.CONTACT, + page: () => SupportScreen(), + ), + GetPage( + name: Routes.PAST_QUERIES, + page: () => PastQueriesScreen(), + ), + + // Cart Routes + GetPage( + name: Routes.CART, + page: () => const CartRootView(), + ), + + // Order Routes + GetPage( + name: Routes.ORDER_CONFIRMED, + page: () => const OrderConfirmedScreen(), + ), + GetPage( + name: Routes.ORDER_DETAILS, + page: () => const OrderDetailScreen(), + ), + + // Admin Routes + GetPage( + name: Routes.UPLOAD_BANNERS, + page: () => const AdminBannerUploadScreen(), + ), + GetPage( + name: Routes.EDIT_BANNER, + page: () => const AdminBannerListScreen(), + ), + GetPage( + name: Routes.APPROVE_SELLERS, + page: () => const ApproveSellerScreen(), + ), + GetPage( + name: Routes.ADD_CATEGORY, + page: () => const AddCategoryScreen(), + ), + GetPage( + name: Routes.VIEW_CATEGORIES, + page: () => CategoryListScreen(), + ), + + // Product Detail Route + GetPage( + name: Routes.PRODUCT_DETAILS, + page: () => ProductDetailScreen(productID: Get.parameters['id']!), + ), + GetPage( + name: Routes.PRODUCTS_LISTING, + page: () => const ProductsListingScreen(), ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index f3129d21..d1904740 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -1,54 +1,73 @@ -// ignore_for_file: non_constant_identifier_names, constant_identifier_names +abstract class Routes { + Routes._(); + static const SPLASH = _Paths.SPLASH; + static const WELCOME = _Paths.WELCOME; + static const LOGIN = _Paths.LOGIN; + static const OTP = _Paths.OTP; + static const REGISTER = _Paths.REGISTER; + static const ADDRESS = _Paths.ADDRESS; + static const ROOT = _Paths.ROOT; + //HOME ROUTES + static const CATEGORIES = _Paths.CATEGORIES; + static const SEARCH = _Paths.SEARCH; +//profile routes + static const ACCOUNT_DETAILS = _Paths.ACCOUNT_DETAILS; + static const MANAGE_ADDRESS = _Paths.MANAGE_ADDRESS; + static const ADD_ADDRESS = _Paths.ADD_ADDRESS; + static const CONTACT = _Paths.CONTACT; + static const PAST_QUERIES = _Paths.PAST_QUERIES; + //Cart Routes + static const CART = _Paths.CART; -part of 'app_pages.dart'; -// DO NOT EDIT. This is code generated via package:get_cli/get_cli.dart + //Order + static const ORDER_CONFIRMED = _Paths.ORDER_CONFIRMED; + static const ORDER_DETAILS = _Paths.ORDER_DETAILS; -abstract class Routes { - static const HOME = _Paths.HOME; - // static String PROFILE = Screen.PROFILE.fullPath; - // static String SETTINGS = Screen.SETTINGS.fullPath; - static String LOGIN = Screen.LOGIN.route; - static String REGISTER = Screen.REGISTER.route; - // static String DASHBOARD = Screen.DASHBOARD.fullPath; - // static String PRODUCTS = Screen.PRODUCTS.fullPath; - // static String CART = Screen.CART.fullPath; - // static String CHECKOUT = Screen.CHECKOUT.fullPath; - // static const CATEGORIES = _Paths.HOME + _Paths.CATEGORIES; - // static const TASKS = _Paths.HOME + _Paths.TASKS; - // static const USERS = _Paths.HOME + _Paths.USERS; - // static const MY_PRODUCTS = _Paths.HOME + _Paths.MY_PRODUCTS; - - static String PRODUCT_DETAILS(String productId) => - '${Screen.PRODUCTS.route}/$productId'; - static String CART_DETAILS(String productId) => - '${Screen.CART.route}/$productId'; - static String TASK_DETAILS(String taskId) => '${Screen.TASKS.route}/$taskId'; - static String USER_PROFILE(String uId) => '${Screen.USERS.route}/$uId'; + //Admin + static const UPLOAD_BANNERS = _Paths.UPLOAD_BANNERS; + static const EDIT_BANNER = _Paths.EDIT_BANNER; + static const APPROVE_SELLERS = _Paths.APPROVE_SELLERS; - Routes._(); - static String LOGIN_THEN(String afterSuccessfulLogin) => - '${Screen.LOGIN.route}?then=${Uri.encodeQueryComponent(afterSuccessfulLogin)}'; - static String REGISTER_THEN(String afterSuccessfulLogin) => - '${Screen.REGISTER.route}?then=${Uri.encodeQueryComponent(afterSuccessfulLogin)}'; + static const PRODUCT_DETAILS = _Paths.PRODUCT_DETAILS; + static const PRODUCTS_LISTING = _Paths.PRODUCTS_LISTING; + + static const ADD_CATEGORY = _Paths.ADD_CATEGORY; + + static const VIEW_CATEGORIES = _Paths.VIEW_CATEGORIES; } -// Keeping this as Get_Cli will require it. Any addition can later be added to Screen abstract class _Paths { - static const String HOME = '/home'; - // static const DASHBOARD = '/dashboard'; - // static const PRODUCTS = '/products'; - // static const PROFILE = '/profile'; - // static const SETTINGS = '/settings'; - // static const PRODUCT_DETAILS = '/:productId'; - // static const CART_DETAILS = '/:productId'; - // static const LOGIN = '/login'; - // static const CART = '/cart'; - // static const CHECKOUT = '/checkout'; - // static const REGISTER = '/register'; - // static const CATEGORIES = '/categories'; - // static const TASKS = '/tasks'; - // static const TASK_DETAILS = '/:taskId'; - // static const USERS = '/users'; - // static const USER_PROFILE = '/:uId'; - // static const MY_PRODUCTS = '/my-products'; + static const String SPLASH = '/'; + static const String WELCOME = '/welcome'; + static const String LOGIN = '/login'; + static const String OTP = '/otp'; + static const String REGISTER = '/register'; + static const String ADDRESS = '/address'; + static const String ROOT = '/root'; + + //home routes + static const CATEGORIES = '/categories'; + static const SEARCH = '/search'; + //Profile Routes + static const ACCOUNT_DETAILS = '/account_details'; + static const MANAGE_ADDRESS = '/manage_address'; + static const ADD_ADDRESS = '/add_address'; + static const CONTACT = '/contact'; + static const PAST_QUERIES = '/past_queries'; + //Cart Routes + static const CART = '/cart'; + + //Order + static const ORDER_CONFIRMED = '/order_confirmed'; + static const ORDER_DETAILS = '/order_details'; + + //admin + static const UPLOAD_BANNERS = '/upload_banners'; + static const EDIT_BANNER = '/edit_banner'; + static const APPROVE_SELLERS = '/approve_sellers'; + static const ADD_CATEGORY = '/add_category'; + static const VIEW_CATEGORIES = '/view_categories'; + + static const PRODUCT_DETAILS = '/product/:id'; + static const PRODUCTS_LISTING = '/products'; } diff --git a/lib/app/routes/screen_extension.dart b/lib/app/routes/screen_extension.dart deleted file mode 100644 index aaf138b0..00000000 --- a/lib/app/routes/screen_extension.dart +++ /dev/null @@ -1,125 +0,0 @@ -part of 'app_pages.dart'; - -extension GetViewExtension on GetView { - static final _screens = Expando(); - - Screen? get screen => _screens[this]; - set screen(Screen? x) => _screens[this] = x; -} - -extension GetWidgetExtension on GetWidget { - static final _screens = Expando(); - - Screen? get screen => _screens[this]; - set screen(Screen? x) => _screens[this] = x; -} - -extension GetPageExtension on GetPage {} - -extension ScreenExtension on Screen { - GetPage getPage( - {required GetView Function() page, - Bindings? binding, - List bindings = const [], - List? middlewares, - List>? children, - bool preventDuplicates = true, - Role? role}) { - // we are injecting the Screen variable here - pageW() { - GetView p = page(); - p.screen = this; - return p; - } - - //check role and screen mapping for rolebased access - if (accessLevel == AccessLevel.roleBased) { - if (role == null && - (parent == null || parent!.accessLevel != AccessLevel.roleBased)) { - throw Exception("Role must be provided for RoleBased Screens"); - } - if (role != null && !role.permissions.contains(this)) { - throw Exception("Role must permit this Screen"); - } - } - - return _getPage(pageW, binding, bindings, middlewares, children, - preventDuplicates, role); - } - - GetPage getPages( - {required GetWidget Function() page, - Bindings? binding, - List bindings = const [], - List? middlewares, - List>? children, - bool preventDuplicates = false, - Role? role}) { - pageW() { - GetWidget p = page(); - p.screen = this; - return p; - } - - return _getPage(pageW, binding, bindings, middlewares, children, - preventDuplicates, role); - } - - GetPage _getPage( - Widget Function() pageW, - Bindings? binding, - List bindings, - List? middlewares, - List>? children, - bool preventDuplicates, - Role? role) { - return binding != null - ? GetPage( - preventDuplicates: preventDuplicates, - middlewares: middlewares ?? defaultMiddlewares(role), - name: path, - page: pageW, - title: label, - transition: Transition.fade, - binding: binding, - children: children ?? const []) - : GetPage( - preventDuplicates: preventDuplicates, - middlewares: middlewares, - name: path, - page: pageW, - title: label, - transition: Transition.fade, - bindings: bindings, - children: children ?? const []); - } - - List? defaultMiddlewares(Role? role) => (parent == null || - parent!.accessLevel.index < accessLevel.index) - ? switch (accessLevel) { - AccessLevel.public => null, - AccessLevel.guest => [EnsureAuthOrGuestMiddleware()], - AccessLevel.authenticated => [EnsureAuthedAndNotGuestMiddleware()], - AccessLevel.roleBased => [EnsureRoleMiddleware(role ?? Role.buyer)], - AccessLevel.masked => throw UnimplementedError(), //not for screens - AccessLevel.secret => throw UnimplementedError(), //not for screens - AccessLevel.notAuthed => [EnsureNotAuthedOrGuestMiddleware()], - } - : null; -} - -extension RoleExtension on Role { - int getCurrentIndexFromRoute(GetNavConfig? currentRoute) { - final String? currentLocation = currentRoute?.location; - int currentIndex = 0; - if (currentLocation != null) { - currentIndex = - tabs.indexWhere((tab) => currentLocation.startsWith(tab.path)); - } - return (currentIndex > 0) ? currentIndex : 0; - } - - void routeTo(int value, GetDelegate delegate) { - delegate.toNamed(tabs[value].route, arguments: {'role': this}); - } -} diff --git a/lib/app/widgets/cart/cart_bottom_button.dart b/lib/app/widgets/cart/cart_bottom_button.dart new file mode 100644 index 00000000..08ed75eb --- /dev/null +++ b/lib/app/widgets/cart/cart_bottom_button.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CartBottomButton extends StatelessWidget { + final VoidCallback onPressed; + final String label; + final String input; + final bool disabled; + + const CartBottomButton({ + super.key, + required this.onPressed, + required this.label, + required this.input, + this.disabled = false, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: AppTheme.paddingDefault, + decoration: BoxDecoration( + color: AppTheme.colorWhite, + boxShadow: AppTheme.cardBoxShadow, + ), + child: Material( + color: disabled ? AppTheme.backgroundColor : AppTheme.colorRed, + borderRadius: AppTheme.borderRadius, + child: InkWell( + onTap: disabled ? null : onPressed, + child: Container( + padding: AppTheme.paddingTiny, + height: AppTheme.spacingExtraLarge, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: AppTheme.colorWhite, + ), + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + color: AppTheme.colorRed, + ), + padding: AppTheme.paddingTiny, + child: Text( + input, + style: AppTheme.fontStyleHeadingDefault + .copyWith(color: AppTheme.colorWhite), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/app/widgets/cart/order_detail_card.dart b/lib/app/widgets/cart/order_detail_card.dart new file mode 100644 index 00000000..c4bbe2b8 --- /dev/null +++ b/lib/app/widgets/cart/order_detail_card.dart @@ -0,0 +1,86 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/order_model.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderDetailProductCard extends StatelessWidget { + final ProductModel product; + final ProductData productData; + + const OrderDetailProductCard({ + super.key, + required this.productData, + required this.product, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: AppTheme.spacingSmall), + child: Container( + decoration: AppTheme.cardDecoration, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: AppTheme.borderRadius.topLeft, + bottomLeft: AppTheme.borderRadius.bottomLeft, + ), + child: CachedNetworkImage( + imageUrl: product.images[0], + height: 152, + width: 146, + fit: BoxFit.cover, + ), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingExtraSmall), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: AppTheme.fontStyleDefaultBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + product.description, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingSmall), + Text( + productData.price.toString(), + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingExtraSmall), + RichText( + text: TextSpan( + children: [ + const TextSpan( + text: "Quantity: ", + style: AppTheme.fontStyleDefault, + ), + TextSpan( + text: productData.quantity.toString(), + style: AppTheme.fontStyleDefaultBold), + ], + )), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/cart/order_summary_widget.dart b/lib/app/widgets/cart/order_summary_widget.dart new file mode 100644 index 00000000..1df28717 --- /dev/null +++ b/lib/app/widgets/cart/order_summary_widget.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderSummaryWidget extends StatelessWidget { + final int subTotalPrice; + final int priceDiscount; + final int couponDiscount; + final int totalPrice; + final String? couponCode; + + const OrderSummaryWidget({ + super.key, + required this.subTotalPrice, + required this.priceDiscount, + required this.couponDiscount, + required this.totalPrice, + this.couponCode, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: AppTheme.cardDecoration, + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Subtotal", + style: AppTheme.fontStyleDefault, + ), + Text( + subTotalPrice.toString(), + style: AppTheme.fontStyleDefaultBold, + ), + ], + ), + const SizedBox(height: AppTheme.spacingTiny), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Discount", + style: AppTheme.fontStyleDefault, + ), + Text( + priceDiscount.toString(), + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorRed, + ), + ), + ], + ), + if (couponCode != null && couponCode!.isNotEmpty) ...[ + const SizedBox(height: AppTheme.spacingTiny), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RichText( + text: TextSpan( + text: "Coupon (", + style: AppTheme.fontStyleDefault, + children: [ + TextSpan( + text: couponCode, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorBlue, + ), + ), + const TextSpan( + text: ")", + style: AppTheme.fontStyleDefault, + ), + ], + ), + ), + Text( + "-$couponDiscount", + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorRed, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + const SizedBox(height: AppTheme.spacingTiny), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Total", + style: AppTheme.fontStyleDefault, + ), + Text( + totalPrice.toString(), + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorBlue, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/app/widgets/cart/payment_selection_card.dart b/lib/app/widgets/cart/payment_selection_card.dart new file mode 100644 index 00000000..21c0b085 --- /dev/null +++ b/lib/app/widgets/cart/payment_selection_card.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class PaymentMethodSelectionCard extends StatelessWidget { + final String paymentMethod; + final String selectedPaymentMethod; + final String? label; + final ValueChanged onChanged; + final String displayText; + + const PaymentMethodSelectionCard({ + super.key, + required this.paymentMethod, + required this.selectedPaymentMethod, + required this.onChanged, + required this.displayText, + this.label, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + onChanged(paymentMethod); + }, + child: Container( + decoration: AppTheme.cardDecoration.copyWith( + border: Border.all( + color: AppTheme.borderColor, + width: 2.0, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Radio( + value: paymentMethod, + groupValue: selectedPaymentMethod, + onChanged: (value) { + if (value != null) { + onChanged(value); + } + }, + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return AppTheme.colorRed; + } + return Colors.grey; + }), + ), + Text( + displayText, + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true, + ), + Text( + label ?? '', + style: AppTheme.fontStyleDefaultBold + .copyWith(color: AppTheme.colorYellow), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/cart/select_address_card.dart b/lib/app/widgets/cart/select_address_card.dart new file mode 100644 index 00000000..3d681237 --- /dev/null +++ b/lib/app/widgets/cart/select_address_card.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SelectAddressCard extends StatelessWidget { + final AddressModel address; + final bool isDefault; + final Function(AddressModel) onSelect; + final String selectedAddressID; + + const SelectAddressCard({ + super.key, + required this.address, + required this.onSelect, + required this.selectedAddressID, + required this.isDefault, + }); + + @override + Widget build(BuildContext context) { + bool isSelected = selectedAddressID == address.id; + + return GestureDetector( + onTap: () => onSelect(address), + child: Container( + decoration: AppTheme.cardDecoration.copyWith( + border: Border.all( + color: isSelected ? AppTheme.colorRed : AppTheme.borderColor, + width: 2.0, + ), + ), + padding: const EdgeInsets.all(AppTheme.spacingExtraSmall), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(right: AppTheme.spacingTiny), + child: Radio( + activeColor: AppTheme.colorRed, + value: address.id, + groupValue: selectedAddressID, + onChanged: (value) => onSelect(address), + ), + ), + const Spacing(size: AppTheme.spacingExtraLarge), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + address.name, + style: AppTheme.fontStyleDefault.copyWith( + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "${address.line1}\n${address.line2}", + maxLines: 2, + style: AppTheme.fontStyleDefault, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "${address.district}, ${address.city}", + maxLines: 1, + style: AppTheme.fontStyleDefault, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "PhoneNumber ${address.phoneNumber}", + maxLines: 2, + style: AppTheme.fontStyleDefault, + overflow: TextOverflow.ellipsis, + ), + if (isDefault) + const Text( + 'Default Address', + style: TextStyle( + color: AppTheme.colorRed, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/change_password_dialog.dart b/lib/app/widgets/change_password_dialog.dart deleted file mode 100644 index 78c392e5..00000000 --- a/lib/app/widgets/change_password_dialog.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../constants.dart'; - -class ChangePasswordDialog extends StatefulWidget { - final User user; - - ChangePasswordDialog(this.user, {super.key}); - - final _formKey = GlobalKey(); - final _formValues = FormValues(); - - @override - State createState() => _ChangePasswordDialogState(); - - void onSubmit() async { - if (_formKey.currentState != null && _formKey.currentState!.validate()) { - _formKey.currentState?.save(); - try { - AuthCredential credential = EmailAuthProvider.credential( - email: user.email!, password: _formValues.old!); - await user.reauthenticateWithCredential(credential); - await user.updatePassword(_formValues.newP!); - Get.back(result: true); - } catch (e) { - _formValues.authError = "Incorrect Password"; - _formKey.currentState!.validate(); - // Get.snackbar("Error", e.toString()); - } - } - } - - void onReset() { - _formKey.currentState?.reset(); - } -} - -class FormValues { - String? old; - String? newP; - String? authError; -} - -class _ChangePasswordDialogState extends State { - @override - Widget build(BuildContext context) { - return Material( - child: Form( - key: widget._formKey, - child: Column( - children: [ - TextFormField( - textInputAction: TextInputAction.done, - autovalidateMode: AutovalidateMode.onUserInteraction, - obscureText: true, - cursorColor: kPrimaryColor, - decoration: const InputDecoration( - hintText: "Old Password", - prefixIcon: Padding( - padding: EdgeInsets.all(defaultPadding), - child: Icon(Icons.key), - ), - ), - validator: (String? value) { - return widget._formValues.authError ?? - ((value != null && value.length < 8) - ? 'Pwd cannot be less than 8 characters' - : null); - }, - onChanged: (value) { - setState(() { - widget._formValues.old = value; - widget._formValues.authError = null; - }); - }, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: defaultPadding), - child: TextFormField( - textInputAction: TextInputAction.done, - autovalidateMode: AutovalidateMode.onUserInteraction, - obscureText: true, - cursorColor: kPrimaryColor, - decoration: const InputDecoration( - hintText: "New Password", - prefixIcon: Padding( - padding: EdgeInsets.all(defaultPadding), - child: Icon(Icons.lock), - ), - ), - validator: (String? value) { - return (value != null && value.length < 8) - ? 'Pwd cannot be less than 8 characters' - : (value != null && value == widget._formValues.old) - ? 'Pwd cannot be same as old Pwd' - : null; - }, - onChanged: (value) { - setState(() { - widget._formValues.newP = value; - }); - }, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: defaultPadding), - child: TextFormField( - textInputAction: TextInputAction.done, - autovalidateMode: AutovalidateMode.onUserInteraction, - obscureText: true, - cursorColor: kPrimaryColor, - decoration: const InputDecoration( - hintText: "Confirm Password", - prefixIcon: Padding( - padding: EdgeInsets.all(defaultPadding), - child: Icon(Icons.confirmation_num), - ), - ), - validator: (String? value) { - return (value != null && value != widget._formValues.newP) - ? 'Pwd does not match' - : null; - }, - ), - ), - const SizedBox(height: defaultPadding), - ElevatedButton( - onPressed: widget.onReset, - child: const Text( - "Reset", - ), - ), - ], - ), - )); - } -} diff --git a/lib/app/widgets/common/custom_bottom_button.dart b/lib/app/widgets/common/custom_bottom_button.dart new file mode 100644 index 00000000..0dfd162b --- /dev/null +++ b/lib/app/widgets/common/custom_bottom_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CustomBottomButton extends StatelessWidget { + final VoidCallback onPressed; + final String label; + final bool isDisabled; + + const CustomBottomButton({ + super.key, + required this.onPressed, + required this.label, + this.isDisabled = false, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4.0), + child: Material( + color: isDisabled ? AppTheme.colorDisabled : AppTheme.colorRed, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: isDisabled ? null : onPressed, + borderRadius: BorderRadius.circular(12), + child: Container( + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorWhite, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/custom_button.dart b/lib/app/widgets/common/custom_button.dart new file mode 100644 index 00000000..a6b18330 --- /dev/null +++ b/lib/app/widgets/common/custom_button.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CustomButton extends StatelessWidget { + final VoidCallback onPressed; + final String text; + final bool isDisabled; + + const CustomButton( + {super.key, + required this.onPressed, + required this.text, + this.isDisabled = false}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: isDisabled ? null : onPressed, + child: Container( + width: double.infinity, + height: 50, + decoration: BoxDecoration( + color: isDisabled ? AppTheme.colorDisabled : AppTheme.colorRed, + borderRadius: BorderRadius.circular(12), + ), + alignment: Alignment.center, + child: Text( + text, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorWhite, + ), + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/custom_dropdown.dart b/lib/app/widgets/common/custom_dropdown.dart new file mode 100644 index 00000000..6808269d --- /dev/null +++ b/lib/app/widgets/common/custom_dropdown.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CustomDropdown extends StatefulWidget { + final String hintText; + final List> items; + final ValueChanged onChanged; + final T? value; + + const CustomDropdown({ + super.key, + required this.hintText, + required this.items, + required this.onChanged, + this.value, + }); + + @override + CustomDropdownState createState() => CustomDropdownState(); +} + +class CustomDropdownState extends State> { + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppTheme.borderColor, width: 1.0), + ), + ), + child: DropdownButtonFormField( + value: widget.value, + items: widget.items, + isExpanded: true, + iconEnabledColor: AppTheme.borderColor, + iconDisabledColor: AppTheme.borderColor, + onChanged: widget.onChanged, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + hint: Text( + widget.hintText, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.only( + top: AppTheme.spacingExtraSmall, + left: AppTheme.spacingTiny, + bottom: AppTheme.spacingExtraSmall), + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: InputBorder.none, + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/custom_phone_textfield.dart b/lib/app/widgets/common/custom_phone_textfield.dart new file mode 100644 index 00000000..e332f24d --- /dev/null +++ b/lib/app/widgets/common/custom_phone_textfield.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class PhoneTextField extends StatefulWidget { + final TextEditingController? controller; + final String hintText; + final bool readOnly; + const PhoneTextField( + {super.key, + this.controller, + required this.hintText, + required this.readOnly}); + + @override + State createState() => _PhoneTextFieldState(); +} + +class _PhoneTextFieldState extends State { + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: widget.readOnly ? AppTheme.backgroundColor : Colors.transparent, + border: widget.readOnly + ? const Border( + bottom: BorderSide( + color: AppTheme.borderColor, + width: 1.0, + ), + ) + : null, + ), + child: widget.readOnly ? _buildReadOnlyField() : _buildEditableField(), + ); + } + + Widget _buildReadOnlyField() { + final phoneNumber = widget.controller?.text ?? ''; + + return Padding( + padding: const EdgeInsets.all(AppTheme.fontSizeSmall), + child: Row( + children: [ + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Text( + "+91", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + ), + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Container( + width: 1, + height: 20, + color: AppTheme.greyTextColor, + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Expanded( + child: Text( + phoneNumber.substring(3), + style: AppTheme.fontStyleDefault, + ), + ), + const SizedBox(width: 8), + // const CustomIcon(asset: iconCheckCircle), + ], + ), + ); + } + + Widget _buildEditableField() { + return TextField( + textAlignVertical: TextAlignVertical.center, + style: AppTheme.fontStyleDefault, + controller: widget.controller, + keyboardType: TextInputType.phone, + cursorColor: AppTheme.colorRed, + maxLength: 10, + readOnly: widget.readOnly, + onChanged: (value) { + if (value.length == 10) { + FocusScope.of(context).unfocus(); + } + }, + decoration: InputDecoration( + counterText: '', + isDense: true, + hintText: widget.hintText, + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldUnderlineBorder, + focusedBorder: AppTheme.textfieldUnderlineBorder, + enabledBorder: AppTheme.textfieldUnderlineBorder, + prefixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Text( + "+91", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + ), + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Container( + width: 1, + height: 20, + color: AppTheme.greyTextColor, + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/custom_textfield.dart b/lib/app/widgets/common/custom_textfield.dart new file mode 100644 index 00000000..0b17fc7f --- /dev/null +++ b/lib/app/widgets/common/custom_textfield.dart @@ -0,0 +1,41 @@ + +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + + +class CustomTextField extends StatelessWidget { + final String labelText; + final bool obscureText; + final TextInputType keyboardType; + final TextEditingController controller; + + const CustomTextField({ + super.key, + required this.labelText, + this.obscureText = false, + this.keyboardType = TextInputType.text, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + obscureText: obscureText, + keyboardType: keyboardType, + decoration: InputDecoration( + labelText: labelText, + labelStyle: + AppTheme.fontStyleDefault.copyWith(color: AppTheme.greyTextColor), + contentPadding: const EdgeInsets.only(left: 8.0), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: AppTheme.borderColor), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: AppTheme.colorRed), + ), + ), + style: AppTheme.fontStyleDefault, + ); + } +} diff --git a/lib/app/widgets/common/overlay_loader.dart b/lib/app/widgets/common/overlay_loader.dart new file mode 100644 index 00000000..0664737a --- /dev/null +++ b/lib/app/widgets/common/overlay_loader.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class LoadingWidget extends StatefulWidget { + const LoadingWidget({super.key}); + + @override + State createState() => _LoadingWidgetState(); +} + +class _LoadingWidgetState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(rhombusLoader, height: 150), + const Text( + "Setting You UP...", + style: AppTheme.fontStyleDefault, + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/secondary_button.dart b/lib/app/widgets/common/secondary_button.dart new file mode 100644 index 00000000..f18f4c47 --- /dev/null +++ b/lib/app/widgets/common/secondary_button.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SecondaryButton extends StatelessWidget { + final String label; + final VoidCallback onPressed; + final bool disabled; + + const SecondaryButton({ + super.key, + required this.label, + required this.onPressed, + this.disabled = false, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: disabled ? null : onPressed, + child: Text( + label, + textAlign: TextAlign.center, + style: AppTheme.fontStyleHeadingDefault.copyWith( + fontWeight: FontWeight.bold, + color: disabled ? AppTheme.greyTextColor : AppTheme.colorRed, + decoration: TextDecoration.underline, + decorationColor: + disabled ? AppTheme.greyTextColor : AppTheme.colorRed), + ), + ); + } +} diff --git a/lib/app/widgets/common/show_loader.dart b/lib/app/widgets/common/show_loader.dart new file mode 100644 index 00000000..042dcc5d --- /dev/null +++ b/lib/app/widgets/common/show_loader.dart @@ -0,0 +1,9 @@ +import 'package:flutter_easyloading/flutter_easyloading.dart'; + +showLoader() async { + await EasyLoading.show(); +} + +dismissLoader() async { + await EasyLoading.dismiss(); +} diff --git a/lib/app/widgets/common/show_toast.dart b/lib/app/widgets/common/show_toast.dart new file mode 100644 index 00000000..238039eb --- /dev/null +++ b/lib/app/widgets/common/show_toast.dart @@ -0,0 +1,14 @@ +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +void showToast(message, {bool isError = false, bool isShort = false}) { +//TODO: implement showToast correctly + Fluttertoast.showToast( + msg: message, + toastLength: isShort ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG, + gravity: ToastGravity.TOP, + timeInSecForIosWeb: isShort ? 1 : 5, + backgroundColor: isError ? AppTheme.colorRed : AppTheme.colorRed, + textColor: isError ? AppTheme.colorBlack : AppTheme.colorWhite, + fontSize: AppTheme.fontSizeDefault); +} diff --git a/lib/app/widgets/common/spacing.dart b/lib/app/widgets/common/spacing.dart new file mode 100644 index 00000000..bcf4d28d --- /dev/null +++ b/lib/app/widgets/common/spacing.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; + +class Spacing extends StatelessWidget { + final double size; + final bool isHorizontal; + + const Spacing({ + super.key, + required this.size, + this.isHorizontal = false, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: isHorizontal ? null : size, + width: isHorizontal ? size : null, + ); + } +} diff --git a/lib/app/widgets/image_picker_button.dart b/lib/app/widgets/image_picker_button.dart deleted file mode 100644 index d6e87ff4..00000000 --- a/lib/app/widgets/image_picker_button.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:image_picker/image_picker.dart'; - -import '../../models/action_enum.dart'; -import 'menu_sheet_button.dart'; - -enum ImageSources implements ActionEnum { - camera(Icons.camera, 'Camera'), - gallery(Icons.photo_library, 'Gallery'), - file(Icons.file_upload, 'File'); - - const ImageSources(this.icon, this.label); - - @override - Future doAction() async { - switch (this) { - case ImageSources.camera: - return await getImage(ImageSource.camera); - case ImageSources.gallery: - return await getImage(ImageSource.gallery); - case ImageSources.file: - return await getFile(); - default: - } - return null; - } - - @override - final IconData? icon; - @override - final String? label; - - static Future getImage(ImageSource imageSource) async { - final pickedFile = await ImagePicker().pickImage(source: imageSource); - if (pickedFile != null) { - return pickedFile.path; - } else { - Get.snackbar('Error', 'Image Not Selected'); - return null; - } - } - - static Future getFile() async { - FilePickerResult? result = await FilePicker.platform - .pickFiles(type: FileType.image, allowMultiple: false); - - if (result != null && result.files.isNotEmpty) { - final fileBytes = result.files.first.bytes; - final fileName = result.files.first.name; - GetStorage().write(fileName, fileBytes); - - return fileName; - //result.files.single.path;//is causing issues for Web, see https://github.com/miguelpruivo/flutter_file_picker/wiki/FAQ - } else { - Get.snackbar('Error', 'Image Not Selected'); - return null; - } - } -} - -class ImagePickerButton extends MenuSheetButton { - final ValueSetter? callback; - - const ImagePickerButton( - {super.key, - super.icon = const Icon(Icons.image), - super.label = 'Pick an Image', - this.callback}); - - @override - Iterable get values => ImageSources.values; - - @override - void callbackFunc(act) { - if (callback != null) callback!(act); - } - - @override - Widget build(BuildContext context) { - return !(GetPlatform.isAndroid || GetPlatform.isIOS) - ? TextButton.icon( - onPressed: () async => callbackFunc(await ImageSources.getFile()), - icon: icon, - label: const Text('Pick an Image'), - ) - : builder(context); - } -} diff --git a/lib/app/widgets/login_widgets.dart b/lib/app/widgets/login_widgets.dart deleted file mode 100644 index b8f2d8c1..00000000 --- a/lib/app/widgets/login_widgets.dart +++ /dev/null @@ -1,95 +0,0 @@ -// ignore_for_file: inference_failure_on_function_invocation - -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../services/auth_service.dart'; -import '../../models/screens.dart'; -import '../../services/remote_config.dart'; -import 'menu_sheet_button.dart'; - -class LoginWidgets { - static Widget headerBuilder(context, constraints, shrinkOffset) { - return Padding( - padding: const EdgeInsets.all(20), - child: AspectRatio( - aspectRatio: 1, - child: Image.asset('assets/images/flutterfire_300x.png'), - ), - ); - } - - static Widget footerBuilder(myWidget) { - return Column( - children: [ - myWidget, - const Padding( - padding: EdgeInsets.only(top: 16), - child: Text( - 'By signing in, you agree to our terms and conditions.', - style: TextStyle(color: Colors.grey), - )) - ], - ); - } - - static Widget sideBuilder(context, shrinkOffset) { - return Padding( - padding: const EdgeInsets.all(20), - child: AspectRatio( - aspectRatio: 1, - child: Image.asset('assets/images/flutterfire_300x.png'), - ), - ); - } -} - -class LoginBottomSheetToggle extends MenuSheetButton { - const LoginBottomSheetToggle(this.current, {super.key}); - final GetNavConfig current; - - @override - Iterable get values { - MenuItemsController controller = Get.find(); - return controller.values.value; - } - - @override - Icon? get icon => (AuthService.to.isLoggedInValue) - ? values.length == 1 - ? const Icon(Icons.logout) - : const Icon(Icons.menu) - : const Icon(Icons.login); - - @override - String? get label => (AuthService.to.isLoggedInValue) - ? values.length == 1 - ? 'Logout' - : 'Click for Options' - : 'Login'; - - @override - Widget build(BuildContext context) { - MenuItemsController controller = Get.put( - MenuItemsController([]), - permanent: true); //must make true else gives error - Screen.sheet(null).then((val) { - controller.values.value = val; - }); - RemoteConfig.instance.then((ins) => - ins.addUseBottomSheetForProfileOptionsListener((val) async => - {controller.values.value = await Screen.sheet(null)})); - return Obx(() => (AuthService.to.isLoggedInValue) - ? builder(context, vals: controller.values.value) - : !(current.currentPage!.name == Screen.LOGIN.path) - ? IconButton( - onPressed: () async { - await Screen.LOGIN.doAction(); - // controller.toggle(Screen.LOGIN); - }, - icon: Icon(Screen.LOGIN.icon), - tooltip: Screen.LOGIN.label, - ) - : const SizedBox.shrink()); //should be only for loggedin case - } -} diff --git a/lib/app/widgets/menu_sheet_button.dart b/lib/app/widgets/menu_sheet_button.dart deleted file mode 100644 index abd3873e..00000000 --- a/lib/app/widgets/menu_sheet_button.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../models/action_enum.dart'; - -class MenuItemsController extends GetxController { - MenuItemsController(Iterable iter) : values = Rx>(iter); - - final Rx> values; -} - -class MenuSheetButton extends StatelessWidget { - final Iterable? values_; - final Icon? icon; - final String? label; - - const MenuSheetButton( - {super.key, - this.values_, - this.icon, - this.label}); //passing scaffoldKey means that bottomSheet is added to it - - Iterable get values => values_!; - - static Widget bottomSheet( - Iterable values, ValueSetter? callback) { - return SizedBox( - height: 180, - width: Get.mediaQuery.size.width, - child: ListView( - // mainAxisAlignment: MainAxisAlignment.center, - children: values - .map( - (ActionEnum value) => ListTile( - leading: Icon(value.icon), - title: Text( - value.label!, - ), - onTap: () async { - Get.back(); - callback != null - ? callback(await value.doAction()) - : await value.doAction(); - }), - ) - .toList(), - ), - ); - } - - List> getItems(BuildContext context, Iterable values) { - return values.map>(createPopupMenuItem).toList(); - } - - PopupMenuEntry createPopupMenuItem(dynamic value) => PopupMenuItem( - value: value, - child: Text(value.label ?? ''), //TODO add Icon - ); - - @override - Widget build(BuildContext context) { - return builder(context); - } - -//This should be a modal bottom sheet if on Mobile (See https://mercyjemosop.medium.com/select-and-upload-images-to-firebase-storage-flutter-6fac855970a9) - Widget builder(BuildContext context, {Iterable? vals}) { - Iterable values = vals ?? values_!; - return values.length == 1 || - Get.mediaQuery.orientation == Orientation.portrait - // : Get.context!.isPortrait - ? (icon != null - ? IconButton( - onPressed: () => buttonPressed(values), - icon: icon!, - tooltip: label, - ) - : TextButton( - onPressed: () => buttonPressed(values), - child: Text(label ?? 'Need Label'))) - : PopupMenuButton( - itemBuilder: (context_) => getItems(context_, values), - icon: icon, - tooltip: label, - onSelected: (T value) async => - callbackFunc(await value.doAction())); - } - - void buttonPressed(Iterable values) async => values.length == 1 - ? callbackFunc(await values.first.doAction()) - : Get.bottomSheet(MenuSheetButton.bottomSheet(values, callbackFunc), - backgroundColor: Colors.white); - - void callbackFunc(act) {} -} diff --git a/lib/app/widgets/orders/order_status_indicator.dart b/lib/app/widgets/orders/order_status_indicator.dart new file mode 100644 index 00000000..5014a57b --- /dev/null +++ b/lib/app/widgets/orders/order_status_indicator.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderStatusIndicator extends StatefulWidget { + final OrderStatus currentStatus; + + const OrderStatusIndicator({super.key, required this.currentStatus}); + + @override + OrderStatusIndicatorState createState() => OrderStatusIndicatorState(); +} + +class OrderStatusIndicatorState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late List> _animations; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 3200), + vsync: this, + )..forward(); + + _animations = List.generate(4, (index) { + final start = index * 0.25; + final end = start + 0.25; + return Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: Interval(start, end, curve: Curves.easeIn), + ), + ); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Color _getBarColor(OrderStatus status, int index) { + if (index <= status.index) { + return AppTheme.colorYellow; + } + return AppTheme.backgroundColor; + } + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(4, (index) { + return Expanded( + child: Padding( + padding: const EdgeInsets.only(right: AppTheme.spacingTiny), + child: AnimatedBuilder( + animation: _animations[index], + builder: (context, child) { + double fillPercentage = (index <= widget.currentStatus.index) + ? _animations[index].value + : 0.0; + return Stack( + children: [ + Container( + height: 4, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + color: AppTheme.backgroundColor, + ), + ), + FractionallySizedBox( + widthFactor: fillPercentage, + child: Container( + height: 4, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + color: _getBarColor(widget.currentStatus, index), + ), + ), + ), + ], + ); + }, + ), + ), + ); + }), + ); + } +} diff --git a/lib/app/widgets/orders/primary_button.dart b/lib/app/widgets/orders/primary_button.dart new file mode 100644 index 00000000..a9d94fb6 --- /dev/null +++ b/lib/app/widgets/orders/primary_button.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class PrimaryButton extends StatelessWidget { + final String label; + final VoidCallback onPressed; + final bool disabled; + final bool smallBorder; + + final bool isOutlined; + const PrimaryButton({ + super.key, + required this.label, + required this.onPressed, + this.disabled = false, + this.smallBorder = false, + this.isOutlined = false, + }); + + @override + Widget build(BuildContext context) { + return Material( + borderRadius: AppTheme.borderRadius, + color: disabled + ? AppTheme.backgroundColor + : isOutlined + ? Colors.transparent + : AppTheme.colorRed, + child: InkWell( + onTap: disabled ? null : onPressed, + child: Container( + height: AppTheme.spacingExtraLarge, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + border: isOutlined + ? Border.all( + color: AppTheme.colorRed, + ) + : null, + ), + child: Center( + child: Text( + label, + textAlign: TextAlign.center, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: disabled + ? AppTheme.greyTextColor + : isOutlined + ? AppTheme.colorBlue + : AppTheme.colorWhite, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/widgets/product/add_to_cart_button.dart b/lib/app/widgets/product/add_to_cart_button.dart new file mode 100644 index 00000000..5834faf6 --- /dev/null +++ b/lib/app/widgets/product/add_to_cart_button.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_toast.dart'; +import 'package:get_flutter_fire/models/cart_model.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; // Import for UserType + +class AddToCartButton extends StatelessWidget { + final ProductModel product; + final bool disableButton; + + const AddToCartButton({ + super.key, + required this.product, + this.disableButton = false, + }); + + @override + Widget build(BuildContext context) { + final CartController cartController = Get.find(); + final AddressController addressController = Get.put(AddressController()); + final AuthController authController = Get.find(); + + return Obx(() { + final isInCart = cartController.isProductInCart(product.id); + return Material( + borderRadius: AppTheme.borderRadius, + color: disableButton + ? AppTheme.backgroundColor + : isInCart + ? Colors.white + : const Color.fromARGB(255, 237, 0, 0), + child: GestureDetector( + onTap: () async { + if (!disableButton) { + if (authController.user!.userType == UserType.guest) { + showToast('Please log in or register to add products to cart.'); + Get.toNamed(Routes.WELCOME); + return; + } + + if (isInCart) { + if (addressController.addresses.isEmpty) { + await addressController.fetchAddresses(); + } + + if (addressController.addresses.isEmpty) { + // No address found, redirect to Manage Address page + Get.snackbar('Info', 'Please add an address to proceed.'); + Get.offAllNamed(Routes.MANAGE_ADDRESS); + } else { + // User has at least one address, go to cart + Get.toNamed(Routes.CART); + } + } else { + // Add product to cart + cartController.addItem(CartItem( + id: product.id, + quantity: 1, + price: product.unitPrice, + )); + } + } + }, + child: Material( + borderRadius: AppTheme.borderRadius, + color: disableButton + ? AppTheme.backgroundColor + : isInCart + ? Colors.white + : AppTheme.colorRed, + child: Container( + height: AppTheme.spacingExtraLarge, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + border: isInCart ? Border.all(color: AppTheme.colorRed) : null, + ), + child: Center( + child: Text( + isInCart ? 'Go to cart' : 'Add to cart', + textAlign: TextAlign.center, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: isInCart ? AppTheme.colorRed : AppTheme.colorWhite, + ), + ), + ), + ), + ), + ), + ); + }); + } +} diff --git a/lib/app/widgets/profile/address_container.dart b/lib/app/widgets/profile/address_container.dart new file mode 100644 index 00000000..2050c62d --- /dev/null +++ b/lib/app/widgets/profile/address_container.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class AddressContainer extends StatelessWidget { + final AddressModel address; + + final bool isDefault; + final Function(String) onDelete; + final Function(String) onSetAsDefault; + + const AddressContainer({ + super.key, + required this.address, + required this.isDefault, + required this.onDelete, + required this.onSetAsDefault, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: AppTheme.paddingTiny, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + address.name, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorBlack, + ), + ), + if (isDefault) + Text( + ' (Default)', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorRed, + ), + ), + ], + ), + Text( + '${address.line1}, ${address.line2}', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + '${address.district}, ${address.city}', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + Text.rich( + TextSpan( + text: 'Phone Number: ', + style: AppTheme.fontStyleMedium, + children: [ + TextSpan( + text: address.phoneNumber, + style: AppTheme.fontStyleDefaultBold, + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingSmall), + if (!isDefault) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + onSetAsDefault(address.id); + }, + child: Text( + 'Set as default address', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorRed, + decoration: TextDecoration.underline, + ), + ), + ), + InkWell( + onTap: () { + onDelete(address.id); + }, + child: const Text('Delete', style: AppTheme.fontStyleDefault), + ), + ], + ), + const Spacing(size: AppTheme.spacingTiny), + ], + ), + ); + } +} diff --git a/lib/app/widgets/profile/profile_list_widget.dart b/lib/app/widgets/profile/profile_list_widget.dart new file mode 100644 index 00000000..df10d938 --- /dev/null +++ b/lib/app/widgets/profile/profile_list_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class ProfileListItem extends StatelessWidget { + final IconData? icon; + final String? imagePath; + final String text; + final Function() onTap; + + const ProfileListItem({ + super.key, + this.icon, + this.imagePath, + required this.text, + required this.onTap, + }) : assert(icon != null || imagePath != null, + 'Either icon or image must be provided'); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ListTile( + leading: icon != null + ? Icon(icon, color: AppTheme.greyTextColor) + : imagePath != null + ? Image.asset(imagePath!, width: 24, height: 24) + : null, + title: Text(text, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.greyTextColor, + )), + trailing: const Icon(Icons.arrow_forward_ios, + color: AppTheme.colorBlack, size: AppTheme.fontSizeSmall), + onTap: onTap, + ), + const Divider(color: AppTheme.borderColor, height: 0.5), + ], + ); + } +} diff --git a/lib/app/widgets/screen_widget.dart b/lib/app/widgets/screen_widget.dart deleted file mode 100644 index d80c9275..00000000 --- a/lib/app/widgets/screen_widget.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../routes/app_pages.dart'; -import '../../models/role.dart'; -import '../../models/screens.dart'; - -class ScreenWidget extends StatelessWidget { - final Widget body; - final Role? role; - - final GetDelegate? delegate; - - final GetNavConfig? currentRoute; - - final Screen screen; - final AppBar? appBar; - - const ScreenWidget({ - super.key, - required this.body, - required this.screen, - this.role = Role.buyer, - this.delegate, - this.currentRoute, - this.appBar, - }); - - @override - Widget build(BuildContext context) { - int currentIndex = - role != null ? role!.getCurrentIndexFromRoute(currentRoute) : 0; - Iterable fabs = screen.fabs; - return Scaffold( - body: body, - appBar: appBar, - bottomNavigationBar: (screen.navTabs.isNotEmpty) - ? BottomNavigationBar( - currentIndex: currentIndex, - onTap: (value) { - if (delegate != null) { - role!.routeTo(value, delegate!); - } - }, - items: - role!.tabs //screen may have more navTabs but we need by role - .map((Screen tab) => BottomNavigationBarItem( - icon: Icon(tab.icon), - label: tab.label, - )) - .toList(), - ) - : null, - floatingActionButton: fabs.isNotEmpty ? getFAB(fabs) : null, - // bottomSheet: //this is used for persistent bar like status bar - ); - } - - FloatingActionButton? getFAB(Iterable fabs) { - if (fabs.length == 1) { - var screen = fabs.firstOrNull!; - return FloatingActionButton.extended( - backgroundColor: Colors.blue, - onPressed: () => Get.rootDelegate.toNamed(screen.route), - label: Text(screen.label ?? ''), - icon: screen.icon == null - ? null - : Icon( - screen.icon, - color: Colors.white, - ), - ); - } - return null; //TODO multi fab button on press - } -} diff --git a/lib/constants.dart b/lib/constants.dart index dd5d17ce..e21c607b 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,21 +1,15 @@ -import 'package:flutter/material.dart'; -// import 'package:get/get_utils/src/platform/platform.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:firebase_auth/firebase_auth.dart'; -const kPrimaryColor = Color(0xFF6F35A5); -const kPrimaryLightColor = Color(0xFFF1E6FF); +// Firebase Instance +final db = FirebaseFirestore.instance; +final auth = FirebaseAuth.instance; +final firestore = FirebaseFirestore.instance; +final functions = FirebaseFunctions.instance; -const double defaultPadding = 16.0; +// Database References +final usersRef = firestore.collection('users'); -const useEmulator = false; - -const useRecaptcha = false; - -const sendMailFromClient = - true; // set this true if in server using custom claim status - -const emulatorHost = - "127.0.0.1"; // GetPlatform.isAndroid ? "10.0.2.2" : "127.0.0.1"; //This is not required due to automaticHostMapping - -const baseUrl = useEmulator ? "http://127.0.0.1" : "your domain"; - -const bundleID = "com.example"; +// Development +const bool development = !bool.fromEnvironment('dart.vm.product'); diff --git a/lib/enums/enum_parser.dart b/lib/enums/enum_parser.dart new file mode 100644 index 00000000..ba890013 --- /dev/null +++ b/lib/enums/enum_parser.dart @@ -0,0 +1,63 @@ +import 'package:get_flutter_fire/enums/enums.dart'; + +UserType parseUserType(String userType) { + switch (userType) { + case 'buyer': + return UserType.buyer; + case 'seller': + return UserType.seller; + case 'admin': + return UserType.admin; + case 'guest': + return UserType.guest; + default: + return UserType.buyer; + } +} + +QueryType parseQueryType(String queryType) { + switch (queryType) { + case 'product': + return QueryType.product; + case 'delivery': + return QueryType.delivery; + case 'general': + return QueryType.general; + case 'payment': + return QueryType.payment; + case 'app': + return QueryType.app; + default: + return QueryType.general; + } +} + +EnquiryStatus parseQueryStatus(String queryStatus) { + switch (queryStatus) { + case 'pending': + return EnquiryStatus.pending; + case 'in-progress': + return EnquiryStatus.inProgress; + case 'completed': + return EnquiryStatus.completed; + default: + return EnquiryStatus.pending; + } +} + +OrderStatus parseOrderStatus(String orderStatus) { + switch (orderStatus) { + case 'placed': + return OrderStatus.placed; + case 'processed': + return OrderStatus.processed; + case 'shipped': + return OrderStatus.shipped; + case 'delivered': + return OrderStatus.delivered; + case 'cancelled': + return OrderStatus.cancelled; + default: + return OrderStatus.placed; + } +} diff --git a/lib/enums/enums.dart b/lib/enums/enums.dart new file mode 100644 index 00000000..aa7a83a7 --- /dev/null +++ b/lib/enums/enums.dart @@ -0,0 +1,17 @@ +enum UserType { buyer, seller, admin, guest } + +enum AccessLevel { + public, + guest, + notAuthed, + authenticated, + roleBased, + masked, + secret +} + +enum EnquiryStatus { pending, inProgress, completed } + +enum QueryType { product, delivery, general, payment, app } + +enum OrderStatus { placed, processed, shipped, delivered, cancelled } diff --git a/lib/firebase_options.template b/lib/firebase_options.template deleted file mode 100644 index 57f5e36d..00000000 --- a/lib/firebase_options.template +++ /dev/null @@ -1,96 +0,0 @@ -// This is a template of the file generated by FlutterFire CLI. -// Actual file will be .dart extension -// ignore_for_file: type=lint -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:firebase_storage/firebase_storage.dart'; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; - -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptions { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - return web; - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; - case TargetPlatform.windows: - return windows; - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions web = FirebaseOptions( - apiKey: 'YOUR_API_KEY', - appId: 'YOUR_APP_ID', - messagingSenderId: 'YOUR_MESSAGING_ID', - projectId: 'YOUR_PROJECT_ID', - authDomain: 'YOUR_PROJECT_ID.firebaseapp.com', - storageBucket: 'YOUR_PROJECT_ID.appspot.com', - measurementId: 'YOUR_MEASUREMENT_ID', - ); - - static const String webClientId = - 'YOUR_APP_ID.apps.googleusercontent.com'; - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'YOUR_APP_ID', - appId: 'YOUR_APP_ID', - messagingSenderId: 'YOUR_MESSAGING_ID', - projectId: 'YOUR_PROJECT_ID', - storageBucket: 'YOUR_PROJECT_ID.appspot.com', - ); - - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'YOUR_API_KEY', - appId: 'YOUR_APP_ID', - messagingSenderId: 'YOUR_MESSAGING_ID', - projectId: 'YOUR_PROJECT_ID', - storageBucket: 'YOUR_PROJECT_ID.appspot.com', - iosBundleId: 'com.example.complete', - ); - - static const FirebaseOptions macos = FirebaseOptions( - apiKey: 'YOUR_API_KEY', - appId: 'YOUR_APP_ID', - messagingSenderId: 'YOUR_MESSAGING_ID', - projectId: 'YOUR_PROJECT_ID', - storageBucket: 'YOUR_PROJECT_ID.appspot.com', - iosBundleId: 'com.example.complete', - ); - - static const FirebaseOptions windows = FirebaseOptions( - apiKey: 'YOUR_API_KEY', - appId: 'YOUR_APP_ID', - messagingSenderId: 'YOUR_MESSAGING_ID', - projectId: 'YOUR_PROJECT_ID', - authDomain: 'YOUR_PROJECT_ID.firebaseapp.com', - storageBucket: 'YOUR_PROJECT_ID.appspot.com', - measurementId: 'YOUR_MEASUREMENT_ID', - ); - - final storage = - FirebaseStorage.instanceFor(bucket: "gs://YOUR_PROJECT_ID.appspot.com"); -} diff --git a/lib/main.dart b/lib/main.dart index 30c258f2..81f4ff59 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,44 +1,72 @@ -// ignore_for_file: inference_failure_on_instance_creation - import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/order_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/product_controller.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_pages.dart'; +import 'package:get_flutter_fire/constants.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; import 'package:get_storage/get_storage.dart'; - -import 'app/routes/app_pages.dart'; import 'firebase_options.dart'; -import 'services/auth_service.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await GetStorage.init(); - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + + if (Firebase.apps.isEmpty) { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + } + + if (development) { + functions.useFunctionsEmulator('localhost', 5001); + auth.useAuthEmulator('localhost', 9099); + db.useFirestoreEmulator('localhost', 8080); + } + + _configLoading(); runApp( - GetMaterialApp.router( - debugShowCheckedModeBanner: - false, //the debug banner will automatically disappear in prod build - title: 'Application', + GetMaterialApp( + debugShowCheckedModeBanner: false, + title: 'Pepe.com', + getPages: AppPages.routes, + initialRoute: AppPages.INITIAL, initialBinding: BindingsBuilder( () { Get.put(AuthService()); + Get.put(AuthController()); + Get.put(AddressController()); + Get.put(CartController()); + Get.put(OrderController()); + Get.put(ProductController()); }, ), - getPages: AppPages.routes, - // routeInformationParser: GetInformationParser( - // // initialRoute: Routes.HOME, - // ), - // routerDelegate: GetDelegate( - // backButtonPopMode: PopMode.History, - // preventDuplicateHandlingMode: - // PreventDuplicateHandlingMode.ReorderRoutes, - // ), theme: ThemeData( - highlightColor: Colors.black.withOpacity(0.5), - bottomSheetTheme: - const BottomSheetThemeData(surfaceTintColor: Colors.blue)), + colorScheme: ColorScheme.fromSeed(seedColor: AppTheme.colorWhite), + useMaterial3: true, + ), + builder: EasyLoading.init(), ), ); } + +_configLoading() { + EasyLoading.instance + ..loadingStyle = EasyLoadingStyle.custom + ..indicatorWidget = Image.asset(rhombusLoader, height: 150) + ..maskType = EasyLoadingMaskType.custom + ..maskColor = AppTheme.colorBlack.withOpacity(0.7) + ..backgroundColor = Colors.transparent + ..textColor = AppTheme.colorWhite + ..indicatorColor = AppTheme.colorWhite + ..userInteractions = false + ..boxShadow = []; +} diff --git a/lib/models/access_level.dart b/lib/models/access_level.dart deleted file mode 100644 index a7b89742..00000000 --- a/lib/models/access_level.dart +++ /dev/null @@ -1,9 +0,0 @@ -enum AccessLevel { - public, //available without any login - guest, //available with guest login - notAuthed, // used for login screens - authenticated, //available on login - roleBased, //available on login and with allowed roles - masked, //available in a partly masked manner based on role - secret //never visible -} diff --git a/lib/models/action_enum.dart b/lib/models/action_enum.dart deleted file mode 100644 index 023adba5..00000000 --- a/lib/models/action_enum.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter/material.dart'; - -abstract class ActionEnum { - Future doAction(); - IconData? get icon; - String? get label; -} diff --git a/lib/models/address_model.dart b/lib/models/address_model.dart new file mode 100644 index 00000000..83e536a7 --- /dev/null +++ b/lib/models/address_model.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; + +class AddressModel { + final String name; + final String phoneNumber; + final String line1; + final String line2; + final String city; + final String district; + final double latitude; + final double longitude; + final String id; + final String userID; + AddressModel({ + required this.name, + required this.phoneNumber, + required this.line1, + required this.line2, + required this.city, + required this.district, + required this.latitude, + required this.longitude, + required this.id, + required this.userID, + }); + + AddressModel copyWith({ + String? name, + String? phoneNumber, + String? line1, + String? line2, + String? city, + String? district, + double? latitude, + double? longitude, + String? id, + String? userID, + }) { + return AddressModel( + name: name ?? this.name, + phoneNumber: phoneNumber ?? this.phoneNumber, + line1: line1 ?? this.line1, + line2: line2 ?? this.line2, + city: city ?? this.city, + district: district ?? this.district, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + id: id ?? this.id, + userID: userID ?? this.userID, + ); + } + + Map toMap() { + return { + 'name': name, + 'phoneNumber': phoneNumber, + 'line1': line1, + 'line2': line2, + 'city': city, + 'district': district, + 'latitude': latitude, + 'longitude': longitude, + 'id': id, + 'userID': userID, + }; + } + + factory AddressModel.fromMap(Map map) { + return AddressModel( + name: map['name'] as String, + phoneNumber: map['phoneNumber'] as String, + line1: map['line1'] as String, + line2: map['line2'] as String, + city: map['city'] as String, + district: map['district'] as String, + latitude: map['latitude'] as double, + longitude: map['longitude'] as double, + id: map['id'] as String, + userID: map['userID'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory AddressModel.fromJson(String source) => + AddressModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return '$line1, $line2, $district, $city'; + } +} diff --git a/lib/models/banner_model.dart b/lib/models/banner_model.dart new file mode 100644 index 00000000..45ca3e6d --- /dev/null +++ b/lib/models/banner_model.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +class BannerModel { + final String id; + final String imageUrl; + final String productID; + final bool isActive; + BannerModel({ + required this.id, + required this.imageUrl, + required this.productID, + required this.isActive, + }); + + BannerModel copyWith({ + String? id, + String? imageUrl, + String? productID, + bool? isActive, + }) { + return BannerModel( + id: id ?? this.id, + imageUrl: imageUrl ?? this.imageUrl, + productID: productID ?? this.productID, + isActive: isActive ?? this.isActive, + ); + } + + Map toMap() { + return { + 'id': id, + 'imageUrl': imageUrl, + 'productID': productID, + 'isActive': isActive, + }; + } + + factory BannerModel.fromMap(Map map) { + return BannerModel( + id: map['id'] as String, + imageUrl: map['imageUrl'] as String, + productID: map['productID'] as String, + isActive: map['isActive'] as bool, + ); + } + + String toJson() => json.encode(toMap()); + + factory BannerModel.fromJson(String source) => + BannerModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'BannerModel(id: $id, imageUrl: $imageUrl, productID: $productID, isActive: $isActive)'; + } +} diff --git a/lib/models/cart_model.dart b/lib/models/cart_model.dart new file mode 100644 index 00000000..67c2ae58 --- /dev/null +++ b/lib/models/cart_model.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; + +class CartModel { + List items; + final String id; + + CartModel({ + required this.items, + required this.id, + }); + + int get itemCount => items.length; + + CartModel copyWith({ + List? items, + String? id, + }) { + return CartModel( + items: items ?? this.items, + id: id ?? this.id, + ); + } + + Map toMap() { + return { + 'items': items.map((x) => x.toMap()).toList(), + 'id': id, + }; + } + + factory CartModel.fromMap(Map map) { + return CartModel( + items: List.from( + map['items']?.map((x) => CartItem.fromMap(x as Map)) ?? + [], + ), + id: map['id'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory CartModel.fromJson(String source) => + CartModel.fromMap(json.decode(source) as Map); + + @override + String toString() => 'Cart(items: $items, id: $id)'; +} + +class CartItem { + String id; + final int price; + int quantity; + CartItem({ + required this.id, + required this.price, + required this.quantity, + }); + + CartItem copyWith({ + String? id, + int? price, + int? quantity, + }) { + return CartItem( + id: id ?? this.id, + price: price ?? this.price, + quantity: quantity ?? this.quantity, + ); + } + + Map toMap() { + return { + 'id': id, + 'price': price, + 'quantity': quantity, + }; + } + + factory CartItem.fromMap(Map map) { + return CartItem( + id: map['id'] as String, + price: map['price'] as int, + quantity: map['quantity'] as int, + ); + } + + String toJson() => json.encode(toMap()); + + factory CartItem.fromJson(String source) => + CartItem.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'CartItem(id: $id, price: $price, quantity: $quantity)'; + } +} diff --git a/lib/models/category_model.dart b/lib/models/category_model.dart new file mode 100644 index 00000000..2afb304c --- /dev/null +++ b/lib/models/category_model.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +class CategoryModel { + final String id; + final String name; + final String imageUrl; + final String? bannerImageUrl; + + CategoryModel({ + required this.id, + required this.name, + required this.imageUrl, + this.bannerImageUrl, // Make this field nullable + }); + + CategoryModel copyWith({ + String? id, + String? name, + String? imageUrl, + String? bannerImageUrl, + }) { + return CategoryModel( + id: id ?? this.id, + name: name ?? this.name, + imageUrl: imageUrl ?? this.imageUrl, + bannerImageUrl: bannerImageUrl ?? this.bannerImageUrl, + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'imageUrl': imageUrl, + 'bannerImageUrl': bannerImageUrl, + }; + } + + factory CategoryModel.fromMap(Map map) { + return CategoryModel( + id: map['id'] as String, + name: map['name'] as String, + imageUrl: map['imageUrl'] as String, + bannerImageUrl: map['bannerImageUrl'] as String?, // Safely handle null + ); + } + + String toJson() => json.encode(toMap()); + + factory CategoryModel.fromJson(String source) => + CategoryModel.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'CategoryModel(id: $id, name: $name, imageUrl: $imageUrl, bannerImageUrl: $bannerImageUrl)'; +} diff --git a/lib/models/contact_enquiry_model.dart b/lib/models/contact_enquiry_model.dart new file mode 100644 index 00000000..0f07b9a0 --- /dev/null +++ b/lib/models/contact_enquiry_model.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; + +import 'package:get_flutter_fire/enums/enum_parser.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; + +class ContactEnquiryModel { + final String id; + final String message; + final String userID; + final EnquiryStatus status; + final DateTime timestamp; + final String reference; + final QueryType queryType; + ContactEnquiryModel({ + required this.id, + required this.message, + required this.userID, + required this.status, + required this.timestamp, + required this.reference, + required this.queryType, + }); + + ContactEnquiryModel copyWith({ + String? id, + String? message, + String? userID, + EnquiryStatus? status, + DateTime? timestamp, + String? reference, + QueryType? queryType, + }) { + return ContactEnquiryModel( + id: id ?? this.id, + message: message ?? this.message, + userID: userID ?? this.userID, + status: status ?? this.status, + timestamp: timestamp ?? this.timestamp, + reference: reference ?? this.reference, + queryType: queryType ?? this.queryType, + ); + } + + Map toMap() { + return { + 'id': id, + 'message': message, + 'userID': userID, + 'status': status.name, + 'timestamp': timestamp, + 'reference': reference, + 'queryType': queryType.name, + }; + } + + factory ContactEnquiryModel.fromMap(Map map) { + return ContactEnquiryModel( + id: map['id'] as String, + message: map['message'] as String, + userID: map['userID'] as String, + status: parseQueryStatus(map['status'] as String), + timestamp: map['timestamp'].toDate(), + reference: map['reference'] as String, + queryType: parseQueryType(map['queryType'] as String), + ); + } + + String toJson() => json.encode(toMap()); + + factory ContactEnquiryModel.fromJson(String source) => + ContactEnquiryModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'Contact(id: $id, message: $message, userID: $userID, status: $status, timestamp: $timestamp, reference: $reference, queryType: $queryType)'; + } +} diff --git a/lib/models/order_model.dart b/lib/models/order_model.dart new file mode 100644 index 00000000..e97f7d80 --- /dev/null +++ b/lib/models/order_model.dart @@ -0,0 +1,206 @@ +import 'dart:convert'; +import 'package:get_flutter_fire/enums/enum_parser.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; + +class OrderModel { + final String id; + final List products; + final double totalWeight; + final int totalPrice; + final String userID; + final AddressModel address; + final List statusUpdates; + final OrderStatus currentStatus; + final String paymentMethod; + final DateTime createdAt; + final String couponID; + final int couponDiscount; + OrderModel({ + required this.id, + required this.products, + required this.totalWeight, + required this.totalPrice, + required this.userID, + required this.address, + required this.statusUpdates, + required this.currentStatus, + required this.paymentMethod, + required this.createdAt, + required this.couponID, + required this.couponDiscount, + }); + + OrderModel copyWith({ + String? id, + List? products, + double? totalWeight, + int? totalPrice, + String? userID, + AddressModel? address, + List? statusUpdates, + OrderStatus? currentStatus, + String? paymentMethod, + DateTime? createdAt, + String? couponID, + int? couponDiscount, + }) { + return OrderModel( + id: id ?? this.id, + products: products ?? this.products, + totalWeight: totalWeight ?? this.totalWeight, + totalPrice: totalPrice ?? this.totalPrice, + userID: userID ?? this.userID, + address: address ?? this.address, + statusUpdates: statusUpdates ?? this.statusUpdates, + currentStatus: currentStatus ?? this.currentStatus, + paymentMethod: paymentMethod ?? this.paymentMethod, + createdAt: createdAt ?? this.createdAt, + couponID: couponID ?? this.couponID, + couponDiscount: couponDiscount ?? this.couponDiscount, + ); + } + + Map toMap() { + return { + 'id': id, + 'products': products.map((x) => x.toMap()).toList(), + 'totalWeight': totalWeight, + 'totalPrice': totalPrice, + 'userID': userID, + 'address': address.toMap(), + 'statusUpdates': statusUpdates.map((x) => x.toMap()).toList(), + 'currentStatus': currentStatus.name, + 'paymentMethod': paymentMethod, + 'createdAt': createdAt, + 'couponID': couponID, + 'couponDiscount': couponDiscount, + }; + } + + factory OrderModel.fromMap(Map map) { + return OrderModel( + id: map['id'] as String, + products: List.from( + (map['products'] as List).map( + (x) => ProductData.fromMap(x as Map), + ), + ), + totalWeight: map['totalWeight'] as double, + totalPrice: map['totalPrice'] as int, + userID: map['userID'] as String, + address: AddressModel.fromMap(map['address'] as Map), + statusUpdates: List.from( + (map['statusUpdates'] as List).map( + (x) => OrderStatusUpdate.fromMap(x as Map), + ), + ), + currentStatus: parseOrderStatus(map['currentStatus'] as String), + paymentMethod: map['paymentMethod'] as String, + createdAt: map['createdAt'].toDate(), + couponID: map['couponID'] as String, + couponDiscount: map['couponDiscount'] as int, + ); + } + + String toJson() => json.encode(toMap()); + + factory OrderModel.fromJson(String source) => + OrderModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'Order(id: $id, products: $products, totalWeight: $totalWeight, totalPrice: $totalPrice, userID: $userID, address: $address, statusUpdates: $statusUpdates, currentStatus: $currentStatus, paymentMethod: $paymentMethod, createdAt: $createdAt, couponID: $couponID, couponDiscount: $couponDiscount)'; + } +} + +class ProductData { + final String id; + final int price; + final int quantity; + ProductData({ + required this.id, + required this.price, + required this.quantity, + }); + + ProductData copyWith({ + String? id, + int? price, + int? priceRrp, + int? quantity, + }) { + return ProductData( + id: id ?? this.id, + price: price ?? this.price, + quantity: quantity ?? this.quantity, + ); + } + + Map toMap() { + return { + 'id': id, + 'price': price, + 'quantity': quantity, + }; + } + + factory ProductData.fromMap(Map map) { + return ProductData( + id: map['id'] as String, + price: map['price'] as int, + quantity: map['quantity'] as int, + ); + } + + String toJson() => json.encode(toMap()); + + factory ProductData.fromJson(String source) => + ProductData.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'ProductData(id: $id, price: $price, quantity: $quantity)'; + } +} + +class OrderStatusUpdate { + final OrderStatus status; + final DateTime timestamp; + OrderStatusUpdate({ + required this.status, + required this.timestamp, + }); + + OrderStatusUpdate copyWith({ + OrderStatus? status, + DateTime? timestamp, + }) { + return OrderStatusUpdate( + status: status ?? this.status, + timestamp: timestamp ?? this.timestamp, + ); + } + + Map toMap() { + return { + 'status': status.name, + 'timestamp': timestamp, + }; + } + + factory OrderStatusUpdate.fromMap(Map map) { + return OrderStatusUpdate( + status: parseOrderStatus(map['status'] as String), + timestamp: map['timestamp'].toDate(), + ); + } + + String toJson() => json.encode(toMap()); + + factory OrderStatusUpdate.fromJson(String source) => + OrderStatusUpdate.fromMap(json.decode(source) as Map); + + @override + String toString() => 'Status(status: $status, timestamp: $timestamp)'; +} diff --git a/lib/models/product.dart b/lib/models/product.dart deleted file mode 100644 index 003d5785..00000000 --- a/lib/models/product.dart +++ /dev/null @@ -1,9 +0,0 @@ -class Product { - final String name; - final String id; - - Product({ - required this.name, - required this.id, - }); -} diff --git a/lib/models/product_model.dart b/lib/models/product_model.dart new file mode 100644 index 00000000..1c46f772 --- /dev/null +++ b/lib/models/product_model.dart @@ -0,0 +1,108 @@ +import 'dart:convert'; + +class ProductModel { + final String id; + final String categoryID; + final List images; + final String name; + final String description; + final int unitWeight; + final int unitPrice; + final int remainingQuantity; + final bool isActive; + final DateTime createdAt; + final DateTime updatedAt; + final String sellerId; + ProductModel({ + required this.id, + required this.categoryID, + required this.images, + required this.name, + required this.description, + required this.unitWeight, + required this.unitPrice, + required this.remainingQuantity, + required this.isActive, + required this.createdAt, + required this.updatedAt, + required this.sellerId, + }); + + ProductModel copyWith({ + String? id, + String? categoryID, + List? images, + String? name, + String? description, + int? unitWeight, + int? unitPrice, + int? wholesalePrice, + int? specialPrice, + int? wholesaleQuantity, + int? specialQuantity, + int? remainingQuantity, + bool? isActive, + DateTime? createdAt, + DateTime? updatedAt, + String? sellerId, + }) { + return ProductModel( + id: id ?? this.id, + categoryID: categoryID ?? this.categoryID, + images: images ?? this.images, + name: name ?? this.name, + description: description ?? this.description, + unitWeight: unitWeight ?? this.unitWeight, + unitPrice: unitPrice ?? this.unitPrice, + remainingQuantity: remainingQuantity ?? this.remainingQuantity, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + sellerId: sellerId ?? this.sellerId, + ); + } + + Map toMap() { + return { + 'id': id, + 'categoryID': categoryID, + 'images': images, + 'name': name, + 'description': description, + 'unitWeight': unitWeight, + 'unitPrice': unitPrice, + 'remainingQuantity': remainingQuantity, + 'isActive': isActive, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'sellerId': sellerId, + }; + } + + factory ProductModel.fromMap(Map map) { + return ProductModel( + id: map['id'] as String, + categoryID: map['categoryID'] as String, + images: List.from((map['images'])), + name: map['name'] as String, + description: map['description'] as String, + unitWeight: map['unitWeight'] as int, + unitPrice: map['unitPrice'] as int, + remainingQuantity: map['remainingQuantity'] as int, + isActive: map['isActive'] as bool, + createdAt: map['createdAt'].toDate(), + updatedAt: map['updatedAt'].toDate(), + sellerId: map['sellerId'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory ProductModel.fromJson(String source) => + ProductModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'ProductModel(id: $id, categoryID: $categoryID, images: $images, name: $name, description: $description, unitWeight: $unitWeight, unitPrice: $unitPrice, remainingQuantity: $remainingQuantity, isActive: $isActive. createdAt: $createdAt, updatedAt: $updatedAt, sellerId: $sellerId)'; + } +} diff --git a/lib/models/role.dart b/lib/models/role.dart deleted file mode 100644 index 50ee31b4..00000000 --- a/lib/models/role.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'screens.dart'; - -// First tab for all except Admin is Home/Dashboard which is diferrent for each role -// Admin is User List By Roles with slide to Change Role or Disable -// Second tab for -// Guest & Buyer is Public Product List by Category with Slide to Add to Cart -// Seller is Product List by Category with Add Product FAB leading to Product Form -// Admin is Category List with Add Category FAB -// Third tab for -// Guest is Cart with Guest Auth -// Buyer is Cart with own Auth -// Seller is MyProducts -// Admin is Tasks/Approvals -// Profile and Settings is in Drawer - -enum Role { - buyer([Screen.DASHBOARD, Screen.PRODUCTS, Screen.CART]), - seller([Screen.DASHBOARD, Screen.PRODUCTS, Screen.MY_PRODUCTS]), - admin([Screen.USERS, Screen.CATEGORIES, Screen.TASKS]); -//higher role can assume a lower role - - const Role(this.permissions); - final List - permissions; //list of screens, with accessLevel = roleBased, visible for the role - - static Role fromString(String? name) => (name != null - ? Role.values.firstWhere((role) => role.name == name) - : Role.buyer); - bool hasAccess(Role role) => index >= role.index; - bool hasAccessOf(String role) => index >= fromString(role).index; - - List get tabs => permissions - .where((screen) => screen.accessor_ == AccessedVia.navigator) - .toList(); //the ones in tab -} diff --git a/lib/models/screens.dart b/lib/models/screens.dart deleted file mode 100644 index 24dee39f..00000000 --- a/lib/models/screens.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../app/widgets/login_widgets.dart'; -import '../services/remote_config.dart'; -import 'action_enum.dart'; -import 'access_level.dart'; -import '../../services/auth_service.dart'; - -enum AccessedVia { - auto, - widget, //example: top right button - navigator, //bottom nav. can be linked to drawer items //handled in ScreenWidget - drawer, //creates nav tree //handled in RootView - bottomSheet, //context menu for web handled via the Button that calls the sheet - fab, //handled in ScreenWidget - singleTap, //when an item of a list is clicked - longTap //or double click -} - -enum Screen implements ActionEnum { - HOME('/home', - icon: Icons.home, - label: "Home", - accessor_: AccessedVia.drawer, - accessLevel: AccessLevel.public), //first screen is default screen - DASHBOARD('/dashboard', - icon: Icons.home, - label: "Home", - accessor_: AccessedVia.navigator, - accessLevel: AccessLevel.public, - parent: HOME), - PRODUCTS('/products', - icon: Icons.dataset, - label: "Products", - accessor_: AccessedVia.navigator, - accessLevel: AccessLevel.public, - parent: HOME), - PRODUCT_DETAILS('/:productId', - accessLevel: AccessLevel.public, parent: PRODUCTS), - LOGIN('/login', - icon: Icons.login, - accessor_: AccessedVia.widget, - accessLevel: AccessLevel.notAuthed), - PROFILE('/profile', - icon: Icons.account_box_rounded, - label: "Profile", - accessor_: AccessedVia.drawer, - accessLevel: AccessLevel.authenticated, - remoteConfig: true), - SETTINGS('/settings', - icon: Icons.settings, - label: "Settings", - accessor_: AccessedVia.drawer, - accessLevel: AccessLevel.authenticated, - remoteConfig: true), - CART('/cart', - icon: Icons.trolley, - label: "Cart", - parent: HOME, - accessor_: AccessedVia.navigator, - accessLevel: AccessLevel.guest), - CART_DETAILS('/:productId', parent: CART, accessLevel: AccessLevel.guest), - CHECKOUT('/checkout', - icon: Icons.check_outlined, - label: "Checkout", - accessor_: AccessedVia.fab, //fab appears in parent - parent: CART, - accessLevel: AccessLevel.authenticated), - REGISTER('/register', - accessor_: AccessedVia.auto, accessLevel: AccessLevel.authenticated), - CATEGORIES('/categories', - icon: Icons.category, - label: "Categories", - parent: HOME, - accessor_: AccessedVia.navigator, - accessLevel: AccessLevel.roleBased), - TASKS('/tasks', - icon: Icons.task, - label: "Tasks", - parent: HOME, - accessor_: AccessedVia.navigator, - accessLevel: AccessLevel.roleBased), - TASK_DETAILS('/:taskId', parent: TASKS, accessLevel: AccessLevel.roleBased), - USERS('/users', - icon: Icons.verified_user, - label: "Users", - parent: HOME, - accessor_: AccessedVia.navigator, - accessLevel: AccessLevel.roleBased), - USER_PROFILE('/:uId', parent: USERS, accessLevel: AccessLevel.roleBased), - MY_PRODUCTS('/my-products', - parent: HOME, - icon: Icons.inventory, - accessor_: AccessedVia.navigator, - label: "Inventory", - accessLevel: AccessLevel.roleBased), - MY_PRODUCT_DETAILS('/:productId', - parent: MY_PRODUCTS, accessLevel: AccessLevel.roleBased), - LOGOUT('/login', - icon: Icons.logout, - label: "Logout", - accessor_: AccessedVia.bottomSheet, - accessLevel: AccessLevel.authenticated), - ; - - const Screen(this.path, - {this.icon, - this.label, - this.parent, - this.accessor_ = AccessedVia.singleTap, - this.accessLevel = AccessLevel.authenticated, - this.remoteConfig = false}); - - @override - final IconData? icon; - @override - final String? label; - - final String path; - final AccessedVia accessor_; - final Screen? parent; - final AccessLevel - accessLevel; //if false it is role based. true means allowed for all - final bool remoteConfig; - - Future get accessor async { - if (remoteConfig && - (await RemoteConfig.instance).useBottomSheetForProfileOptions()) { - return AccessedVia.bottomSheet; - } - return accessor_; - } - - Iterable get children => - Screen.values.where((Screen screen) => screen.parent == this); - - Iterable get fabs => Screen.values.where((Screen screen) => - screen.parent == this && screen.accessor_ == AccessedVia.fab); - - Iterable get navTabs => Screen.values.where((Screen screen) => - screen.parent == this && screen.accessor_ == AccessedVia.navigator); - - String get route => (parent != null ? parent?.route : '')! + path; - - static Future> sheet(Screen? parent) async { - List list = []; - await Future.forEach(Screen.values, (Screen screen) async { - if (screen.parent == parent && - (await screen.accessor) == AccessedVia.bottomSheet) { - list.add(screen); - } - }); - return list; - } - - static Future> drawer() async { - //drawer is not parent linked - List list = []; - await Future.forEach(Screen.values, (Screen screen) async { - if ((await screen.accessor) == AccessedVia.drawer) { - list.add(screen); - } - }); - return list; - } - - @override - Future doAction() async { - if (this == LOGOUT) { - AuthService.to.logout(); - } - Get.rootDelegate.toNamed(route); - } - - Widget? widget(GetNavConfig current) => - (this == LOGIN) ? LoginBottomSheetToggle(current) : null; -} diff --git a/lib/models/seller_model.dart b/lib/models/seller_model.dart new file mode 100644 index 00000000..533831e3 --- /dev/null +++ b/lib/models/seller_model.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; +import 'package:get_flutter_fire/enums/enum_parser.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; + +class SellerModel extends UserModel { + final String sellerId; + final List products; + + SellerModel({ + required super.id, + required super.name, + required super.phoneNumber, + super.email, + required super.isBusiness, + super.businessName, + super.businessType, + super.gstNumber, + super.panNumber, + required super.userType, + required super.defaultAddressID, + required super.createdAt, + required super.lastSeenAt, + required this.sellerId, + required this.products, + super.fcmTokens, // Add this line to pass the fcmTokens to the UserModel + }); + + @override + SellerModel copyWith({ + String? sellerId, + List? products, + String? id, + String? name, + String? phoneNumber, + String? email, + bool? isBusiness, + String? businessName, + String? businessType, + String? gstNumber, + String? panNumber, + UserType? userType, + String? defaultAddressID, + DateTime? createdAt, + DateTime? lastSeenAt, + List? fcmTokens, // Add this parameter + }) { + return SellerModel( + sellerId: sellerId ?? this.sellerId, + products: products ?? this.products, + id: id ?? this.id, + name: name ?? this.name, + phoneNumber: phoneNumber ?? this.phoneNumber, + email: email ?? this.email, + isBusiness: isBusiness ?? this.isBusiness, + businessName: businessName ?? this.businessName, + businessType: businessType ?? this.businessType, + gstNumber: gstNumber ?? this.gstNumber, + panNumber: panNumber ?? this.panNumber, + userType: userType ?? this.userType, + defaultAddressID: defaultAddressID ?? this.defaultAddressID, + createdAt: createdAt ?? this.createdAt, + lastSeenAt: lastSeenAt ?? this.lastSeenAt, + fcmTokens: fcmTokens ?? this.fcmTokens, // Add this line + ); + } + + @override + Map toMap() { + final map = super.toMap(); + map.addAll({ + 'sellerId': sellerId, + 'products': products.map((product) => product.toMap()).toList(), + }); + return map; + } + + factory SellerModel.fromMap(Map map) { + return SellerModel( + id: map['id'] as String, + name: map['name'] as String, + phoneNumber: map['phoneNumber'] as String, + email: map['email'] != null ? map['email'] as String : null, + isBusiness: map['isBusiness'] as bool, + businessName: + map['businessName'] != null ? map['businessName'] as String : null, + businessType: + map['businessType'] != null ? map['businessType'] as String : null, + gstNumber: map['gstNumber'] != null ? map['gstNumber'] as String : null, + panNumber: map['panNumber'] != null ? map['panNumber'] as String : null, + userType: parseUserType(map['userType'] as String), + defaultAddressID: map['defaultAddressID'] as String, + createdAt: DateTime.parse(map['createdAt'] as String), + lastSeenAt: DateTime.parse(map['lastSeenAt'] as String), + sellerId: map['sellerId'] as String, + products: List.from( + map['products']?.map((x) => ProductModel.fromMap(x))), + fcmTokens: + List.from(map['fcmTokens'] as List), // Add this line + ); + } + + @override + String toJson() => json.encode(toMap()); + + factory SellerModel.fromJson(String source) => + SellerModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'SellerModel(sellerId: $sellerId, products: $products, ${super.toString()})'; + } +} diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart new file mode 100644 index 00000000..aa2b0d4b --- /dev/null +++ b/lib/models/user_model.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'package:get_flutter_fire/enums/enum_parser.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; + +class UserModel { + final String id; + final String name; + final String phoneNumber; + final String? email; + final bool isBusiness; + final String? businessName; + final String? businessType; + final String? gstNumber; + final String? panNumber; + final UserType userType; + final String defaultAddressID; + final DateTime createdAt; + final DateTime lastSeenAt; + final List fcmTokens; + + UserModel({ + required this.id, + required this.name, + required this.phoneNumber, + this.email, + required this.isBusiness, + this.businessName, + this.businessType, + this.gstNumber, + this.panNumber, + required this.userType, + required this.defaultAddressID, + required this.createdAt, + required this.lastSeenAt, + this.fcmTokens = const [], + }); + + get role => null; + + UserModel copyWith({ + String? id, + String? name, + String? phoneNumber, + String? email, + bool? isBusiness, + String? businessName, + String? businessType, + String? gstNumber, + String? panNumber, + UserType? userType, + String? defaultAddressID, + DateTime? createdAt, + DateTime? lastSeenAt, + List? fcmTokens, + }) { + return UserModel( + id: id ?? this.id, + name: name ?? this.name, + phoneNumber: phoneNumber ?? this.phoneNumber, + email: email ?? this.email, + isBusiness: isBusiness ?? this.isBusiness, + businessName: businessName ?? this.businessName, + businessType: businessType ?? this.businessType, + gstNumber: gstNumber ?? this.gstNumber, + panNumber: panNumber ?? this.panNumber, + userType: userType ?? this.userType, + defaultAddressID: defaultAddressID ?? this.defaultAddressID, + createdAt: createdAt ?? this.createdAt, + lastSeenAt: lastSeenAt ?? this.lastSeenAt, + fcmTokens: fcmTokens ?? this.fcmTokens, + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'phoneNumber': phoneNumber, + 'email': email, + 'isBusiness': isBusiness, + 'businessName': businessName, + 'businessType': businessType, + 'gstNumber': gstNumber, + 'panNumber': panNumber, + 'userType': userType.name, + 'defaultAddressID': defaultAddressID, + 'createdAt': createdAt.toIso8601String(), + 'lastSeenAt': lastSeenAt.toIso8601String(), + 'fcmTokens': fcmTokens + }; + } + + factory UserModel.fromMap(Map map) { + return UserModel( + id: map['id'] as String, + name: map['name'] as String, + phoneNumber: map['phoneNumber'] as String, + email: map['email'] != null ? map['email'] as String : null, + isBusiness: map['isBusiness'] as bool, + businessName: + map['businessName'] != null ? map['businessName'] as String : null, + businessType: + map['businessType'] != null ? map['businessType'] as String : null, + gstNumber: map['gstNumber'] != null ? map['gstNumber'] as String : null, + panNumber: map['panNumber'] != null ? map['panNumber'] as String : null, + userType: parseUserType(map['userType'] as String), + defaultAddressID: map['defaultAddressID'] as String, + createdAt: DateTime.parse(map['createdAt'] as String), + lastSeenAt: DateTime.parse(map['lastSeenAt'] as String), + fcmTokens: map['fcmTokens'] != null + ? List.from(map['fcmTokens'] as List) + : [], + ); + } + + String toJson() => json.encode(toMap()); + + factory UserModel.fromJson(String source) => + UserModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'UserModel(id: $id, name: $name, phoneNumber: $phoneNumber, email: $email, isBusiness: $isBusiness, businessName: $businessName, businessType: $businessType, gstNumber: $gstNumber, panNumber: $panNumber, userType: $userType, defaultAddressID: $defaultAddressID, createdAt: $createdAt, lastSeenAt: $lastSeenAt, fcmTokens: $fcmTokens)'; + } +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 8bf72aaa..326ec88c 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,201 +1,64 @@ -// ignore_for_file: avoid_print - +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; -import 'package:firebase_ui_auth/firebase_ui_auth.dart' as fbui; -import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; - -import '../models/screens.dart'; -import '../constants.dart'; -import '../models/role.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/constants.dart'; class AuthService extends GetxService { static AuthService get to => Get.find(); - final FirebaseAuth _auth = FirebaseAuth.instance; - late Rxn credential = Rxn(); - final Rxn _firebaseUser = Rxn(); - final Rx _userRole = Rx(Role.buyer); - final Rx robot = RxBool(useRecaptcha); - final RxBool registered = false.obs; - - User? get user => _firebaseUser.value; - Role get maxRole => _userRole.value; - - @override - onInit() { - super.onInit(); - if (useEmulator) _auth.useAuthEmulator(emulatorHost, 9099); - _firebaseUser.bindStream(_auth.authStateChanges()); - _auth.authStateChanges().listen((User? user) { - if (user != null) { - user.getIdTokenResult().then((token) { - _userRole.value = Role.fromString(token.claims?["role"]); - }); - } - }); - } - - bool get isEmailVerified => - user != null && (user!.email == null || user!.emailVerified); - - bool get isLoggedInValue => user != null; - - bool get isAdmin => user != null && _userRole.value == Role.admin; + String _verificationId = ''; + String _phoneNumber = ''; + String get phoneNumber => _phoneNumber; - bool hasRole(Role role) => user != null && _userRole.value == role; + String get userID => auth.currentUser!.uid; - bool get isAnon => user != null && user!.isAnonymous; - - String? get userName => (user != null && !user!.isAnonymous) - ? (user!.displayName ?? user!.email) - : 'Guest'; - - void login() { - // this is not needed as we are using Firebase UI for the login part - } - - void sendVerificationMail({EmailAuthCredential? emailAuth}) async { - if (sendMailFromClient) { - if (_auth.currentUser != null) { - await _auth.currentUser?.sendEmailVerification(); - } else if (emailAuth != null) { - // Approach 1: sending email auth link requires deep linking which is - // a TODO as the current Flutter methods are deprecated - // sendSingInLink(emailAuth); - - // Approach 2: This is a hack. - // We are using createUser to send the verification link from the server side by adding suffix .verify in the email - // if the user already exists and the password is also same and sign in occurs via custom token on server side - try { - await _auth.createUserWithEmailAndPassword( - email: "${credential.value!.email}.verify", - password: credential.value!.password!); - } on FirebaseAuthException catch (e) { - int i = e.message!.indexOf("message") + 10; - int j = e.message!.indexOf('"', i); - Get.snackbar( - e.message!.substring(i, j), - 'Please verify your email by clicking the link on the Email sent', - ); - } - } + // Save the address to Firestore + Future saveAddress(AddressModel address) async { + try { + await FirebaseFirestore.instance + .collection('addresses') + .doc(address.id) + .set(address.toMap()); + } catch (e) { + Get.snackbar('Error', 'Failed to save address: $e'); } } - void sendSingInLink(EmailAuthCredential emailAuth) { - var acs = ActionCodeSettings( - // URL you want to redirect back to. The domain (www.example.com) for this - // URL must be whitelisted in the Firebase Console. - url: - '$baseUrl:5001/flutterfast-92c25/us-central1/handleEmailLinkVerification', - // // This must be true if deep linking. - // // If deeplinking. See [https://firebase.google.com/docs/dynamic-links/flutter/receive] - handleCodeInApp: true, - // iOSBundleId: '$bundleID.ios', - // androidPackageName: '$bundleID.android', - // // installIfNotAvailable - // androidInstallApp: true, - // // minimumVersion - // androidMinimumVersion: '12' + Future verifyPhoneNumber(String phoneNumber) async { + _phoneNumber = phoneNumber; + await auth.verifyPhoneNumber( + phoneNumber: phoneNumber, + verificationCompleted: (PhoneAuthCredential credential) {}, + verificationFailed: (FirebaseAuthException e) { + if (e.code == 'invalid-phone-number') { + Get.snackbar('Error', 'Invalid Phone Number. Please try again.'); + } + }, + codeSent: (String verificationId, int? resendToken) { + _verificationId = verificationId; + Get.toNamed(Routes.OTP, arguments: {'phoneNumber': _phoneNumber}); + }, + codeAutoRetrievalTimeout: (String verificationId) {}, ); - _auth - .sendSignInLinkToEmail(email: emailAuth.email, actionCodeSettings: acs) - .catchError( - (onError) => print('Error sending email verification $onError')) - .then((value) => print('Successfully sent email verification')); } - void register() { - registered.value = true; - // logout(); // Uncomment if we need to enforce relogin - final thenTo = - Get.rootDelegate.currentConfiguration!.currentPage!.parameters?['then']; - Get.rootDelegate - .offAndToNamed(thenTo ?? Screen.PROFILE.route); //Profile has the forms - } - - void logout() { - _auth.signOut(); - if (isAnon) _auth.currentUser?.delete(); - _firebaseUser.value = null; - } - - Future guest() async { - return await Get.defaultDialog( - middleText: 'Sign in as Guest', - barrierDismissible: true, - onConfirm: loginAsGuest, - onCancel: () => Get.back(result: false), - textConfirm: 'Yes, will SignUp later', - textCancel: 'No, will SignIn now'); - } - - void loginAsGuest() async { + Future verifyOTP(String otp) async { try { - await FirebaseAuth.instance.signInAnonymously(); - Get.back(result: true); - Get.snackbar( - 'Alert!', - 'Signed in with temporary account.', - ); - } on FirebaseAuthException catch (e) { - switch (e.code) { - case "operation-not-allowed": - print("Anonymous auth hasn't been enabled for this project."); - break; - default: - print("Unknown error."); + PhoneAuthCredential credential = PhoneAuthProvider.credential( + verificationId: _verificationId, smsCode: otp); + UserCredential userCredential = + await auth.signInWithCredential(credential); + if (userCredential.user != null) { + return true; } - Get.back(result: false); - } - } - - void errorMessage(BuildContext context, fbui.AuthFailed state, - Function(bool, EmailAuthCredential?) callback) { - fbui.ErrorText.localizeError = - (BuildContext context, FirebaseAuthException e) { - final defaultLabels = FirebaseUILocalizations.labelsOf(context); - - // for verification error, also set a boolean flag to trigger button visibility to resend verification mail - String? verification; - if (e.code == "internal-error" && - e.message!.contains('"status":"UNAUTHENTICATED"')) { - // Note that (possibly in Emulator only) the e.email is always coming as null - // String? email = e.email ?? parseEmail(e.message!); - callback(true, credential.value); - verification = - "Please verify email id by clicking the link on the email sent"; - } else { - callback(false, credential.value); + return false; + } on FirebaseAuthException catch (error) { + if (error.code == 'invalid-verification-code') { + Get.snackbar('Error', 'Invalid OTP. Please try again.'); } - - return switch (e.code) { - 'invalid-credential' => 'User ID or Password incorrect', - 'user-not-found' => 'Please create an account first.', - 'credential-already-in-use' => 'This email is already in use.', - _ => fbui.localizedErrorText(e.code, defaultLabels) ?? - verification ?? - 'Oh no! Something went wrong.', - }; - }; - } -} - -class MyCredential extends AuthCredential { - final EmailAuthCredential cred; - MyCredential(this.cred) - : super(providerId: "custom", signInMethod: cred.signInMethod); - - @override - Map asMap() { - return cred.asMap(); + return false; + } } } - -parseEmail(String message) { - int i = message.indexOf('"message":') + 13; - int j = message.indexOf('"', i); - return message.substring(i, j - 1); -} diff --git a/lib/services/get_storage_service.dart b/lib/services/get_storage_service.dart new file mode 100644 index 00000000..7871d9bf --- /dev/null +++ b/lib/services/get_storage_service.dart @@ -0,0 +1,40 @@ +import 'package:get_storage/get_storage.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; + +class GetStorageService { + final GetStorage _storage = GetStorage(); + + // Save user data to storage + void saveUserData(UserModel user) { + _storage.write('user', user.toMap()); + } + + // Load user data from storage + UserModel? getUserData() { + final storedUser = _storage.read>('user'); + if (storedUser != null) { + return UserModel.fromMap(storedUser); + } + return null; + } + + // Clear user data from storage + void clearUserData() { + _storage.remove('user'); + } + + // Save user role + void saveUserRole(String role) { + _storage.write('role', role); + } + + // Load user role + String? getUserRole() { + return _storage.read('role'); + } + + // Clear user role + void clearUserRole() { + _storage.remove('role'); + } +} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart new file mode 100644 index 00000000..d26ce705 --- /dev/null +++ b/lib/services/notification_service.dart @@ -0,0 +1,13 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/constants.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; + +class NotificationService { + Future storeToken(String userID) async { + String? token = await FirebaseMessaging.instance.getToken(); + if (token == null) return; + await firestore.collection('users').doc(userID).update({ + 'fcmTokens': FieldValue.arrayUnion([token]) + }); + } +} diff --git a/lib/services/remote_config.dart b/lib/services/remote_config.dart deleted file mode 100644 index 5d1145a5..00000000 --- a/lib/services/remote_config.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:firebase_remote_config/firebase_remote_config.dart'; -import 'package:get/get_utils/src/platform/platform.dart'; - -enum Typer { integer, boolean, double, string } - -class RemoteConfig { - static RemoteConfig? _instance; - - static Future get instance async { - _instance = _instance ?? RemoteConfig(); - await _instance!.init(); - return _instance!; - } - - final FirebaseRemoteConfig _remoteConfig = FirebaseRemoteConfig.instance; - final List listeners = []; - - Future init() async { - await _remoteConfig.setConfigSettings(RemoteConfigSettings( - fetchTimeout: const Duration(minutes: 1), - minimumFetchInterval: //const Duration(hours: 1), //use for prod - const Duration(minutes: 5), //use for testing only - )); - - await _remoteConfig.setDefaults(const { - "useBottomSheetForProfileOptions": false, - "showSearchBarOnTop": true, - }); - - await fetch(); - } - - Future fetch() async { - return await _remoteConfig.fetchAndActivate(); - } - -//can be used to change config without restart - void addListener(String key, Typer typ, Function listener) async { - if (!GetPlatform.isWeb) { - _remoteConfig.onConfigUpdated. //not supported in web - listen((event) async { - await _remoteConfig.activate(); - if (event.updatedKeys.contains(key)) { - _remoteConfig.fetch(); - var val = _remoteConfig.getValue(key); - switch (typ) { - case Typer.integer: - listener(val.asInt()); - break; - case Typer.boolean: - listener(val.asInt()); - break; - case Typer.double: - listener(val.asDouble()); - break; - default: - listener(val.asString()); - } - } - }); - } - } - - bool useBottomSheetForProfileOptions() { - return _remoteConfig.getBool("useBottomSheetForProfileOptions"); - } - - bool showSearchBarOnTop() { - return _remoteConfig.getBool("showSearchBarOnTop"); - } - - void addUseBottomSheetForProfileOptionsListener(listener) { - addListener("useBottomSheetForProfileOptions", Typer.boolean, listener); - if (!listeners.contains(listener)) { - listeners.add(listener); - } - } -} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 00000000..fdc6785a --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + // Colors + static const Color colorBlack = Color(0xff040520); + static const Color colorWhite = Colors.white; + static const Color colorBlue = Color(0xff1B499C); + static const Color colorDarkBlue = Color(0xff113370); + static const Color colorYellow = Color(0xffFDD30B); + static const Color greyTextColor = Color(0xffA7A7AF); + static const Color backgroundColor = Color(0xffF2F2F3); + static const Color borderColor = Color(0xffDCDCE0); + static const Color colorDisabled = Color(0xFFCFCFCF); + static const Color colorRed = Color(0xffED0000); + + //Font Size + static const double fontSizeSmall = 12.0; + static const double fontSizeDefault = 14.0; + static const double fontSizeMedium = 16.0; + static const double fontSizeLarge = 24.0; + + //Spacing + static const double spacingTiny = 8.0; + static const double spacingExtraSmall = 12.0; + static const double spacingSmall = 16.0; + static const double spacingSemiMedium = 20.0; + static const double spacingDefault = 24.0; + static const double spacingMedium = 32.0; + static const double spacingLarge = 40.0; + static const double spacingExtraLarge = 48.0; + + // Font Styles + + static const TextStyle fontStyleSmall = TextStyle( + fontFamily: 'Mulish', + fontSize: fontSizeSmall, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleDefault = TextStyle( + fontFamily: 'Mulish', + fontSize: fontSizeDefault, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleDefaultBold = TextStyle( + fontFamily: 'Mulish', + fontSize: fontSizeDefault, + fontWeight: FontWeight.bold, + color: colorBlack, + ); + + static const TextStyle fontStyleMedium = TextStyle( + fontFamily: 'Mulish', + fontSize: fontSizeMedium, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleHeadingDefault = TextStyle( + fontFamily: 'Nexa-Bold', + fontSize: fontSizeDefault, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleHeadingMedium = TextStyle( + fontFamily: 'Nexa-Bold', + fontSize: fontSizeMedium, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleLarge = TextStyle( + fontFamily: 'Nexa-Bold', + fontSize: fontSizeLarge, + fontWeight: FontWeight.w400, + color: colorRed, + ); + + // Padding + static const EdgeInsets paddingDefault = EdgeInsets.all(spacingDefault); + static const EdgeInsets paddingSmall = EdgeInsets.all(spacingSmall); + static const EdgeInsets paddingTiny = EdgeInsets.all(spacingTiny); + + // BoxShadow + static List cardBoxShadow = [ + BoxShadow( + offset: const Offset(4, 4), + blurRadius: 16, + color: AppTheme.colorBlack.withOpacity(0.05), + ) + ]; + + static List secondaryBoxShadow = [ + BoxShadow( + offset: const Offset(0, 2), + blurRadius: 8, + color: AppTheme.colorBlack.withOpacity(0.05), + ) + ]; + + static List bottomBoxShadow = [ + BoxShadow( + offset: const Offset(-2, -2), + blurRadius: 8, + color: AppTheme.colorBlack.withOpacity(0.05), + ) + ]; + + static List appBarShadow = [ + BoxShadow( + offset: const Offset(0, 2), + blurRadius: 20, + color: AppTheme.colorBlack.withOpacity(0.06), + ) + ]; + + // Border Radius + static BorderRadius borderRadiusSmall = BorderRadius.circular(4); + static BorderRadius borderRadius = BorderRadius.circular(8); + + // Shapes + static ShapeBorder rrShapeSmall = RoundedRectangleBorder( + borderRadius: borderRadiusSmall, + ); + + static ShapeBorder rrShape = RoundedRectangleBorder( + borderRadius: borderRadius, + ); + + // Borders + static Border cardBorder = Border.all(color: borderColor, width: 1); + static OutlineInputBorder textfieldBorder = OutlineInputBorder( + borderSide: const BorderSide(color: borderColor, width: 1), + borderRadius: borderRadius, + ); + + // Decorations + static BoxDecoration cardDecoration = BoxDecoration( + borderRadius: borderRadius, border: cardBorder, color: colorWhite); + + // Gradients + static const LinearGradient appBarGradient = LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + Color(0xFFFCF1BF), + Color(0xFFB2C1D8), + ], + ); + static const UnderlineInputBorder textfieldUnderlineBorder = + UnderlineInputBorder( + borderSide: BorderSide(color: borderColor, width: 1), + ); + + static const LinearGradient primaryGradient = LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + Color.fromARGB(159, 178, 193, 216), + Color.fromARGB(145, 252, 241, 191), + ], + ); +} diff --git a/lib/theme/assets.dart b/lib/theme/assets.dart new file mode 100644 index 00000000..5c46aea6 --- /dev/null +++ b/lib/theme/assets.dart @@ -0,0 +1,37 @@ +const assets = "assets/images/"; +const icons = "assets/icons/"; +const animation = "assets/animations/"; + +const logo = "${icons}sheru.png"; +const mainImage = "${assets}main_image.jpg"; + +//currently used loader(change later on) +const rhombusLoader = "${animation}loader.gif"; + +// icons +const iconArrowGo = "${icons}icon_arrow_go.png"; +const iconCartCheckout = "${icons}icon_cart_checkout.png"; +const iconCart = "${icons}icon_cart.png"; +const iconCategory = "${icons}icon_category.png"; +const iconCheckCircle = "${icons}icon_check_circle.png"; +const iconChevronLeft = "${icons}icon_chevron_left.png"; +const iconChevronRight = "${icons}icon_chevron_right.png"; +const iconFile = "${icons}icon_file.png"; +const iconHistory = "${icons}icon_history.png"; +const iconHome = "${icons}icon_home.png"; +const iconLocation = "${icons}icon_location.png"; +const iconMail = "${icons}icon_mail.png"; +const iconNotification = "${icons}icon_notification.png"; +const iconOrderDelivered = "${icons}icon_order_delivered.png"; +const iconOrderDispatched = "${icons}icon_order_dispatched.png"; +const iconOrderPlaced = "${icons}icon_order_placed.png"; +const iconOrderShipped = "${icons}icon_order_shipped.png"; +const iconOrder = "${icons}icon_order.png"; +const iconPayment = "${icons}icon_payment.png"; +const iconPhone = "${icons}icon_phone.png"; +const iconProfile = "${icons}icon_profile.png"; +const iconSearch = "${icons}icon_search.png"; +const iconShare = "${icons}icon_share.png"; +const iconSignout = "${icons}icon_signout.png"; +const iconSupport = "${icons}icon_support.png"; +const iconWhatsapp = "${icons}icon_whatsapp.png"; diff --git a/lib/utils/get_reference.dart b/lib/utils/get_reference.dart new file mode 100644 index 00000000..96de71d6 --- /dev/null +++ b/lib/utils/get_reference.dart @@ -0,0 +1,9 @@ +import 'dart:math'; + +String getReference() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + Random rnd = Random(); + String result = String.fromCharCodes( + Iterable.generate(6, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); + return result; +} diff --git a/lib/utils/get_uuid.dart b/lib/utils/get_uuid.dart new file mode 100644 index 00000000..1bbccbb2 --- /dev/null +++ b/lib/utils/get_uuid.dart @@ -0,0 +1,5 @@ +import 'package:uuid/uuid.dart'; + +String getUUID() { + return const Uuid().v4(); +} diff --git a/lib/utils/months.dart b/lib/utils/months.dart new file mode 100644 index 00000000..8497dd32 --- /dev/null +++ b/lib/utils/months.dart @@ -0,0 +1,30 @@ +String monthString(int month) { + switch (month) { + case 1: + return 'January'; + case 2: + return 'February'; + case 3: + return 'March'; + case 4: + return 'April'; + case 5: + return 'May'; + case 6: + return 'June'; + case 7: + return 'July'; + case 8: + return 'August'; + case 9: + return 'September'; + case 10: + return 'October'; + case 11: + return 'November'; + case 12: + return 'December'; + default: + return ''; + } +} diff --git a/pubspec.lock b/pubspec.lock index 877fc75e..b1c20ae6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" + url: "https://pub.dev" + source: hosted + version: "3.4.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae" + url: "https://pub.dev" + source: hosted + version: "5.0.0" characters: dependency: transitive description: @@ -49,6 +81,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185 + url: "https://pub.dev" + source: hosted + version: "4.17.5" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81" + url: "https://pub.dev" + source: hosted + version: "6.2.5" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9" + url: "https://pub.dev" + source: hosted + version: "3.12.5" + cloud_functions: + dependency: "direct main" + description: + name: cloud_functions + sha256: ddec68a2fbee603527c009bb20c6bd071559dfa87fda55d9d92052d1ebff5377 + url: "https://pub.dev" + source: hosted + version: "4.7.6" + cloud_functions_platform_interface: + dependency: transitive + description: + name: cloud_functions_platform_interface + sha256: "0c6fca0e64fc2d3a3834d39f99b0ee6f76d96f94bb5acf4593af891df914d175" + url: "https://pub.dev" + source: hosted + version: "5.5.28" + cloud_functions_web: + dependency: transitive + description: + name: cloud_functions_web + sha256: af536e7c7223c64250c6cc384dc553d76bbacc9b9127389df0c654887f203911 + url: "https://pub.dev" + source: hosted + version: "4.9.6" collection: dependency: transitive description: @@ -89,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.15" + dots_indicator: + dependency: "direct main" + description: + name: dots_indicator + sha256: "58b6a365744aa62aa1b70c4ea29e5106fbe064f5edaf7e9652e9b856edbfd9bb" + url: "https://pub.dev" + source: hosted + version: "3.0.0" email_validator: dependency: transitive description: @@ -113,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" file_picker: dependency: "direct main" description: @@ -181,26 +277,26 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: f0a75f61992d036e4c46ad0e9febd364d98aa2c092690a5475cb1421a8243cfe + sha256: cfc2d970829202eca09e2896f0a5aa7c87302817ecc0bdfa954f026046bf10ba url: "https://pub.dev" source: hosted - version: "4.19.5" + version: "4.20.0" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: feb77258404309ffc7761c78e1c0ad2ed5e4fdc378e035619e2cc13be4397b62 + sha256: a0270e1db3b2098a14cb2a2342b3cd2e7e458e0c391b1f64f6f78b14296ec093 url: "https://pub.dev" source: hosted - version: "7.2.6" + version: "7.3.0" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "6d527f357da2bf93a67a42b423aa92943104a0c290d1d72ad9a42c779d501cd2" + sha256: "64e067e763c6378b7e774e872f0f59f6812885e43020e25cde08f42e9459837b" url: "https://pub.dev" source: hosted - version: "5.11.5" + version: "5.12.0" firebase_core: dependency: "direct main" description: @@ -241,6 +337,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.6+33" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: a1662cc95d9750a324ad9df349b873360af6f11414902021f130c68ec02267c4 + url: "https://pub.dev" + source: hosted + version: "14.9.4" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "87c4a922cb6f811cfb7a889bdbb3622702443c52a0271636cbc90d813ceac147" + url: "https://pub.dev" + source: hosted + version: "4.5.37" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "0d34dca01a7b103ed7f20138bffbb28eb0e61a677bf9e78a028a932e2c7322d5" + url: "https://pub.dev" + source: hosted + version: "3.8.7" firebase_remote_config: dependency: "direct main" description: @@ -290,7 +410,7 @@ packages: source: hosted version: "3.9.5" firebase_ui_auth: - dependency: "direct main" + dependency: transitive description: name: firebase_ui_auth sha256: "62c3ce9c8da134e0780bf8ed7d7ed91dd2308596ee3cb56fab03eb79f8323479" @@ -329,11 +449,35 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea + url: "https://pub.dev" + source: hosted + version: "3.4.0" + flutter_easyloading: + dependency: "direct main" + description: + name: flutter_easyloading + sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c + url: "https://pub.dev" + source: hosted + version: "3.0.5" flutter_lints: dependency: "direct dev" description: @@ -355,6 +499,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.19" + flutter_spinkit: + dependency: transitive + description: + name: flutter_spinkit + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472 + url: "https://pub.dev" + source: hosted + version: "5.2.1" + flutter_staggered_animations: + dependency: "direct main" + description: + name: flutter_staggered_animations + sha256: "81d3c816c9bb0dca9e8a5d5454610e21ffb068aedb2bde49d2f8d04f75538351" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_staggered_grid_view: + dependency: "direct main" + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_svg: dependency: transitive description: @@ -373,6 +541,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc" + url: "https://pub.dev" + source: hosted + version: "8.2.8" get: dependency: "direct main" description: @@ -465,10 +641,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "33974eca2e87e8b4e3727f1b94fa3abcb25afe80b6bc2c4d449a0e150aedf720" + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" image_picker_android: dependency: transitive description: @@ -597,6 +773,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: "direct main" description: @@ -669,6 +853,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + pin_code_fields: + dependency: "direct main" + description: + name: pin_code_fields + sha256: "4c0db7fbc889e622e7c71ea54b9ee624bb70c7365b532abea0271b17ea75b729" + url: "https://pub.dev" + source: hosted + version: "8.0.1" platform: dependency: transitive description: @@ -685,6 +877,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sky_engine: dependency: transitive description: flutter @@ -698,6 +898,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + url: "https://pub.dev" + source: hosted + version: "2.5.4" stack_trace: dependency: transitive description: @@ -722,6 +946,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -746,6 +978,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" vector_graphics: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2909a374..bc5cced8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,35 +2,46 @@ name: get_flutter_fire version: 1.0.0+1 publish_to: none description: Boilerplate for Flutter with GetX, showing sample utilization of Firebase capabilities -environment: - sdk: '>=3.3.4 <4.0.0' +environment: + sdk: ">=3.3.4 <4.0.0" -dependencies: +dependencies: cupertino_icons: ^1.0.6 get: 4.6.6 - flutter: + flutter: sdk: flutter firebase_core: ^2.31.0 - firebase_ui_auth: ^1.14.0 - firebase_auth: ^4.19.5 + firebase_auth: ^4.20.0 google_sign_in: ^6.2.1 firebase_ui_oauth_google: ^1.3.2 google_fonts: ^6.2.1 firebase_storage: ^11.7.5 - image_picker: ^1.1.1 + image_picker: ^1.1.2 file_picker: ^8.0.3 path: ^1.9.0 get_storage: ^2.1.1 firebase_ui_localizations: ^1.12.0 firebase_remote_config: ^4.4.7 firebase_analytics: ^10.10.7 + cloud_firestore: ^4.17.5 + pin_code_fields: ^8.0.1 + uuid: ^4.4.2 + cloud_functions: ^4.7.6 + fluttertoast: ^8.2.8 + flutter_easyloading: ^3.0.5 + carousel_slider: ^5.0.0 + cached_network_image: ^3.4.0 + flutter_staggered_animations: ^1.1.1 + flutter_staggered_grid_view: ^0.7.0 + dots_indicator: ^3.0.0 + firebase_messaging: ^14.9.4 -dev_dependencies: +dev_dependencies: flutter_lints: 3.0.2 - flutter_test: + flutter_test: sdk: flutter -flutter: +flutter: fonts: - family: SocialIcons fonts: @@ -39,5 +50,7 @@ flutter: - assets/images/flutterfire_300x.png - assets/images/dash.png - assets/icons/logo.png + - assets/animations/ + - assets/images/ + - assets/icons/ uses-material-design: true - diff --git a/storage.rules b/storage.rules new file mode 100644 index 00000000..f08744f0 --- /dev/null +++ b/storage.rules @@ -0,0 +1,12 @@ +rules_version = '2'; + +// Craft rules based on data in your Firestore database +// allow write: if firestore.get( +// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if false; + } + } +} diff --git a/y b/y new file mode 100644 index 00000000..f08744f0 --- /dev/null +++ b/y @@ -0,0 +1,12 @@ +rules_version = '2'; + +// Craft rules based on data in your Firestore database +// allow write: if firestore.get( +// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if false; + } + } +}