diff --git a/.gitignore b/.gitignore
index c9583676..0cdd1ba4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,9 @@ migrate_working_dir/
.pub/
/build/
+node_modules
+android/app/google-services.json
+
# Symbolication related
app.*.symbols
@@ -61,10 +64,15 @@ android/app/src/main/res/values-night/styles.xml
android/app/src/profile/AndroidManifest.xml
android/gradle/wrapper/gradle-wrapper.properties
ios/.gitignore
+ios/Flutter/flutter_export_environment.sh
+ios/Flutter/Generated.xcconfig
+ios/Runner/GeneratedPluginRegistrant.h
+ios/Runner/GeneratedPluginRegistrant.m
ios/Flutter/Debug.xcconfig
ios/Flutter/Release.xcconfig
ios/Runner/AppDelegate.swift
ios/Runner/Runner-Bridging-Header.h
+ios/Runner/GoogleService-Info.plist
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
@@ -110,6 +118,7 @@ macos/.gitignore
macos/Flutter/Flutter-Debug.xcconfig
macos/Flutter/Flutter-Release.xcconfig
macos/Flutter/GeneratedPluginRegistrant.swift
+macos/Flutter/ephemeral/
macos/Runner/AppDelegate.swift
macos/Runner/DebugProfile.entitlements
macos/Runner/MainFlutterWindow.swift
diff --git a/README.md b/README.md
index be46f067..e937aa70 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@ Step 7: Additional Auth flow items
* 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]
+4. Added 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
diff --git a/assets/icons/google.png b/assets/icons/google.png
new file mode 100644
index 00000000..494acede
Binary files /dev/null and b/assets/icons/google.png differ
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index fc7f1dd4..3d1ee42c 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -52,4 +52,20 @@
NSPhotoLibraryUsageDescription
Allow access to photo library
+
+
+CFBundleURLTypes
+
+
+ CFBundleTypeRole
+ Editor
+ CFBundleURLSchemes
+
+
+
+ com.googleusercontent.apps.260821266676-n1dskq31gq05s8egsuemfl2slek3vd9b
+
+
+
+
diff --git a/lib/app/modules/login/views/custom_login.dart b/lib/app/modules/login/views/custom_login.dart
new file mode 100644
index 00000000..3184c099
--- /dev/null
+++ b/lib/app/modules/login/views/custom_login.dart
@@ -0,0 +1,275 @@
+import 'dart:developer';
+
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import '../../../../models/screens.dart';
+import '../../../../services/auth_service.dart';
+
+class CustomSignIn extends StatelessWidget {
+ const CustomSignIn({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final height = MediaQuery.of(context).size.height;
+ final width = MediaQuery.of(context).size.width;
+
+ final emailController = TextEditingController();
+ final passwordController = TextEditingController();
+
+ return Scaffold(
+ backgroundColor: Colors.grey[300],
+ body: SingleChildScrollView(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(height: height * 0.02),
+ //logo
+ Image.asset(
+ 'assets/icons/logo.png',
+ height: height * 0.25,
+ width: width * 0.25,
+ ),
+
+ SizedBox(
+ height: height * 0.02,
+ ),
+
+ // welcome back
+ const Text("You were missed! Welcome Back"),
+ SizedBox(height: height * 0.02),
+
+ //username textfield
+ InputWidget(
+ width: width,
+ emailController: emailController,
+ hintText: 'Email ID',
+ obscureText: false,
+ keyboardType: TextInputType.emailAddress,
+ ),
+ SizedBox(height: height * 0.02),
+ //password textfield
+ InputWidget(
+ width: width,
+ emailController: passwordController,
+ hintText: 'Password',
+ obscureText: true,
+ ),
+
+ //forgot password?
+ Padding(
+ padding: EdgeInsets.only(right: width * 0.055),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(
+ onPressed: () {
+ TextEditingController resetController =
+ TextEditingController();
+ Get.defaultDialog(
+ title: "Reset Passsword",
+ content: Column(
+ children: [
+ const Text("Enter your Email-ID"),
+ InputWidget(
+ width: width,
+ emailController: resetController,
+ hintText: "Email",
+ obscureText: false),
+ ],
+ ),
+ actions: [
+ TextButton(
+ child: const Text("Cancel"),
+ onPressed: () {
+ Get.back();
+ },
+ ),
+ TextButton(
+ child: const Text('Send Reset Link'),
+ onPressed: () async {
+ try {
+ await AuthService().resetPassword(
+ email: resetController.text.trim());
+ Get.snackbar('Password Reset',
+ 'A password reset link has been sent to your email.');
+ Get.back(); // Close dialog using GetX
+ } catch (e) {
+ Get.snackbar(
+ 'Error', 'Failed to send reset link.');
+ log("Error sending reset link: $e");
+ }
+ },
+ ),
+ ],
+ );
+ },
+ child: Text(
+ "Forgot Password?",
+ style: TextStyle(color: Colors.grey.shade600),
+ )),
+ ],
+ ),
+ ),
+ //sign in button
+ GestureDetector(
+ onTap: () {
+ if (emailController.text.isEmpty ||
+ !emailController.text.contains('@')) {
+ Get.snackbar(
+ 'Invalid Email',
+ "Please enter a valid email id.",
+ );
+ } else if (passwordController.text.isEmpty ||
+ passwordController.text.length < 6) {
+ Get.snackbar(
+ 'Invalid Password',
+ "Please enter a password longer than 6 letters.",
+ );
+ } else if (emailController.text.isNotEmpty &&
+ passwordController.text.isNotEmpty) {
+ AuthService()
+ .login(emailController.text, passwordController.text);
+ }
+ },
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: width * 0.05),
+ margin: EdgeInsets.symmetric(horizontal: width * 0.1),
+ height: height * 0.07,
+ width: width * 0.2,
+ decoration: BoxDecoration(
+ color: Colors.black,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: const Center(
+ child: Text("Sign In",
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ fontSize: 18))),
+ ),
+ ),
+
+ SizedBox(height: height * 0.05),
+
+ //or continue with
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(
+ width: width * 0.2,
+ child: Divider(
+ thickness: 0.5,
+ color: Colors.grey.shade900,
+ )),
+ SizedBox(
+ width: width * 0.05,
+ ),
+ const Text("Or Continue with"),
+ SizedBox(
+ width: width * 0.05,
+ ),
+ SizedBox(
+ width: width * 0.2,
+ child: Divider(
+ thickness: 0.5,
+ color: Colors.grey.shade900,
+ )),
+ ],
+ ),
+
+ SizedBox(height: height * 0.02),
+
+ //google sign in
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ GestureDetector(
+ onTap: () => AuthService().signInwithGoogle(),
+ child: Container(
+ padding: EdgeInsets.all(width * 0.03),
+ margin: EdgeInsets.symmetric(horizontal: width * 0.04),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade200,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Image.asset('assets/icons/google.png',
+ height: height * 0.045),
+ ),
+ ),
+ GestureDetector(
+ onTap: () =>
+ Get.rootDelegate.toNamed(Screen.MOBILEAUTH.route),
+ child: Container(
+ padding: EdgeInsets.all(width * 0.03),
+ margin: EdgeInsets.symmetric(horizontal: width * 0.04),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade200,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Icon(Icons.phone, size: height * 0.045),
+ ),
+ ),
+ ],
+ ),
+
+ SizedBox(height: height * 0.02),
+
+ //register now / sign up now
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text('New user?'),
+ GestureDetector(
+ onTap: () {
+ Get.rootDelegate.toNamed(Screen.SIGNUP.route);
+ },
+ child: const Text(' Register now',
+ style: TextStyle(color: Colors.blue))),
+ ],
+ ),
+ const SizedBox(
+ height: 40,
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class InputWidget extends StatelessWidget {
+ const InputWidget(
+ {super.key,
+ required this.width,
+ required this.emailController,
+ required this.hintText,
+ required this.obscureText,
+ this.keyboardType});
+
+ final double width;
+ final TextEditingController emailController;
+ final String hintText;
+ final bool obscureText;
+ final TextInputType? keyboardType;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.symmetric(horizontal: width * 0.08),
+ child: TextField(
+ controller: emailController,
+ decoration: InputDecoration(
+ enabledBorder: const OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.white)),
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.grey.shade400)),
+ fillColor: Colors.grey.shade200,
+ filled: true,
+ hintText: hintText),
+ obscureText: obscureText,
+ ),
+ );
+ }
+}
diff --git a/lib/app/modules/login/views/custom_signUp.dart b/lib/app/modules/login/views/custom_signUp.dart
new file mode 100644
index 00000000..de452068
--- /dev/null
+++ b/lib/app/modules/login/views/custom_signUp.dart
@@ -0,0 +1,225 @@
+// ignore_for_file: file_names
+
+import 'package:firebase_ui_auth/firebase_ui_auth.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import '../../../../models/screens.dart';
+import '../../../../services/auth_service.dart';
+
+class CustomSignUp extends GetView {
+ const CustomSignUp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final height = MediaQuery.of(context).size.height;
+ final width = MediaQuery.of(context).size.width;
+
+ final emailController = TextEditingController();
+ final passwordController = TextEditingController();
+ final confirmpasswordController = TextEditingController();
+ return Scaffold(
+ backgroundColor: Colors.grey[300],
+ body: SingleChildScrollView(
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(height: height * 0.02),
+
+ //logo
+ Image.asset(
+ 'assets/icons/logo.png',
+ height: height * 0.2,
+ width: width * 0.2,
+ ),
+
+ SizedBox(height: height * 0.01),
+
+ // welcome back
+ const Text("Welcome Aboard! Let's have a great time."),
+ SizedBox(height: height * 0.02),
+
+ //username textfield
+ InputWidget(
+ width: width,
+ emailController: emailController,
+ hintText: 'Email ID',
+ obscureText: false,
+ ),
+ SizedBox(height: height * 0.01),
+ //password textfield
+ InputWidget(
+ width: width,
+ emailController: passwordController,
+ hintText: 'Password',
+ obscureText: true,
+ ),
+ SizedBox(height: height * 0.01),
+ //password textfield
+ InputWidget(
+ width: width,
+ emailController: confirmpasswordController,
+ hintText: 'Confirm Password',
+ obscureText: true,
+ ),
+ SizedBox(height: height * 0.03),
+
+ //sign in button
+ GestureDetector(
+ onTap: () {
+ if (emailController.text.isEmpty ||
+ !emailController.text.contains('@')) {
+ Get.snackbar(
+ 'Invalid Email',
+ "Please enter a valid email id.",
+ );
+ } else if (passwordController.text.isEmpty ||
+ passwordController.text.length < 6) {
+ Get.snackbar(
+ 'Invalid Password',
+ "Please enter a password longer than 6 letters.",
+ );
+ } else if (passwordController.text ==
+ confirmpasswordController.text) {
+ AuthService()
+ .signUp(emailController.text, passwordController.text);
+ }
+ },
+ child: Container(
+ padding: EdgeInsets.symmetric(horizontal: width * 0.05),
+ margin: EdgeInsets.symmetric(horizontal: width * 0.1),
+ height: height * 0.07,
+ width: width * 0.2,
+ decoration: BoxDecoration(
+ color: Colors.black,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: const Center(
+ child: Text("Sign Up",
+ style: TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ fontSize: 18))),
+ ),
+ ),
+
+ SizedBox(height: height * 0.05),
+
+ //or continue with
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(
+ width: width * 0.2,
+ child: Divider(
+ thickness: 0.5,
+ color: Colors.grey.shade900,
+ )),
+ SizedBox(
+ width: width * 0.05,
+ ),
+ const Text("Or Continue with"),
+ SizedBox(
+ width: width * 0.05,
+ ),
+ SizedBox(
+ width: width * 0.2,
+ child: Divider(
+ thickness: 0.5,
+ color: Colors.grey.shade900,
+ )),
+ ],
+ ),
+
+ SizedBox(height: height * 0.01),
+
+ //google sign in
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ GestureDetector(
+ onTap: () => AuthService().signInwithGoogle(),
+ child: Container(
+ padding: EdgeInsets.all(width * 0.03),
+ margin: EdgeInsets.symmetric(horizontal: width * 0.04),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade200,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Image.asset('assets/icons/google.png',
+ height: height * 0.045),
+ ),
+ ),
+ GestureDetector(
+ onTap: () =>
+ Get.rootDelegate.toNamed(Screen.MOBILEAUTH.route),
+ child: Container(
+ padding: EdgeInsets.all(width * 0.03),
+ margin: EdgeInsets.symmetric(horizontal: width * 0.04),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade200,
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Icon(Icons.phone, size: height * 0.045),
+ ),
+ ),
+ ],
+ ),
+
+ SizedBox(height: height * 0.01),
+
+ //register now / sign up now
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text('Exisiting user?'),
+ GestureDetector(
+ onTap: () {
+ Get.rootDelegate.toNamed(Screen.LOGIN.route);
+ // print(Get.rootDelegate.history);
+ },
+ child: const Text(' Sign In',
+ style: TextStyle(color: Colors.blue))),
+ ],
+ ),
+ SizedBox(height: height * 0.1)
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class InputWidget extends StatelessWidget {
+ const InputWidget(
+ {super.key,
+ required this.width,
+ required this.emailController,
+ required this.hintText,
+ required this.obscureText});
+
+ final double width;
+ final TextEditingController emailController;
+ final String hintText;
+ final bool obscureText;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.symmetric(horizontal: width * 0.08),
+ child: TextField(
+ controller: emailController,
+ decoration: InputDecoration(
+ enabledBorder: const OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.white)),
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.grey.shade400)),
+ fillColor: Colors.grey.shade200,
+ filled: true,
+ hintText: hintText),
+ obscureText: obscureText,
+ ),
+ );
+ }
+}
diff --git a/lib/app/modules/login/views/login_view.dart b/lib/app/modules/login/views/login_view.dart
index 00c3af3f..010a7cf7 100644
--- a/lib/app/modules/login/views/login_view.dart
+++ b/lib/app/modules/login/views/login_view.dart
@@ -1,15 +1,15 @@
-// ignore_for_file: inference_failure_on_function_invocation
+// ignore_for_file: inference_failure_on_function_invocation, non_constant_identifier_names
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';
+import 'custom_login.dart';
+import 'custom_signUp.dart';
class LoginView extends GetView {
void showReverificationButton(
@@ -51,37 +51,13 @@ class LoginView extends GetView {
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(),
- );
+ if (!(GetPlatform.isAndroid || GetPlatform.isIOS) && controller.isRobot) {
+ ui = recaptcha();
+ } else {
+ ui = const CustomSignIn();
+ }
} 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(),
- );
+ ui = const CustomSignUp();
} else {
final thenTo = Get
.rootDelegate.currentConfiguration!.currentPage!.parameters?['then'];
@@ -92,6 +68,41 @@ class LoginView extends GetView {
return ui;
}
+ // RegisterScreen Register() {
+ // return 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(),
+ // );
+ // }
+
+ // SignInScreen SignIn() {
+ // return SignInScreen(
+ // providers: [
+ // GoogleProvider(
+ // clientId:
+ // clientID),
+ // 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(),
+ // );
+ // }
+
Widget recaptcha() {
//TODO: Add Recaptcha
return Scaffold(
diff --git a/lib/app/modules/phone_auth/bindings/phone_auth_binding.dart b/lib/app/modules/phone_auth/bindings/phone_auth_binding.dart
new file mode 100644
index 00000000..c3aedaaf
--- /dev/null
+++ b/lib/app/modules/phone_auth/bindings/phone_auth_binding.dart
@@ -0,0 +1,13 @@
+import 'package:get/get.dart';
+
+import '../controllers/phone_auth_controller.dart';
+
+
+class MobileAuthBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.lazyPut(
+ () => MobileAuthController(),
+ );
+ }
+}
diff --git a/lib/app/modules/phone_auth/controllers/phone_auth_controller.dart b/lib/app/modules/phone_auth/controllers/phone_auth_controller.dart
new file mode 100644
index 00000000..b4eb4393
--- /dev/null
+++ b/lib/app/modules/phone_auth/controllers/phone_auth_controller.dart
@@ -0,0 +1,18 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+// PhoneAuthController is a firebase controller name, hence that name cannot be used.
+class MobileAuthController extends GetxController {
+ final TextEditingController phoneNumberController = TextEditingController();
+ final TextEditingController otpController = TextEditingController();
+ RxString verificationId = ''.obs;
+ RxBool codeSent = false.obs;
+ RxBool isLoading = false.obs;
+
+ @override
+ void onClose() {
+ phoneNumberController.dispose();
+ otpController.dispose();
+ super.onClose();
+ }
+}
diff --git a/lib/app/modules/phone_auth/views/phone_auth.dart b/lib/app/modules/phone_auth/views/phone_auth.dart
new file mode 100644
index 00000000..8ba8d991
--- /dev/null
+++ b/lib/app/modules/phone_auth/views/phone_auth.dart
@@ -0,0 +1,153 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:get_flutter_fire/app/modules/phone_auth/controllers/phone_auth_controller.dart';
+import 'package:get_flutter_fire/services/auth_service.dart';
+
+class MobileAuth extends GetView {
+ const MobileAuth({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Phone Authentication'),
+ centerTitle: true,
+ ),
+ body: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Obx(
+ () => controller.codeSent.value
+ ? OtpScreen(verificationId: controller.verificationId.value)
+ : const VerifyPhoneScreen(),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class VerifyPhoneScreen extends GetView {
+ const VerifyPhoneScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 500),
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ TextField(
+ controller: controller.phoneNumberController,
+ keyboardType: TextInputType.phone,
+ decoration: InputDecoration(
+ hintText: 'Enter Phone Number',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ prefixIcon: const Icon(Icons.phone),
+ ),
+ ),
+ const SizedBox(height: 20),
+ Obx(() => controller.isLoading.value
+ ? const Center(child: CircularProgressIndicator())
+ : ElevatedButton(
+ onPressed: () => AuthService().verifyMobileNumber(
+ controller.phoneNumberController.text),
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 15),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ ),
+ child: const Text(
+ 'Send OTP',
+ style: TextStyle(fontSize: 16),
+ ),
+ )),
+ ],
+ ),
+ )));
+
+ // return Column(
+ // mainAxisAlignment: MainAxisAlignment.center,
+ // children: [
+ // TextField(
+ // controller: controller.phoneNumberController,
+ // keyboardType: TextInputType.phone,
+ // decoration: const InputDecoration(hintText: 'Enter Phone Number'),
+ // ),
+ // const SizedBox(height: 20),
+ // Obx(() => controller.isLoading.value
+ // ? const CircularProgressIndicator()
+ // : ElevatedButton(
+ // onPressed: () => AuthService()
+ // .verifyMobileNumber(controller.phoneNumberController.text),
+ // child: const Text('Send OTP'),
+ // )),
+ // ],
+ // );
+ }
+}
+
+class OtpScreen extends GetView {
+ final String verificationId;
+ const OtpScreen({super.key, required this.verificationId});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ // appBar: AppBar(
+ // title: const Text('Verify OTP'),
+ // centerTitle: true,
+ // ),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text(
+ 'Enter the OTP sent to your mobile number',
+ style: TextStyle(fontSize: 18),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 20),
+ TextField(
+ controller: controller.otpController,
+ keyboardType: TextInputType.number,
+ decoration: InputDecoration(
+ hintText: 'Enter OTP',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ contentPadding:
+ const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
+ ),
+ ),
+ const SizedBox(height: 20),
+ Obx(() => controller.isLoading.value
+ ? const CircularProgressIndicator()
+ : SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () => AuthService().verifyOTP(
+ controller.otpController.text, verificationId),
+ style: ElevatedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 15),
+ textStyle: const TextStyle(fontSize: 16),
+ ),
+ child: const Text('Verify OTP'),
+ ),
+ )),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/app/modules/profile/views/profile_view.dart b/lib/app/modules/profile/views/profile_view.dart
index c26d11c1..cd9b8e28 100644
--- a/lib/app/modules/profile/views/profile_view.dart
+++ b/lib/app/modules/profile/views/profile_view.dart
@@ -6,6 +6,7 @@ import 'package:get/get.dart';
import '../../../../services/auth_service.dart';
import '../../../../models/screens.dart';
+import '../../../utils/img_constants.dart';
import '../../../widgets/change_password_dialog.dart';
import '../../../widgets/image_picker_button.dart';
import '../controllers/profile_controller.dart';
@@ -62,7 +63,7 @@ class ProfileView extends GetView {
)
: Center(
child: Image.asset(
- 'assets/images/dash.png',
+ ImgConstants.dash,
width: size,
fit: BoxFit.contain,
),
diff --git a/lib/app/modules/root/views/drawer.dart b/lib/app/modules/root/views/drawer.dart
index 908d0223..ed5eb929 100644
--- a/lib/app/modules/root/views/drawer.dart
+++ b/lib/app/modules/root/views/drawer.dart
@@ -7,7 +7,8 @@ import '../../../../models/role.dart';
import '../../../../services/auth_service.dart';
import '../../../../models/screens.dart';
-import '../controllers/my_drawer_controller.dart';
+import '../../../../services/remote_config.dart';
+import '../../../widgets/remotely_config_obx.dart';
class DrawerWidget extends StatelessWidget {
const DrawerWidget({
@@ -16,23 +17,26 @@ class DrawerWidget extends StatelessWidget {
@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),
- ),
- ));
+ return RemotelyConfigObxVal.noparam(
+ (data) => 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, data),
+ ),
+ ),
+ List.empty().obs,
+ "useBottomSheetForProfileOptions",
+ Typer.boolean,
+ func: Screen.drawer,
+ );
}
- List drawerItems(BuildContext context, Rx> values) {
+ List drawerItems(BuildContext context, Iterable values) {
List list = [
Container(
height: 100,
@@ -67,7 +71,7 @@ class DrawerWidget extends StatelessWidget {
}
}
- for (Screen screen in values.value) {
+ for (Screen screen in values) {
list.add(ListTile(
title: Text(screen.label ?? ''),
onTap: () {
diff --git a/lib/app/modules/root/views/root_view.dart b/lib/app/modules/root/views/root_view.dart
index 2bbf228c..c7c6c0ca 100644
--- a/lib/app/modules/root/views/root_view.dart
+++ b/lib/app/modules/root/views/root_view.dart
@@ -5,6 +5,8 @@ import 'package:get/get.dart';
import 'package:get_flutter_fire/services/auth_service.dart';
import '../../../routes/app_pages.dart';
import '../../../../models/screens.dart';
+import '../../../utils/icon_constants.dart';
+import '../../../widgets/screen_widget.dart';
import '../controllers/root_controller.dart';
import 'drawer.dart';
@@ -31,14 +33,14 @@ class RootView extends GetView {
)
: IconButton(
icon: ImageIcon(
- const AssetImage("icons/logo.png"),
+ const AssetImage(IconConstants.logo),
color: Colors.grey.shade800,
),
onPressed: () => AuthService.to.isLoggedInValue
? controller.openDrawer()
: {Screen.HOME.doAction()},
),
- actions: topRightMenuButtons(current),
+ actions: ScreenWidgetExtension.topRightMenuButtons(current),
// automaticallyImplyLeading: false, //removes drawer icon
),
body: GetRouterOutlet(
@@ -52,13 +54,4 @@ class RootView extends GetView {
},
);
}
-
-//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/search/bindings/search_binding.dart b/lib/app/modules/search/bindings/search_binding.dart
new file mode 100644
index 00000000..64cfa36f
--- /dev/null
+++ b/lib/app/modules/search/bindings/search_binding.dart
@@ -0,0 +1,12 @@
+import 'package:get/get.dart';
+
+import '../controllers/search_controller.dart';
+
+class SearchBinding extends Bindings {
+ @override
+ void dependencies() {
+ Get.lazyPut(
+ () => SearchController(),
+ );
+ }
+}
diff --git a/lib/app/modules/search/controllers/search_controller.dart b/lib/app/modules/search/controllers/search_controller.dart
new file mode 100644
index 00000000..f282fabb
--- /dev/null
+++ b/lib/app/modules/search/controllers/search_controller.dart
@@ -0,0 +1,23 @@
+import 'package:get/get.dart';
+
+class SearchController extends GetxController {
+ //TODO: Implement SearchController
+
+ 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/search/views/search_view.dart b/lib/app/modules/search/views/search_view.dart
new file mode 100644
index 00000000..f0c3c16c
--- /dev/null
+++ b/lib/app/modules/search/views/search_view.dart
@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart' hide SearchController;
+
+import 'package:get/get.dart';
+
+import '../controllers/search_controller.dart';
+
+class SearchView extends GetView {
+ const SearchView({Key? key}) : super(key: key);
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('SearchView'),
+ centerTitle: true,
+ ),
+ body: const Center(
+ child: Text(
+ 'SearchView is working',
+ style: TextStyle(fontSize: 20),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart
index 7269755d..f68c06a2 100644
--- a/lib/app/routes/app_pages.dart
+++ b/lib/app/routes/app_pages.dart
@@ -1,8 +1,13 @@
import 'package:flutter/material.dart';
+
import 'package:get/get.dart';
+import 'package:get_flutter_fire/app/modules/login/views/custom_signUp.dart';
+import 'package:get_flutter_fire/app/modules/phone_auth/bindings/phone_auth_binding.dart';
+import 'package:get_flutter_fire/app/modules/phone_auth/views/phone_auth.dart';
import '../../models/access_level.dart';
import '../../models/role.dart';
+import '../../models/screens.dart';
import '../middleware/auth_middleware.dart';
import '../modules/cart/bindings/cart_binding.dart';
import '../modules/cart/views/cart_view.dart';
@@ -28,6 +33,8 @@ 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/search/bindings/search_binding.dart';
+import '../modules/search/views/search_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';
@@ -36,7 +43,6 @@ 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';
@@ -60,6 +66,14 @@ class AppPages {
page: () => const LoginView(),
binding: LoginBinding(),
),
+ Screen.SIGNUP.getPage(
+ page: () => const CustomSignUp(),
+ binding: LoginBinding(),
+ ),
+ Screen.MOBILEAUTH.getPage(
+ page: () => const MobileAuth(),
+ binding: MobileAuthBinding(),
+ ),
Screen.REGISTER.getPage(
page: () => const RegisterView(),
binding: RegisterBinding(),
@@ -150,5 +164,9 @@ class AppPages {
)
],
),
+ Screen.SEARCH.getPage(
+ page: () => const SearchView(),
+ binding: SearchBinding(),
+ ),
];
}
diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart
index f3129d21..7391dcc0 100644
--- a/lib/app/routes/app_routes.dart
+++ b/lib/app/routes/app_routes.dart
@@ -30,6 +30,7 @@ abstract class Routes {
'${Screen.LOGIN.route}?then=${Uri.encodeQueryComponent(afterSuccessfulLogin)}';
static String REGISTER_THEN(String afterSuccessfulLogin) =>
'${Screen.REGISTER.route}?then=${Uri.encodeQueryComponent(afterSuccessfulLogin)}';
+ // static const SEARCH = _Paths.SEARCH;
}
// Keeping this as Get_Cli will require it. Any addition can later be added to Screen
@@ -51,4 +52,5 @@ abstract class _Paths {
// static const USERS = '/users';
// static const USER_PROFILE = '/:uId';
// static const MY_PRODUCTS = '/my-products';
+ // static const SEARCH = '/search';
}
diff --git a/lib/app/routes/screen_extension.dart b/lib/app/routes/screen_extension.dart
index aaf138b0..23877c7d 100644
--- a/lib/app/routes/screen_extension.dart
+++ b/lib/app/routes/screen_extension.dart
@@ -110,11 +110,14 @@ extension ScreenExtension on Screen {
extension RoleExtension on Role {
int getCurrentIndexFromRoute(GetNavConfig? currentRoute) {
- final String? currentLocation = currentRoute?.location;
+ final String? currentLocation = currentRoute?.uri.path;
int currentIndex = 0;
if (currentLocation != null) {
- currentIndex =
- tabs.indexWhere((tab) => currentLocation.startsWith(tab.path));
+ currentIndex = tabs.indexWhere((tab) {
+ String parentPath = tab.parent?.path ?? '';
+ String fullPath = '$parentPath${tab.path}';
+ return currentLocation.startsWith(fullPath);
+ });
}
return (currentIndex > 0) ? currentIndex : 0;
}
diff --git a/lib/app/utils/icon_constants.dart b/lib/app/utils/icon_constants.dart
new file mode 100644
index 00000000..b3351a95
--- /dev/null
+++ b/lib/app/utils/icon_constants.dart
@@ -0,0 +1,5 @@
+abstract class IconConstants {
+ static const _assetsIcon = 'assets/icons';
+
+ static const logo = '$_assetsIcon/logo.png';
+}
diff --git a/lib/app/utils/img_constants.dart b/lib/app/utils/img_constants.dart
new file mode 100644
index 00000000..83a5c0c6
--- /dev/null
+++ b/lib/app/utils/img_constants.dart
@@ -0,0 +1,6 @@
+abstract class ImgConstants {
+ static const _assetsImg = 'assets/images';
+
+ static const dash = '$_assetsImg/dash.png';
+ static const flutterfire = '$_assetsImg/flutterfire_300x.png';
+}
diff --git a/lib/app/widgets/login_widgets.dart b/lib/app/widgets/login_widgets.dart
index b8f2d8c1..63162b44 100644
--- a/lib/app/widgets/login_widgets.dart
+++ b/lib/app/widgets/login_widgets.dart
@@ -6,7 +6,9 @@ import 'package:get/get.dart';
import '../../services/auth_service.dart';
import '../../models/screens.dart';
import '../../services/remote_config.dart';
+import '../utils/img_constants.dart';
import 'menu_sheet_button.dart';
+import 'remotely_config_obx.dart';
class LoginWidgets {
static Widget headerBuilder(context, constraints, shrinkOffset) {
@@ -14,7 +16,7 @@ class LoginWidgets {
padding: const EdgeInsets.all(20),
child: AspectRatio(
aspectRatio: 1,
- child: Image.asset('assets/images/flutterfire_300x.png'),
+ child: Image.asset(ImgConstants.flutterfire),
),
);
}
@@ -38,7 +40,7 @@ class LoginWidgets {
padding: const EdgeInsets.all(20),
child: AspectRatio(
aspectRatio: 1,
- child: Image.asset('assets/images/flutterfire_300x.png'),
+ child: Image.asset(ImgConstants.flutterfire),
),
);
}
@@ -56,31 +58,36 @@ class LoginBottomSheetToggle extends MenuSheetButton {
@override
Icon? get icon => (AuthService.to.isLoggedInValue)
- ? values.length == 1
+ ? 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
+ ? values.length <= 1
? 'Logout'
: 'Click for Options'
: 'Login';
+ @override
+ void buttonPressed(Iterable values) async => values.isEmpty
+ ? callbackFunc(await Screen.LOGOUT.doAction())
+ : super.buttonPressed(values);
+
@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)}));
+ MenuItemsController controller =
+ MenuItemsController(const Iterable.empty());
return Obx(() => (AuthService.to.isLoggedInValue)
- ? builder(context, vals: controller.values.value)
+ ? RemotelyConfigObx(
+ () => builder(context, vals: controller.values.value),
+ controller,
+ Screen.sheet,
+ Screen.NONE,
+ "useBottomSheetForProfileOptions",
+ Typer.boolean,
+ )
: !(current.currentPage!.name == Screen.LOGIN.path)
? IconButton(
onPressed: () async {
diff --git a/lib/app/widgets/menu_sheet_button.dart b/lib/app/widgets/menu_sheet_button.dart
index abd3873e..31649e4d 100644
--- a/lib/app/widgets/menu_sheet_button.dart
+++ b/lib/app/widgets/menu_sheet_button.dart
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../models/action_enum.dart';
+import 'remotely_config_obx.dart';
-class MenuItemsController extends GetxController {
- MenuItemsController(Iterable iter) : values = Rx>(iter);
-
- final Rx> values;
+class MenuItemsController
+ extends RemoteConfigController> {
+ MenuItemsController(super.iter);
}
class MenuSheetButton extends StatelessWidget {
@@ -65,7 +65,7 @@ class MenuSheetButton extends StatelessWidget {
//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 ||
+ return values.length <= 1 ||
Get.mediaQuery.orientation == Orientation.portrait
// : Get.context!.isPortrait
? (icon != null
diff --git a/lib/app/widgets/remotely_config_obx.dart b/lib/app/widgets/remotely_config_obx.dart
new file mode 100644
index 00000000..684503c0
--- /dev/null
+++ b/lib/app/widgets/remotely_config_obx.dart
@@ -0,0 +1,58 @@
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+import '../../services/remote_config.dart';
+
+class RemoteConfigController extends GetxController {
+ RemoteConfigController(L iter) : values = Rx(iter);
+ final Rx values;
+}
+
+class RemotelyConfigObx>>
+ extends ObxWidget {
+ final Widget Function() builder;
+ final X param;
+ final C controller;
+ final String config;
+ final Typer configType;
+ final Future Function(X) func;
+
+ RemotelyConfigObx(this.builder, this.controller, this.func, this.param,
+ this.config, this.configType,
+ {super.key}) {
+ Get.put(controller, permanent: true); //must make true else gives error
+ func(param).then((v) {
+ controller.values.value = v;
+ });
+ RemoteConfig.instance.then((ins) => ins.addListener(config, configType,
+ (val) async => {controller.values.value = await func(param)}));
+ }
+
+ @override
+ Widget build() => builder();
+}
+
+class RemotelyConfigObxVal extends ObxWidget {
+ final Widget Function(T) builder;
+ final T data;
+ final String config;
+ final Typer configType;
+
+ RemotelyConfigObxVal(this.builder, this.data, this.config, this.configType,
+ {super.key, required Future Function(X) func, required X param}) {
+ func(param).then((v) => {data.value = v});
+ RemoteConfig.instance.then((ins) => ins.addListener(
+ config, configType, (val) async => {data.value = await func(param)}));
+ }
+
+ RemotelyConfigObxVal.noparam(
+ this.builder, this.data, this.config, this.configType,
+ {super.key, required Future Function() func}) {
+ func().then((v) => {data.value = v});
+ RemoteConfig.instance.then((ins) => ins.addListener(
+ config, configType, (val) async => {data.value = await func()}));
+ }
+
+ @override
+ Widget build() => builder(data);
+}
diff --git a/lib/app/widgets/screen_widget.dart b/lib/app/widgets/screen_widget.dart
index d80c9275..438f91e5 100644
--- a/lib/app/widgets/screen_widget.dart
+++ b/lib/app/widgets/screen_widget.dart
@@ -3,6 +3,7 @@ import 'package:get/get.dart';
import '../routes/app_pages.dart';
import '../../models/role.dart';
import '../../models/screens.dart';
+import 'login_widgets.dart';
class ScreenWidget extends StatelessWidget {
final Widget body;
@@ -73,3 +74,31 @@ class ScreenWidget extends StatelessWidget {
return null; //TODO multi fab button on press
}
}
+
+extension ScreenWidgetExtension on Screen {
+ Widget? widget(GetNavConfig current) {
+ //those with accessor == widget must be handled here
+ switch (this) {
+ case Screen.SEARCH:
+ return IconButton(onPressed: () => {}, icon: Icon(icon));
+ case Screen.LOGIN:
+ return LoginBottomSheetToggle(current);
+ case Screen.LOGOUT:
+ return LoginBottomSheetToggle(current);
+ default:
+ }
+ return null;
+ }
+
+//This could be used to add icon buttons in expanded web view instead of the context menu
+ static List topRightMenuButtons(GetNavConfig current) {
+ List widgets = [];
+ for (var screen in Screen.topRightMenu()) {
+ widgets.add(Container(
+ margin: const EdgeInsets.only(right: 15),
+ child: screen.widget(current)));
+ }
+
+ return widgets; //This will return empty. We need a Obx
+ }
+}
diff --git a/lib/app/widgets/search_bar_button.dart b/lib/app/widgets/search_bar_button.dart
new file mode 100644
index 00000000..14f60d37
--- /dev/null
+++ b/lib/app/widgets/search_bar_button.dart
@@ -0,0 +1,3 @@
+// This uses Remote Config to know where to locate the search button
+// If on top, then it expands to title area on press in mobiles and is already expanded in web
+// If on Nav Bar, the it initiates a SearchAnchor as a bottomsheet in mobile and like a drawer/nav column in web
\ No newline at end of file
diff --git a/lib/models/access_level.dart b/lib/models/access_level.dart
index a7b89742..20b79252 100644
--- a/lib/models/access_level.dart
+++ b/lib/models/access_level.dart
@@ -1,7 +1,7 @@
enum AccessLevel {
+ notAuthed, // used for login screens
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
diff --git a/lib/models/screens.dart b/lib/models/screens.dart
index 24dee39f..601f4940 100644
--- a/lib/models/screens.dart
+++ b/lib/models/screens.dart
@@ -1,6 +1,5 @@
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';
@@ -8,9 +7,13 @@ 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
+ widget,
+ topRight,
+ topCenter,
+ topLeft,
+ topBar, //bar below the main top menu bar
+ navigator, //bottom nav. can be linked to drawer items. left strip in expanded web //handled in ScreenWidget
+ drawer, //creates nav tree. persistant in expanded web and linked with nav icons //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
@@ -18,6 +21,7 @@ enum AccessedVia {
}
enum Screen implements ActionEnum {
+ NONE.none(), //null
HOME('/home',
icon: Icons.home,
label: "Home",
@@ -37,7 +41,11 @@ enum Screen implements ActionEnum {
parent: HOME),
PRODUCT_DETAILS('/:productId',
accessLevel: AccessLevel.public, parent: PRODUCTS),
- LOGIN('/login',
+ SIGNUP('/signUp',
+ icon: Icons.login,
+ accessor_: AccessedVia.widget,
+ accessLevel: AccessLevel.notAuthed),
+ MOBILEAUTH('/mobileAuth',
icon: Icons.login,
accessor_: AccessedVia.widget,
accessLevel: AccessLevel.notAuthed),
@@ -46,13 +54,13 @@ enum Screen implements ActionEnum {
label: "Profile",
accessor_: AccessedVia.drawer,
accessLevel: AccessLevel.authenticated,
- remoteConfig: true),
+ remoteConfig: "useBottomSheetForProfileOptions"),
SETTINGS('/settings',
icon: Icons.settings,
label: "Settings",
accessor_: AccessedVia.drawer,
accessLevel: AccessLevel.authenticated,
- remoteConfig: true),
+ remoteConfig: "useBottomSheetForProfileOptions"),
CART('/cart',
icon: Icons.trolley,
label: "Cart",
@@ -96,20 +104,40 @@ enum Screen implements ActionEnum {
accessLevel: AccessLevel.roleBased),
MY_PRODUCT_DETAILS('/:productId',
parent: MY_PRODUCTS, accessLevel: AccessLevel.roleBased),
+ SEARCH('/search',
+ icon: Icons.search,
+ label: "Search",
+ accessor_: AccessedVia.topRight,
+ remoteConfig: "showSearchBarOnTop",
+ accessLevel: AccessLevel.public),
LOGOUT('/login',
icon: Icons.logout,
label: "Logout",
- accessor_: AccessedVia.bottomSheet,
+ accessor_: AccessedVia.topRight,
+ remoteConfig: "useBottomSheetForProfileOptions",
accessLevel: AccessLevel.authenticated),
+ LOGIN('/login',
+ icon: Icons.login,
+ accessor_: AccessedVia.topRight,
+ accessLevel: AccessLevel.notAuthed),
;
const Screen(this.path,
{this.icon,
this.label,
- this.parent,
+ this.parent = Screen.NONE,
this.accessor_ = AccessedVia.singleTap,
this.accessLevel = AccessLevel.authenticated,
- this.remoteConfig = false});
+ this.remoteConfig});
+
+ const Screen.none()
+ : path = '',
+ icon = null,
+ label = null,
+ parent = null,
+ accessor_ = AccessedVia.singleTap,
+ accessLevel = AccessLevel.authenticated,
+ remoteConfig = null;
@override
final IconData? icon;
@@ -121,10 +149,10 @@ enum Screen implements ActionEnum {
final Screen? parent;
final AccessLevel
accessLevel; //if false it is role based. true means allowed for all
- final bool remoteConfig;
+ final String? remoteConfig;
Future get accessor async {
- if (remoteConfig &&
+ if (remoteConfig == "useBottomSheetForProfileOptions" &&
(await RemoteConfig.instance).useBottomSheetForProfileOptions()) {
return AccessedVia.bottomSheet;
}
@@ -164,6 +192,12 @@ enum Screen implements ActionEnum {
return list;
}
+ static Iterable topRightMenu() {
+ return Screen.values.where((Screen screen) =>
+ screen.accessor_ == AccessedVia.topRight &&
+ AuthService.to.accessLevel.index >= screen.accessLevel.index);
+ }
+
@override
Future doAction() async {
if (this == LOGOUT) {
@@ -171,7 +205,4 @@ enum Screen implements ActionEnum {
}
Get.rootDelegate.toNamed(route);
}
-
- Widget? widget(GetNavConfig current) =>
- (this == LOGIN) ? LoginBottomSheetToggle(current) : null;
}
diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart
index 8bf72aaa..e2f261c6 100644
--- a/lib/services/auth_service.dart
+++ b/lib/services/auth_service.dart
@@ -1,10 +1,16 @@
// ignore_for_file: avoid_print
+import 'dart:developer';
+
+import 'package:cloud_functions/cloud_functions.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 'package:get_flutter_fire/app/modules/phone_auth/controllers/phone_auth_controller.dart';
+import 'package:google_sign_in/google_sign_in.dart';
+import 'package:get_flutter_fire/models/access_level.dart';
import '../models/screens.dart';
import '../constants.dart';
@@ -26,17 +32,30 @@ class AuthService extends GetxService {
@override
onInit() {
super.onInit();
- if (useEmulator) _auth.useAuthEmulator(emulatorHost, 9099);
+ 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"]);
});
+ Get.rootDelegate.offNamed(Screen.HOME.route);
+ } else if (user == null) {
+ Get.rootDelegate.offNamed(Screen.LOGIN.route);
}
});
}
+ AccessLevel get accessLevel => user != null
+ ? user!.isAnonymous
+ ? _userRole.value.index > Role.buyer.index
+ ? AccessLevel.roleBased
+ : AccessLevel.authenticated
+ : AccessLevel.guest
+ : AccessLevel.public;
+
bool get isEmailVerified =>
user != null && (user!.email == null || user!.emailVerified);
@@ -52,8 +71,119 @@ class AuthService extends GetxService {
? (user!.displayName ?? user!.email)
: 'Guest';
- void login() {
- // this is not needed as we are using Firebase UI for the login part
+// Function to handle google sign in.
+//* Can use silent sign in to make the flow smoother on the web.
+ final GoogleSignIn _googleSignIn =
+ GoogleSignIn(clientId: const String.fromEnvironment('CLIENT_ID'));
+ Future signInwithGoogle() async {
+ try {
+ final GoogleSignInAccount? googleSignInAccount =
+ await _googleSignIn.signIn();
+ final GoogleSignInAuthentication googleSignInAuthentication =
+ await googleSignInAccount!.authentication;
+ final AuthCredential credential = GoogleAuthProvider.credential(
+ accessToken: googleSignInAuthentication.accessToken,
+ idToken: googleSignInAuthentication.idToken,
+ );
+ await _auth.signInWithCredential(credential);
+ } on FirebaseAuthException catch (e) {
+ print(e.message);
+ rethrow;
+ }
+ return null;
+ }
+
+ // Functions to handle phone authentication.
+ // Function to verify the phone number. On android this tries to read the code directly without the user having to manually enter the code.
+ Future verifyMobileNumber(String phoneNumber) async {
+ await _auth.verifyPhoneNumber(
+ phoneNumber: phoneNumber,
+ verificationCompleted: (PhoneAuthCredential credential) async {
+ await _auth.signInWithCredential(credential);
+ },
+ verificationFailed: (FirebaseAuthException e) {
+ String errorMessage = 'Verification failed';
+ switch (e.code) {
+ case 'invalid-verification-code':
+ errorMessage = 'Invalid SMS code.';
+ break;
+ case 'expired-verification-code':
+ errorMessage = 'SMS code has expired.';
+ break;
+ default:
+ errorMessage = 'An unknown error occurred. Please try again later.';
+ }
+ Get.snackbar(errorMessage, '');
+ },
+ codeSent: (String verificationId, int? resendToken) {
+ print("Code sent");
+ Get.find().codeSent.value = true;
+ Get.find().verificationId.value = verificationId;
+ },
+ codeAutoRetrievalTimeout: (String verificationId) {},
+ timeout: const Duration(seconds: 60),
+ );
+ }
+
+ // Verify the OTP.
+ Future verifyOTP(String smsCode, String verificationId) async {
+ try {
+ PhoneAuthCredential credential = PhoneAuthProvider.credential(
+ verificationId: verificationId, smsCode: smsCode);
+ await _auth.signInWithCredential(credential);
+ print("Done");
+ } catch (e) {
+ print(e.toString());
+ }
+ }
+
+ Future signUp(String email, String password) async {
+ try {
+ final credential =
+ await FirebaseAuth.instance.createUserWithEmailAndPassword(
+ email: email,
+ password: password,
+ );
+ } on FirebaseAuthException catch (e) {
+ if (e.code == 'weak-password') {
+ print('The password provided is too weak.');
+ } else if (e.code == 'email-already-in-use') {
+ print('The account already exists for that email.');
+ }
+ } catch (e) {
+ print(e);
+ }
+ }
+
+ Future login(String email, String password) async {
+ // isLoading.value = true;
+ try {
+ await _auth.signInWithEmailAndPassword(email: email, password: password);
+ } on FirebaseAuthException catch (e) {
+ print(e);
+ String errorMessage;
+
+ switch (e.code) {
+ case 'user-not-found':
+ errorMessage = 'No user found for that email.';
+ break;
+ case 'wrong-password':
+ errorMessage = 'Wrong password provided for that user.';
+ break;
+ case 'invalid-email':
+ errorMessage = 'The email address is badly formatted.';
+ break;
+ default:
+ errorMessage =
+ 'An unknown error occurred. Please try again later. ${e.toString()}';
+ }
+ Get.snackbar('Login Error', errorMessage);
+ } catch (error) {
+ Get.snackbar('Error', 'An unexpected error occurred. Please try again.');
+ print("Error during login: ${error.toString()}");
+ } finally {
+ // isLoading.value = false;
+ }
}
void sendVerificationMail({EmailAuthCredential? emailAuth}) async {
@@ -116,6 +246,36 @@ class AuthService extends GetxService {
.offAndToNamed(thenTo ?? Screen.PROFILE.route); //Profile has the forms
}
+ Future signup(String email, String password) async {
+ try {
+ UserCredential userCredential =
+ await _auth.createUserWithEmailAndPassword(
+ email: email,
+ password: password,
+ );
+ print("User created");
+ HttpsCallableResult result =
+ await FirebaseFunctions.instance.httpsCallable('addUserRole').call({
+ 'email': email,
+ });
+
+ print("Result: $result");
+
+ // Optionally send verification email
+ if (true) {
+ await userCredential.user!.sendEmailVerification();
+ }
+
+ registered.value = true;
+ // Navigate to profile or other destination
+ final thenTo = Get
+ .rootDelegate.currentConfiguration!.currentPage!.parameters?['then'];
+ Get.rootDelegate.offAndToNamed(thenTo ?? Screen.PROFILE.route);
+ } catch (e) {
+ Get.snackbar('Registration Error', 'Failed to register: $e');
+ }
+ }
+
void logout() {
_auth.signOut();
if (isAnon) _auth.currentUser?.delete();
@@ -181,6 +341,15 @@ class AuthService extends GetxService {
};
};
}
+
+ resetPassword({required String email}) {
+ try {
+ _auth.sendPasswordResetEmail(email: email);
+ } catch (e) {
+ log("Error sending reset link: $e");
+ rethrow;
+ }
+ }
}
class MyCredential extends AuthCredential {
diff --git a/lib/services/remote_config.dart b/lib/services/remote_config.dart
index 5d1145a5..cb2a8928 100644
--- a/lib/services/remote_config.dart
+++ b/lib/services/remote_config.dart
@@ -13,7 +13,6 @@ class RemoteConfig {
}
final FirebaseRemoteConfig _remoteConfig = FirebaseRemoteConfig.instance;
- final List listeners = [];
Future init() async {
await _remoteConfig.setConfigSettings(RemoteConfigSettings(
@@ -68,11 +67,4 @@ class RemoteConfig {
bool showSearchBarOnTop() {
return _remoteConfig.getBool("showSearchBarOnTop");
}
-
- void addUseBottomSheetForProfileOptionsListener(listener) {
- addListener("useBottomSheetForProfileOptions", Typer.boolean, listener);
- if (!listeners.contains(listener)) {
- listeners.add(listener);
- }
- }
}
diff --git a/pubspec.lock b/pubspec.lock
index 877fc75e..38a4c2f5 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -49,6 +49,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
+ 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:
diff --git a/pubspec.yaml b/pubspec.yaml
index 2909a374..9bf993cd 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,13 +2,13 @@ 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
@@ -24,20 +24,20 @@ dependencies:
firebase_ui_localizations: ^1.12.0
firebase_remote_config: ^4.4.7
firebase_analytics: ^10.10.7
+ cloud_functions: ^4.7.6
-dev_dependencies:
+dev_dependencies:
flutter_lints: 3.0.2
- flutter_test:
+ flutter_test:
sdk: flutter
-flutter:
+flutter:
fonts:
- family: SocialIcons
fonts:
- asset: packages/firebase_ui_auth/fonts/SocialIcons.ttf
assets:
- - assets/images/flutterfire_300x.png
- - assets/images/dash.png
- - assets/icons/logo.png
+ - assets/
+ - assets/images/
+ - assets/icons/
uses-material-design: true
-