diff --git a/lib.zip b/lib.zip new file mode 100644 index 0000000..d46d707 Binary files /dev/null and b/lib.zip differ diff --git a/lib/controller/payment/payment_controller.dart b/lib/controller/payment/payment_controller.dart new file mode 100644 index 0000000..29547cd --- /dev/null +++ b/lib/controller/payment/payment_controller.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:razorpay_flutter/razorpay_flutter.dart'; +import 'package:marco/helpers/services/payment_service.dart'; +import 'package:marco/helpers/services/app_logger.dart'; + +class PaymentController with ChangeNotifier { + final 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(); + } + + /// ============================== + /// START PAYMENT + /// ============================== + Future startPayment({ + required double amount, + required String description, + required BuildContext context, + }) async { + _context = context; + isProcessing = true; + notifyListeners(); + + logSafe("đŸŸĸ Starting payment for ₹$amount - $description"); + + // Call backend to create Razorpay order + final result = await _paymentService.createOrder(amount); + + if (result == null) { + _showError(context, "Failed to connect to server."); + isProcessing = false; + notifyListeners(); + return; + } + + final orderId = result['orderId']; + final key = result['key']; + + if (orderId == null || key == null) { + _showError(context, "Invalid response from server."); + isProcessing = false; + notifyListeners(); + return; + } + + var options = { + 'key': key, + 'amount': (amount * 100).toInt(), // Razorpay takes amount in paise + 'name': 'Your Company Name', + 'description': description, + 'order_id': orderId, + 'theme': {'color': '#0D47A1'}, + 'timeout': 120, // 2 minutes timeout + }; + + try { + logSafe("🟠 Opening Razorpay with options: $options"); + _razorpay.open(options); + } catch (e) { + logSafe("❌ Error opening Razorpay: $e", level: LogLevel.error); + _showError(context, "Error opening payment gateway."); + isProcessing = false; + notifyListeners(); + } + } + + /// ============================== + /// EVENT HANDLERS + /// ============================== + 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!, + ); + + isProcessing = false; + notifyListeners(); + + if (result != null && result['verified'] == true) { + _showDialog( + title: "Payment Successful 🎉", + message: "Your payment was verified successfully.", + success: true, + ); + } else { + _showDialog( + title: "Verification Failed ❌", + message: "Payment completed but could not be verified.", + success: false, + ); + } + } + + void _handlePaymentError(PaymentFailureResponse response) { + logSafe("❌ Payment Failed: ${response.message}"); + isProcessing = false; + notifyListeners(); + + _showDialog( + title: "Payment Failed ❌", + message: "Reason: ${response.message ?? 'Unknown error'}", + success: false, + ); + } + + void _handleExternalWallet(ExternalWalletResponse response) { + logSafe("â„šī¸ External Wallet Used: ${response.walletName}"); + } + + /// ============================== + /// HELPER DIALOGS / SNACKBARS + /// ============================== + void _showDialog({ + required String title, + required String message, + required bool success, + }) { + if (_context == null) return; + + showDialog( + context: _context!, + builder: (ctx) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + child: const Text("OK"), + onPressed: () { + Navigator.of(ctx).pop(); + if (success) { + Navigator.of(_context!).pop(true); // Return success + } + }, + ), + ], + ), + ); + } + + void _showError(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message), backgroundColor: Colors.red), + ); + } +} diff --git a/lib/controller/subscriptions/subscription_controller.dart b/lib/controller/subscriptions/subscription_controller.dart new file mode 100644 index 0000000..9a65f61 --- /dev/null +++ b/lib/controller/subscriptions/subscription_controller.dart @@ -0,0 +1,38 @@ +import 'package:get/get.dart'; +import 'package:marco/helpers/services/api_service.dart'; + +class SubscriptionController extends GetxController { + var plans = >[].obs; + var isLoading = true.obs; + + // Frequency tabs + final frequencies = ['monthly', 'quarterly', 'halfyearly', 'yearly']; + var selectedFrequency = 'monthly'.obs; + + @override + void onInit() { + super.onInit(); + fetchPlans(selectedFrequency.value); + } + + Future fetchPlans(String frequency) async { + try { + isLoading.value = true; + selectedFrequency.value = frequency; + + final response = await ApiService.getSubscriptionPlans(frequency); + if (response != null && + response['success'] == true && + response['data'] != null) { + plans.value = List>.from(response['data']); + } else { + plans.clear(); + } + } catch (e) { + print("Error fetching plans: $e"); + plans.clear(); + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 321312e..e5b417d 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -100,4 +100,8 @@ class ApiEndpoints { static const getAllOrganizations = "/organization/list"; 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"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 8b20011..8cbd5a3 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -1824,7 +1824,7 @@ class ApiService { _log("Deleting directory contact at $uri"); final response = await _deleteRequest( - "$endpoint?active=false", + "$endpoint?active=false", ); if (response != null && response.statusCode == 200) { @@ -1957,6 +1957,52 @@ class ApiService { ? _parseResponseForAllData(res, label: 'Contact Bucket List') : null); + /// ============================== + /// SUBSCRIPTION API + /// ============================== + static Future?> getSubscriptionPlans( + String frequency) async { + try { + final endpoint = + "/api/market/list/subscription-plan?frequency=$frequency"; + logSafe("Fetching subscription plans for frequency: $frequency"); + + final response = await _getRequest(endpoint); + if (response == null) { + logSafe("Subscription plans request failed: null response", + level: LogLevel.error); + return null; + } + + final body = response.body.trim(); + if (body.isEmpty) { + logSafe("Subscription plans response body is empty", + level: LogLevel.warning); + return null; + } + + final jsonResponse = jsonDecode(body); + if (jsonResponse is Map) { + if (jsonResponse['success'] == true) { + logSafe("Subscription plans fetched successfully"); + return jsonResponse; // Return full JSON, PaymentController will handle parsing + } else { + logSafe( + "Failed to fetch subscription plans: ${jsonResponse['message'] ?? 'Unknown error'}", + level: LogLevel.warning); + } + } else { + logSafe("Unexpected subscription response format: $jsonResponse", + level: LogLevel.error); + } + } catch (e, stack) { + logSafe("❌ Exception during getSubscriptionPlans: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + return null; + } + // === Attendance APIs === static Future?> getProjects() async => @@ -2085,6 +2131,40 @@ class ApiService { return "${employeeId}_${dateStr}_$imageNumber.jpg"; } + /// Create a payment order + static Future?> createPaymentOrder(double amount) async { + const endpoint = ApiEndpoints.createOrder; // endpoint for order creation + try { + final response = await _postRequest(endpoint, {'amount': amount}); + if (response == null) return null; + return _parseResponse(response, label: "Create Payment Order"); + } catch (e) { + logSafe("Exception during createPaymentOrder: $e", level: LogLevel.error); + return null; + } + } + + /// Verify a payment + static Future?> verifyPayment({ + required String orderId, + required String paymentId, + required String signature, + }) async { + const endpoint = ApiEndpoints.verifyPayment; + try { + final response = await _postRequest(endpoint, { + 'orderId': orderId, + 'paymentId': paymentId, + 'signature': signature, + }); + if (response == null) return null; + return _parseResponse(response, label: "Verify Payment"); + } catch (e) { + logSafe("Exception during verifyPayment: $e", level: LogLevel.error); + return null; + } + } + // === Employee APIs === /// Search employees by first name and last name only (not middle name) /// Returns a list of up to 10 employee records matching the search string. diff --git a/lib/helpers/services/payment_service.dart b/lib/helpers/services/payment_service.dart new file mode 100644 index 0000000..db7d2dd --- /dev/null +++ b/lib/helpers/services/payment_service.dart @@ -0,0 +1,37 @@ +// payment_service.dart +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/services/app_logger.dart'; + +class PaymentService { + /// Create a Razorpay order on backend + Future?> createOrder(double amount) async { + try { + logSafe("đŸŸĸ Calling createPaymentOrder API with amount: ₹$amount"); + final response = await ApiService.createPaymentOrder(amount); + return response; + } catch (e) { + logSafe("❌ Error in createOrder: $e", level: LogLevel.error); + return null; + } + } + + /// Verify Razorpay payment signature + Future?> verifyPayment({ + required String orderId, + required String paymentId, + required String signature, + }) async { + try { + logSafe("đŸŸĸ Calling verifyPayment API: orderId=$orderId, paymentId=$paymentId"); + final response = await ApiService.verifyPayment( + orderId: orderId, + paymentId: paymentId, + signature: signature, + ); + return response; + } catch (e) { + logSafe("❌ Error in verifyPayment: $e", level: LogLevel.error); + return null; + } + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 37a166e..b2d37a1 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -2,124 +2,76 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/tenant_service.dart'; -import 'package:marco/view/auth/forgot_password_screen.dart'; + +// Screens +import 'package:marco/view/auth/login_option_screen.dart'; import 'package:marco/view/auth/login_screen.dart'; import 'package:marco/view/auth/register_account_screen.dart'; import 'package:marco/view/auth/reset_password_screen.dart'; -import 'package:marco/view/error_pages/coming_soon_screen.dart'; -import 'package:marco/view/error_pages/error_404_screen.dart'; -import 'package:marco/view/error_pages/error_500_screen.dart'; +import 'package:marco/view/auth/forgot_password_screen.dart'; +import 'package:marco/view/auth/mpin_screen.dart'; +import 'package:marco/view/auth/mpin_auth_screen.dart'; import 'package:marco/view/dashboard/dashboard_screen.dart'; +import 'package:marco/view/tenant/tenant_selection_screen.dart'; import 'package:marco/view/Attendence/attendance_screen.dart'; import 'package:marco/view/taskPlanning/daily_task_planning.dart'; import 'package:marco/view/taskPlanning/daily_progress_report.dart'; import 'package:marco/view/employees/employees_screen.dart'; -import 'package:marco/view/auth/login_option_screen.dart'; -import 'package:marco/view/auth/mpin_screen.dart'; -import 'package:marco/view/auth/mpin_auth_screen.dart'; -import 'package:marco/view/directory/directory_main_screen.dart'; import 'package:marco/view/expense/expense_screen.dart'; import 'package:marco/view/document/user_document_screen.dart'; -import 'package:marco/view/tenant/tenant_selection_screen.dart'; +import 'package:marco/view/directory/directory_main_screen.dart'; +import 'package:marco/view/error_pages/error_404_screen.dart'; +import 'package:marco/view/error_pages/error_500_screen.dart'; +import 'package:marco/view/error_pages/coming_soon_screen.dart'; +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'); - } + return const RouteSettings(name: '/auth/login-option'); } else if (!TenantService.isTenantSelected) { - if (route != '/select-tenant') { - return const RouteSettings(name: '/select-tenant'); - } + 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()]), +List getPageRoute() { + return [ + GetPage(name: '/', page: () => DashboardScreen(), middlewares: [AuthMiddleware()]), + GetPage(name: '/dashboard', page: () => DashboardScreen(), 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()]), - // 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()]), - // Expense - GetPage( - name: '/dashboard/expense-main-page', - page: () => ExpenseMainScreen(), - middlewares: [AuthMiddleware()]), - // Documents - GetPage( - name: '/dashboard/document-main-page', - page: () => UserDocumentsPage(), - middlewares: [AuthMiddleware()]), - // Authentication + // Tenant + GetPage(name: '/select-tenant', page: () => TenantSelectionScreen(), middlewares: [AuthMiddleware()]), + + // Modules + GetPage(name: '/dashboard/attendance', page: () => AttendanceScreen(), middlewares: [AuthMiddleware()]), + GetPage(name: '/dashboard/employees', page: () => EmployeesScreen(), 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()]), + GetPage(name: '/dashboard/expense-main-page', page: () => ExpenseMainScreen(), middlewares: [AuthMiddleware()]), + GetPage(name: '/dashboard/document-main-page', page: () => UserDocumentsPage(), middlewares: [AuthMiddleware()]), + + // Auth GetPage(name: '/auth/login', page: () => LoginScreen()), GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()), + GetPage(name: '/auth/register_account', page: () => RegisterAccountScreen()), 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()]), + GetPage(name: '/auth/reset_password', page: () => ResetPasswordScreen()), + + // Payment + GetPage(name: '/dashboard/payment', page: () => PaymentScreen(), middlewares: [AuthMiddleware()]), + GetPage(name: '/subscription', page: () => SubscriptionScreen(), middlewares: [AuthMiddleware()]), + + // Error Pages + GetPage(name: '/error/404', page: () => Error404Screen(), middlewares: [AuthMiddleware()]), + GetPage(name: '/error/500', page: () => Error500Screen(), middlewares: [AuthMiddleware()]), + GetPage(name: '/error/coming_soon', page: () => ComingSoonScreen(), 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/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index 3ff2cb4..aa7650f 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -15,9 +15,6 @@ import 'package:marco/view/tenant/tenant_selection_screen.dart'; import 'package:marco/controller/tenant/tenant_switch_controller.dart'; import 'package:marco/view/appearance_screen.dart'; - - - class UserProfileBar extends StatefulWidget { final bool isCondensed; const UserProfileBar({Key? key, this.isCondensed = false}) : super(key: key); @@ -118,101 +115,98 @@ class _UserProfileBarState extends State } /// Row widget to switch tenant with popup menu (button only) - /// Row widget to switch tenant with popup menu (button only) -Widget _switchTenantRow() { - // Use the dedicated switch controller - final TenantSwitchController tenantSwitchController = - Get.put(TenantSwitchController()); + Widget _switchTenantRow() { + final TenantSwitchController tenantSwitchController = + Get.put(TenantSwitchController()); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Obx(() { - if (tenantSwitchController.isLoading.value) { - return _loadingTenantContainer(); - } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Obx(() { + if (tenantSwitchController.isLoading.value) { + return _loadingTenantContainer(); + } - final tenants = tenantSwitchController.tenants; - if (tenants.isEmpty) return _noTenantContainer(); + final tenants = tenantSwitchController.tenants; + if (tenants.isEmpty) return _noTenantContainer(); - final selectedTenant = TenantService.currentTenant; + final selectedTenant = TenantService.currentTenant; - // Sort tenants: selected tenant first - final sortedTenants = List.of(tenants); - if (selectedTenant != null) { - sortedTenants.sort((a, b) { - if (a.id == selectedTenant.id) return -1; - if (b.id == selectedTenant.id) return 1; - return 0; - }); - } + // Sort tenants: selected tenant first + final sortedTenants = List.of(tenants); + if (selectedTenant != null) { + sortedTenants.sort((a, b) { + if (a.id == selectedTenant.id) return -1; + if (b.id == selectedTenant.id) return 1; + return 0; + }); + } - return PopupMenuButton( - onSelected: (tenantId) => - tenantSwitchController.switchTenant(tenantId), - itemBuilder: (_) => sortedTenants.map((tenant) { - return PopupMenuItem( - value: tenant.id, - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Container( - width: 20, - height: 20, - color: Colors.grey.shade200, - child: TenantLogo(logoImage: tenant.logoImage), + return PopupMenuButton( + onSelected: (tenantId) => + tenantSwitchController.switchTenant(tenantId), + itemBuilder: (_) => sortedTenants.map((tenant) { + return PopupMenuItem( + value: tenant.id, + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + width: 20, + height: 20, + color: Colors.grey.shade200, + child: TenantLogo(logoImage: tenant.logoImage), + ), ), - ), - const SizedBox(width: 10), + const SizedBox(width: 10), + Expanded( + child: Text( + tenant.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: tenant.id == selectedTenant?.id + ? FontWeight.bold + : FontWeight.w600, + color: tenant.id == selectedTenant?.id + ? Colors.blueAccent + : Colors.black87, + ), + ), + ), + if (tenant.id == selectedTenant?.id) + const Icon(Icons.check_circle, + color: Colors.blueAccent, size: 18), + ], + ), + ); + }).toList(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon(Icons.swap_horiz, color: Colors.blue.shade600), Expanded( - child: Text( - tenant.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: tenant.id == selectedTenant?.id - ? FontWeight.bold - : FontWeight.w600, - color: tenant.id == selectedTenant?.id - ? Colors.blueAccent - : Colors.black87, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + "Switch Organization", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.blue, fontWeight: FontWeight.bold), ), ), ), - if (tenant.id == selectedTenant?.id) - const Icon(Icons.check_circle, - color: Colors.blueAccent, size: 18), + Icon(Icons.arrow_drop_down, color: Colors.blue.shade600), ], ), - ); - }).toList(), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Icon(Icons.swap_horiz, color: Colors.blue.shade600), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - "Switch Organization", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.blue, fontWeight: FontWeight.bold), - ), - ), - ), - Icon(Icons.arrow_drop_down, color: Colors.blue.shade600), - ], ), - ), - ); - }), - ); -} - + ); + }), + ); + } Widget _loadingTenantContainer() => Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), @@ -309,6 +303,14 @@ Widget _switchTenantRow() { onTap: _onProfileTap, ), SizedBox(height: spacingHeight), + _menuItemRow( + icon: LucideIcons.bell, + label: 'Subscribe', + onTap: _onSubscribeTap, + iconColor: Colors.green.shade600, + textColor: Colors.green.shade800, + ), + SizedBox(height: spacingHeight), _menuItemRow( icon: LucideIcons.settings, label: 'Settings', @@ -316,9 +318,7 @@ Widget _switchTenantRow() { Get.to(() => const AppearancePage()); }, ), - SizedBox(height: spacingHeight), - _menuItemRow( icon: LucideIcons.badge_alert, label: 'Support', @@ -380,6 +380,11 @@ Widget _switchTenantRow() { )); } + void _onSubscribeTap() { + Get.toNamed("/subscription"); + } + + void _onMpinTap() { final controller = Get.put(MPINController()); if (hasMpin) controller.setChangeMpinMode(); diff --git a/lib/view/payment/payment_screen.dart b/lib/view/payment/payment_screen.dart new file mode 100644 index 0000000..bd45e43 --- /dev/null +++ b/lib/view/payment/payment_screen.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:marco/controller/payment/payment_controller.dart'; +import 'package:get/get.dart'; + +class PaymentScreen extends StatelessWidget { + final double amount; + final String description; + + const PaymentScreen({ + super.key, + this.amount = 0.0, + this.description = "No description", + }); + + @override + Widget build(BuildContext context) { + final args = (Get.arguments ?? {}) as Map; + final double finalAmount = args['amount'] ?? amount; + final String finalDescription = args['description'] ?? description; + + final paymentController = Provider.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text("Payment"), + backgroundColor: Colors.blueAccent, + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Description: $finalDescription", + style: const TextStyle(fontSize: 18), + ), + const SizedBox(height: 8), + Text( + "Amount: ₹${finalAmount.toStringAsFixed(2)}", + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 40), + + // ✅ Show loader when payment processing + Center( + child: paymentController.isProcessing + ? const CircularProgressIndicator() + : ElevatedButton( + onPressed: () async { + await paymentController.startPayment( + amount: finalAmount, + description: finalDescription, + context: context, + ); + }, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + backgroundColor: Colors.green, + ), + child: const Text( + "Pay Now", + style: TextStyle(fontSize: 18, color: Colors.white), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/subscriptions/subscriptions_screen.dart b/lib/view/subscriptions/subscriptions_screen.dart new file mode 100644 index 0000000..b5566d7 --- /dev/null +++ b/lib/view/subscriptions/subscriptions_screen.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/subscriptions/subscription_controller.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +class SubscriptionScreen extends StatelessWidget { + final SubscriptionController controller = Get.put(SubscriptionController()); + + SubscriptionScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Subscription Plans'), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Get.back(), + ), + ), + body: SafeArea( + child: Column( + children: [ + _buildFrequencyTabs(), + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + if (controller.plans.isEmpty) { + return const Center(child: Text("No Plans Available")); + } + + return RefreshIndicator( + onRefresh: () => controller + .fetchPlans(controller.selectedFrequency.value), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + children: controller.plans.map((plan) { + final features = _extractFeatures(plan); + final currency = + plan['currency']?['symbol'] ?? '₹'; + return Card( + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 3, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium( + plan['planName'] ?? 'Plan', + fontWeight: 700, + ), + MySpacing.height(4), + MyText.bodySmall( + plan['description'] ?? '', + color: Colors.grey[700], + ), + MySpacing.height(8), + Text( + "$currency${plan['price'] ?? 0}", + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + MySpacing.height(8), + Text( + "Trial: ${plan['trialDays'] ?? 0} days", + style: const TextStyle(color: Colors.grey), + ), + MySpacing.height(12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: features + .map((f) => Row( + children: [ + const Icon(Icons.check, + size: 16, + color: Colors.green), + const SizedBox(width: 6), + Expanded( + child: Text( + f, + style: const TextStyle( + fontSize: 13), + )), + ], + )) + .toList(), + ), + MySpacing.height(16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Get.toNamed('/payment', arguments: { + 'amount': plan['price'] ?? 0, + 'description': + plan['planName'] ?? 'Subscription', + 'planId': plan['id'] ?? '', + }); + }, + child: MyText.bodyMedium( + 'Subscribe for $currency${plan['price']}', + color: Colors.white, + ), + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ); + }), + ), + ], + ), + ), + ); + } + + // --- Frequency Tab Bar --- + Widget _buildFrequencyTabs() { + return Obx(() { + return Container( + color: Colors.blue[50], + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: controller.frequencies.map((freq) { + final isSelected = + controller.selectedFrequency.value == freq; + return GestureDetector( + onTap: () => controller.fetchPlans(freq), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? Colors.blue : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? Colors.blue : Colors.grey.shade300), + ), + child: Text( + _capitalize(freq), + style: TextStyle( + color: isSelected ? Colors.white : Colors.black87, + fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, + ), + ), + ), + ); + }).toList(), + ), + ); + }); + } + + // --- Helper to extract feature names dynamically --- + List _extractFeatures(Map plan) { + final features = []; + try { + final modules = plan['features']?['modules'] ?? {}; + modules.forEach((key, value) { + if (value is Map && value['enabled'] == true) { + features.add(value['name'] ?? key); + } + }); + + final supports = plan['features']?['supports'] ?? {}; + supports.forEach((k, v) { + if (v == true) { + features.add(k.toString().replaceAll(RegExp(r'([a-z])([A-Z])'), r'\1 \2')); + } + }); + } catch (e) { + print("Feature parse error: $e"); + } + return features; + } + + String _capitalize(String str) => + str.isEmpty ? '' : str[0].toUpperCase() + str.substring(1); +} diff --git a/pubspec.yaml b/pubspec.yaml index 31e2374..113eb77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,8 @@ dependencies: flutter_localizations: sdk: flutter + razorpay_flutter: 1.4.0 + # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -149,3 +151,5 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package + +