diff --git a/lib.zip b/lib.zip deleted file mode 100644 index d46d707..0000000 Binary files a/lib.zip and /dev/null differ diff --git a/lib/controller/payment/payment_controller.dart b/lib/controller/payment/payment_controller.dart index 29547cd..acb12b1 100644 --- a/lib/controller/payment/payment_controller.dart +++ b/lib/controller/payment/payment_controller.dart @@ -4,27 +4,16 @@ import 'package:marco/helpers/services/payment_service.dart'; import 'package:marco/helpers/services/app_logger.dart'; class PaymentController with ChangeNotifier { - final Razorpay _razorpay = Razorpay(); + Razorpay? _razorpay; final PaymentService _paymentService = PaymentService(); bool isProcessing = false; - BuildContext? _context; // For showing dialogs/snackbars - - PaymentController() { - // Razorpay event listeners - _razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess); - _razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError); - _razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet); - } - - void disposeController() { - _razorpay.clear(); - } + BuildContext? _context; /// ============================== - /// START PAYMENT + /// START PAYMENT (Safe init) /// ============================== - Future startPayment({ + Future startPayment({ required double amount, required String description, required BuildContext context, @@ -35,44 +24,74 @@ class PaymentController with ChangeNotifier { logSafe("🟢 Starting payment for ₹$amount - $description"); - // Call backend to create Razorpay order - final result = await _paymentService.createOrder(amount); + // ✅ Clear any old instance (prevents freeze on re-init) + _cleanup(); - if (result == null) { - _showError(context, "Failed to connect to server."); - isProcessing = false; - notifyListeners(); - return; + // ✅ Create order first (no login dependency) + Map? result; + try { + result = await _paymentService + .createOrder(amount) + .timeout(const Duration(seconds: 10)); + } catch (e) { + logSafe("⏱️ API Timeout or Exception while creating order: $e", + level: LogLevel.error); } - final orderId = result['orderId']; - final key = result['key']; + if (result == null) { + _showError(context, "Failed to connect to server or timeout."); + isProcessing = false; + notifyListeners(); + return false; + } + final orderId = result['data']?['orderId']; + final key = result['data']?['key']; if (orderId == null || key == null) { _showError(context, "Invalid response from server."); isProcessing = false; notifyListeners(); - return; + return false; } - var options = { + // ✅ Safe initialization of Razorpay (deferred) + try { + logSafe("🟡 Initializing Razorpay instance..."); + _razorpay = Razorpay(); + _razorpay?.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess); + _razorpay?.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError); + _razorpay?.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet); + logSafe("✅ Razorpay instance initialized successfully."); + } catch (e) { + logSafe("❌ Razorpay init failed: $e", level: LogLevel.error); + _showError(context, "Payment system initialization failed."); + isProcessing = false; + notifyListeners(); + return false; + } + + // ✅ Prepare payment options + final options = { 'key': key, - 'amount': (amount * 100).toInt(), // Razorpay takes amount in paise + 'amount': (amount * 100).toInt(), 'name': 'Your Company Name', 'description': description, 'order_id': orderId, 'theme': {'color': '#0D47A1'}, - 'timeout': 120, // 2 minutes timeout + 'timeout': 120, // seconds }; try { - logSafe("🟠 Opening Razorpay with options: $options"); - _razorpay.open(options); + logSafe("🟠 Opening Razorpay checkout with options: $options"); + _razorpay!.open(options); + return true; } catch (e) { logSafe("❌ Error opening Razorpay: $e", level: LogLevel.error); _showError(context, "Error opening payment gateway."); + _cleanup(); isProcessing = false; notifyListeners(); + return false; } } @@ -81,15 +100,21 @@ class PaymentController with ChangeNotifier { /// ============================== void _handlePaymentSuccess(PaymentSuccessResponse response) async { logSafe("✅ Payment Success: ${response.paymentId}"); - isProcessing = true; notifyListeners(); - final result = await _paymentService.verifyPayment( - paymentId: response.paymentId!, - orderId: response.orderId!, - signature: response.signature!, - ); + Map? result; + try { + result = await _paymentService + .verifyPayment( + paymentId: response.paymentId!, + orderId: response.orderId!, + signature: response.signature!, + ) + .timeout(const Duration(seconds: 10)); + } catch (e) { + logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error); + } isProcessing = false; notifyListeners(); @@ -107,6 +132,8 @@ class PaymentController with ChangeNotifier { success: false, ); } + + _cleanup(); } void _handlePaymentError(PaymentFailureResponse response) { @@ -119,6 +146,8 @@ class PaymentController with ChangeNotifier { message: "Reason: ${response.message ?? 'Unknown error'}", success: false, ); + + _cleanup(); } void _handleExternalWallet(ExternalWalletResponse response) { @@ -126,7 +155,25 @@ class PaymentController with ChangeNotifier { } /// ============================== - /// HELPER DIALOGS / SNACKBARS + /// CLEANUP / DISPOSE + /// ============================== + void _cleanup() { + try { + _razorpay?.clear(); + } catch (_) {} + _razorpay = null; + } + + @override + void dispose() { + _cleanup(); + super.dispose(); + } + + void disposeController() => _cleanup(); + + /// ============================== + /// HELPER UI FUNCTIONS /// ============================== void _showDialog({ required String title, @@ -145,9 +192,14 @@ class PaymentController with ChangeNotifier { child: const Text("OK"), onPressed: () { Navigator.of(ctx).pop(); - if (success) { - Navigator.of(_context!).pop(true); // Return success - } + ScaffoldMessenger.of(_context!).showSnackBar( + SnackBar( + content: Text(success + ? "Payment verified successfully!" + : "Payment failed or could not be verified."), + backgroundColor: success ? Colors.green : Colors.red, + ), + ); }, ), ], diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index d0ffb2a..bf0b752 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -102,6 +102,6 @@ class ApiEndpoints { static const String getAssignedServices = "/Project/get/assigned/services"; // Payment Module API Endpoints - static const String createOrder = "/api/payment/create-order"; - static const String verifyPayment = "/api/payment/verify-payment"; + static const String createOrder = "/payment/create-order"; + static const String verifyPayment = "/payment/verify-payment"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 8cbd5a3..cf68692 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -75,10 +75,17 @@ class ApiService { return token; } - static Map _headers(String token) => { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $token', - }; + static Map _headers(String? token) { + final headers = { + 'Content-Type': 'application/json', + }; + + // 👇 Only add Authorization header if token is available + if (token != null && token.isNotEmpty) { + headers['Authorization'] = 'Bearer $token'; + } + return headers; + } static void _log(String message) { if (enableLogs) logSafe(message); @@ -123,32 +130,49 @@ class ApiService { String endpoint, { Map? queryParams, bool hasRetried = false, + bool requireAuth = true, }) async { - String? token = await _getToken(); - if (token == null) { - logSafe("Token is null. Forcing logout from GET request.", - level: LogLevel.error); - await LocalStorage.logout(); - return null; + // ✅ Allow public (no-login) API calls for subscription & payment + final isPublicEndpoint = + endpoint.contains("/subscription") || endpoint.contains("/payment"); + + String? token; + if (requireAuth && !isPublicEndpoint) { + token = await _getToken(); + if (token == null) { + logSafe("⛔ Token is null. Forcing logout from GET request.", + level: LogLevel.error); + await LocalStorage.logout(); + return null; + } } final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") .replace(queryParameters: queryParams); - logSafe("Initiating GET request", level: LogLevel.debug); + logSafe("🌐 Initiating GET request", level: LogLevel.debug); logSafe("URL: $uri", level: LogLevel.debug); logSafe("Query Parameters: ${queryParams ?? {}}", level: LogLevel.debug); logSafe("Headers: ${_headers(token)}", level: LogLevel.debug); try { final response = await http - .get(uri, headers: _headers(token)) + .get( + uri, + headers: (isPublicEndpoint || !requireAuth) + ? _headers(null) + : _headers(token), + ) .timeout(extendedTimeout); logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug); logSafe("Response Body: ${response.body}", level: LogLevel.debug); - if (response.statusCode == 401 && !hasRetried) { + // 🔒 Retry only for private endpoints + if (response.statusCode == 401 && + !hasRetried && + requireAuth && + !isPublicEndpoint) { logSafe("Unauthorized (401). Attempting token refresh...", level: LogLevel.warning); @@ -159,6 +183,7 @@ class ApiService { endpoint, queryParams: queryParams, hasRetried: true, + requireAuth: requireAuth, ); } @@ -176,33 +201,68 @@ class ApiService { static Future _postRequest( String endpoint, - dynamic body, { - Duration customTimeout = extendedTimeout, + dynamic data, { + // <-- changed from Map to dynamic + Map? queryParams, bool hasRetried = false, + bool requireAuth = true, + Duration? customTimeout, }) async { - String? token = await _getToken(); - if (token == null) return null; + // ✅ Allow public (no-login) API calls for subscription & payment + final isPublicEndpoint = + endpoint.contains("/subscription") || endpoint.contains("/payment"); - final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); - logSafe( - "POST $uri\nHeaders: ${_headers(token)}\nBody: $body", - ); + String? token = await _getToken(); + + if (token == null && requireAuth && !isPublicEndpoint) { + logSafe("⛔ Token missing for private POST: $endpoint", + level: LogLevel.error); + await LocalStorage.logout(); + return null; + } + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") + .replace(queryParameters: queryParams); + + logSafe("🌐 POST $uri", level: LogLevel.debug); + logSafe("Headers: ${_headers(token)}", level: LogLevel.debug); + logSafe("Body: $data", level: LogLevel.debug); try { final response = await http - .post(uri, headers: _headers(token), body: jsonEncode(body)) - .timeout(customTimeout); + .post( + uri, + headers: (isPublicEndpoint || !requireAuth) + ? _headers(null) + : _headers(token), + body: jsonEncode(data), // handles both Map and List + ) + .timeout(customTimeout ?? extendedTimeout); - if (response.statusCode == 401 && !hasRetried) { - logSafe("Unauthorized POST. Attempting token refresh..."); + logSafe("Response ${response.statusCode}: ${response.body}", + level: LogLevel.debug); + + // Retry token refresh for private routes only + if (response.statusCode == 401 && + !hasRetried && + requireAuth && + !isPublicEndpoint) { if (await AuthService.refreshToken()) { - return await _postRequest(endpoint, body, - customTimeout: customTimeout, hasRetried: true); + return await _postRequest( + endpoint, + data, + queryParams: queryParams, + hasRetried: true, + requireAuth: requireAuth, + customTimeout: customTimeout, + ); } + await LocalStorage.logout(); } + return response; } catch (e) { - logSafe("HTTP POST Exception: $e", level: LogLevel.error); + logSafe("❌ HTTP POST Exception: $e", level: LogLevel.error); return null; } } @@ -1963,11 +2023,12 @@ class ApiService { static Future?> getSubscriptionPlans( String frequency) async { try { - final endpoint = - "/api/market/list/subscription-plan?frequency=$frequency"; + final endpoint = "/market/list/subscription-plan?frequency=$frequency"; logSafe("Fetching subscription plans for frequency: $frequency"); - final response = await _getRequest(endpoint); + // 👇 Pass `requireAuth: false` to make this API public + final response = await _getRequest(endpoint, requireAuth: false); + if (response == null) { logSafe("Subscription plans request failed: null response", level: LogLevel.error); @@ -2135,11 +2196,18 @@ class ApiService { static Future?> createPaymentOrder(double amount) async { const endpoint = ApiEndpoints.createOrder; // endpoint for order creation try { - final response = await _postRequest(endpoint, {'amount': amount}); + // ✅ Allow this API call without requiring login/token + final response = await _postRequest( + endpoint, + {'amount': amount}, + requireAuth: false, // 👈 this is the key change + ); + if (response == null) return null; return _parseResponse(response, label: "Create Payment Order"); } catch (e) { - logSafe("Exception during createPaymentOrder: $e", level: LogLevel.error); + logSafe("❌ Exception during createPaymentOrder: $e", + level: LogLevel.error); return null; } } @@ -2152,11 +2220,15 @@ class ApiService { }) async { const endpoint = ApiEndpoints.verifyPayment; try { - final response = await _postRequest(endpoint, { - 'orderId': orderId, - 'paymentId': paymentId, - 'signature': signature, - }); + final response = await _postRequest( + endpoint, + { + 'orderId': orderId, + 'paymentId': paymentId, + 'signature': signature, + }, + requireAuth: false, + ); if (response == null) return null; return _parseResponse(response, label: "Verify Payment"); } catch (e) { diff --git a/lib/helpers/services/payment_service.dart b/lib/helpers/services/payment_service.dart index db7d2dd..d438128 100644 --- a/lib/helpers/services/payment_service.dart +++ b/lib/helpers/services/payment_service.dart @@ -1,4 +1,3 @@ -// payment_service.dart import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/app_logger.dart'; diff --git a/lib/routes.dart b/lib/routes.dart index 7070b0d..6dc6306 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -25,112 +25,120 @@ import 'package:marco/view/payment/payment_screen.dart'; import 'package:marco/view/subscriptions/subscriptions_screen.dart'; class AuthMiddleware extends GetMiddleware { -@override -RouteSettings? redirect(String? route) { -if (!AuthService.isLoggedIn) { -if (route != '/auth/login-option') { -return const RouteSettings(name: '/auth/login-option'); -} -} else if (!TenantService.isTenantSelected) { -if (route != '/select-tenant') { -return const RouteSettings(name: '/select-tenant'); -} -} -return null; -} + @override + RouteSettings? redirect(String? route) { + // Public routes (no auth required) + const publicRoutes = [ + '/auth/login-option', + '/auth/login', + '/subscription', // 👈 Allow this route without auth + '/payment', + '/select-tenant', + ]; + + // Skip auth checks for public routes + if (publicRoutes.contains(route)) return null; + + if (!AuthService.isLoggedIn) { + return const RouteSettings(name: '/auth/login-option'); + } + + if (!TenantService.isTenantSelected) { + return const RouteSettings(name: '/select-tenant'); + } + + return null; + } } getPageRoute() { -var routes = [ -GetPage( -name: '/', -page: () => DashboardScreen(), -middlewares: [AuthMiddleware()]), -GetPage( -name: '/dashboard', -page: () => DashboardScreen(), // or your actual home screen -middlewares: [AuthMiddleware()], -), -GetPage( -name: '/select-tenant', -page: () => const TenantSelectionScreen(), -middlewares: [AuthMiddleware()]), + var routes = [ + GetPage( + name: '/', + page: () => DashboardScreen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard', + page: () => DashboardScreen(), // or your actual home screen + middlewares: [AuthMiddleware()], + ), + GetPage( + name: '/select-tenant', + page: () => const TenantSelectionScreen(), + middlewares: [AuthMiddleware()]), // Dashboard -GetPage( -name: '/dashboard/attendance', -page: () => AttendanceScreen(), -middlewares: [AuthMiddleware()]), -GetPage( -name: '/dashboard', -page: () => DashboardScreen(), -middlewares: [AuthMiddleware()]), -GetPage( -name: '/dashboard/employees', -page: () => EmployeesScreen(), -middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/attendance', + page: () => AttendanceScreen(), + middlewares: [AuthMiddleware()]), + // GetPage( + // name: '/dashboard', + // page: () => DashboardScreen(), + // middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/employees', + page: () => EmployeesScreen(), + middlewares: [AuthMiddleware()]), // Daily Task Planning -GetPage( -name: '/dashboard/daily-task-Planning', -page: () => DailyTaskPlanningScreen(), -middlewares: [AuthMiddleware()]), -GetPage( -name: '/dashboard/daily-task-progress', -page: () => DailyProgressReportScreen(), -middlewares: [AuthMiddleware()]), -GetPage( -name: '/dashboard/directory-main-page', -page: () => DirectoryMainScreen(), -middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/daily-task-Planning', + page: () => DailyTaskPlanningScreen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/daily-task-progress', + page: () => DailyProgressReportScreen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/directory-main-page', + page: () => DirectoryMainScreen(), + middlewares: [AuthMiddleware()]), // Expense -GetPage( -name: '/dashboard/expense-main-page', -page: () => ExpenseMainScreen(), -middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/expense-main-page', + page: () => ExpenseMainScreen(), + middlewares: [AuthMiddleware()]), // Documents -GetPage( -name: '/dashboard/document-main-page', -page: () => UserDocumentsPage(), -middlewares: [AuthMiddleware()]), + GetPage( + name: '/dashboard/document-main-page', + page: () => UserDocumentsPage(), + middlewares: [AuthMiddleware()]), // Payment -GetPage( -name: '/dashboard/payment', -page: () => PaymentScreen(), -middlewares: [AuthMiddleware()]), -GetPage( -name: '/subscription', -page: () => SubscriptionScreen(), -middlewares: [AuthMiddleware()]), + GetPage(name: '/payment', page: () => PaymentScreen()), + GetPage( + name: '/subscription', + page: () => SubscriptionScreen(), + ), // Authentication -GetPage(name: '/auth/login', page: () => LoginScreen()), -GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()), -GetPage(name: '/auth/mpin', page: () => MPINScreen()), -GetPage(name: '/auth/mpin-auth', page: () => MPINAuthScreen()), -GetPage( -name: '/auth/register_account', -page: () => const RegisterAccountScreen()), -GetPage(name: '/auth/forgot_password', page: () => ForgotPasswordScreen()), -GetPage( -name: '/auth/reset_password', page: () => const ResetPasswordScreen()), + GetPage(name: '/auth/login', page: () => LoginScreen()), + GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()), + GetPage(name: '/auth/mpin', page: () => MPINScreen()), + GetPage(name: '/auth/mpin-auth', page: () => MPINAuthScreen()), + GetPage( + name: '/auth/register_account', + page: () => const RegisterAccountScreen()), + GetPage(name: '/auth/forgot_password', page: () => ForgotPasswordScreen()), + GetPage( + name: '/auth/reset_password', page: () => const ResetPasswordScreen()), // Error -GetPage( -name: '/error/coming_soon', -page: () => ComingSoonScreen(), -middlewares: [AuthMiddleware()]), -GetPage( -name: '/error/500', -page: () => Error500Screen(), -middlewares: [AuthMiddleware()]), -GetPage( -name: '/error/404', -page: () => Error404Screen(), -middlewares: [AuthMiddleware()]), -]; -return routes -.map((e) => GetPage( -name: e.name, -page: e.page, -middlewares: e.middlewares, -transition: Transition.noTransition)) -.toList(); -} \ No newline at end of file + GetPage( + name: '/error/coming_soon', + page: () => ComingSoonScreen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/error/500', + page: () => Error500Screen(), + middlewares: [AuthMiddleware()]), + GetPage( + name: '/error/404', + page: () => Error404Screen(), + middlewares: [AuthMiddleware()]), + ]; + return routes + .map((e) => GetPage( + name: e.name, + page: e.page, + middlewares: e.middlewares, + transition: Transition.noTransition)) + .toList(); +} diff --git a/lib/view/auth/login_option_screen.dart b/lib/view/auth/login_option_screen.dart index c3364ed..74f71a9 100644 --- a/lib/view/auth/login_option_screen.dart +++ b/lib/view/auth/login_option_screen.dart @@ -7,6 +7,8 @@ import 'package:marco/view/auth/otp_login_form.dart'; import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/view/auth/request_demo_bottom_sheet.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:get/get.dart'; + enum LoginOption { email, otp } @@ -133,6 +135,13 @@ class _WelcomeScreenState extends State option: LoginOption.otp, ), const SizedBox(height: 16), + _buildActionButton( + context, + label: "Subscribe", + icon: LucideIcons.bell, + option: null, + ), + const SizedBox(height: 16), _buildActionButton( context, label: "Request a Demo", @@ -237,6 +246,10 @@ class _WelcomeScreenState extends State shadowColor: Colors.black26, ), onPressed: () { + if (label == "Subscribe") { + Get.toNamed('/subscription'); // Navigate to Subscription screen + return; + } if (option == null) { OrganizationFormBottomSheet.show(context); } else { diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index 081d6dd..1b9b2cc 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -303,14 +303,14 @@ class _UserProfileBarState extends State onTap: _onProfileTap, ), SizedBox(height: spacingHeight), - _menuItemRow( - icon: LucideIcons.bell, - label: 'Subscribe', - onTap: _onSubscribeTap, - iconColor: Colors.redAccent, - textColor: Colors.redAccent, - ), - SizedBox(height: spacingHeight), + // _menuItemRow( + // icon: LucideIcons.bell, + // label: 'Subscribe', + // onTap: _onSubscribeTap, + // iconColor: Colors.redAccent, + // textColor: Colors.redAccent, + // ), + // SizedBox(height: spacingHeight), _menuItemRow( icon: LucideIcons.settings, label: 'Settings', @@ -380,9 +380,9 @@ class _UserProfileBarState extends State )); } - void _onSubscribeTap() { - Get.toNamed("/subscription"); - } + // void _onSubscribeTap() { + // Get.toNamed("/subscription"); + // } void _onMpinTap() { diff --git a/lib/view/payment/payment_screen.dart b/lib/view/payment/payment_screen.dart index 785df19..e0ef820 100644 --- a/lib/view/payment/payment_screen.dart +++ b/lib/view/payment/payment_screen.dart @@ -10,7 +10,7 @@ class PaymentScreen extends StatefulWidget { const PaymentScreen({ super.key, this.amount = 0.0, - this.description = "No description", + this.description = "No description", }); @override @@ -66,8 +66,8 @@ class _PaymentScreenState extends State { final controller = _controller!; return Scaffold( appBar: AppBar( - title: const Text("Payment"), - backgroundColor: Colors.blueAccent, + title: const Text("Payment", style: TextStyle(color: Colors.black),), + backgroundColor: Colors.white, ), body: Padding( padding: const EdgeInsets.all(16.0),