feature/payment #77
163
lib/controller/payment/payment_controller.dart
Normal file
163
lib/controller/payment/payment_controller.dart
Normal file
@ -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<void> 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/controller/subscriptions/subscription_controller.dart
Normal file
38
lib/controller/subscriptions/subscription_controller.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
|
||||
class SubscriptionController extends GetxController {
|
||||
var plans = <Map<String, dynamic>>[].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<void> 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<Map<String, dynamic>>.from(response['data']);
|
||||
} else {
|
||||
plans.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error fetching plans: $e");
|
||||
plans.clear();
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
|
||||
@ -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<Map<String, dynamic>?> 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<String, dynamic>) {
|
||||
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<List<dynamic>?> getProjects() async =>
|
||||
@ -2085,6 +2131,40 @@ class ApiService {
|
||||
return "${employeeId}_${dateStr}_$imageNumber.jpg";
|
||||
}
|
||||
|
||||
/// Create a payment order
|
||||
static Future<Map<String, dynamic>?> 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<Map<String, dynamic>?> 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.
|
||||
|
||||
37
lib/helpers/services/payment_service.dart
Normal file
37
lib/helpers/services/payment_service.dart
Normal file
@ -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<Map<String, dynamic>?> 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<Map<String, dynamic>?> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
136
lib/routes.dart
136
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<GetPage> 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();
|
||||
}
|
||||
|
||||
@ -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<UserProfileBar>
|
||||
}
|
||||
|
||||
/// 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<String>(
|
||||
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<String>(
|
||||
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();
|
||||
|
||||
80
lib/view/payment/payment_screen.dart
Normal file
80
lib/view/payment/payment_screen.dart
Normal file
@ -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<String, dynamic>;
|
||||
final double finalAmount = args['amount'] ?? amount;
|
||||
final String finalDescription = args['description'] ?? description;
|
||||
|
||||
final paymentController = Provider.of<PaymentController>(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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
196
lib/view/subscriptions/subscriptions_screen.dart
Normal file
196
lib/view/subscriptions/subscriptions_screen.dart
Normal file
@ -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<String> _extractFeatures(Map<String, dynamic> plan) {
|
||||
final features = <String>[];
|
||||
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);
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user