implementation of subscription module

This commit is contained in:
Manish Zure 2025-10-29 14:03:41 +05:30 committed by Manish
parent 8415b6549d
commit 2d95e92edb
9 changed files with 335 additions and 193 deletions

BIN
lib.zip

Binary file not shown.

View File

@ -4,27 +4,16 @@ import 'package:marco/helpers/services/payment_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
class PaymentController with ChangeNotifier { class PaymentController with ChangeNotifier {
final Razorpay _razorpay = Razorpay(); Razorpay? _razorpay;
final PaymentService _paymentService = PaymentService(); final PaymentService _paymentService = PaymentService();
bool isProcessing = false; bool isProcessing = false;
BuildContext? _context; // For showing dialogs/snackbars BuildContext? _context;
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 /// START PAYMENT (Safe init)
/// ============================== /// ==============================
Future<void> startPayment({ Future<bool> startPayment({
required double amount, required double amount,
required String description, required String description,
required BuildContext context, required BuildContext context,
@ -35,44 +24,74 @@ class PaymentController with ChangeNotifier {
logSafe("🟢 Starting payment for ₹$amount - $description"); logSafe("🟢 Starting payment for ₹$amount - $description");
// Call backend to create Razorpay order // Clear any old instance (prevents freeze on re-init)
final result = await _paymentService.createOrder(amount); _cleanup();
if (result == null) { // Create order first (no login dependency)
_showError(context, "Failed to connect to server."); Map<String, dynamic>? result;
isProcessing = false; try {
notifyListeners(); result = await _paymentService
return; .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']; if (result == null) {
final key = result['key']; _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) { if (orderId == null || key == null) {
_showError(context, "Invalid response from server."); _showError(context, "Invalid response from server.");
isProcessing = false; isProcessing = false;
notifyListeners(); 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, 'key': key,
'amount': (amount * 100).toInt(), // Razorpay takes amount in paise 'amount': (amount * 100).toInt(),
'name': 'Your Company Name', 'name': 'Your Company Name',
'description': description, 'description': description,
'order_id': orderId, 'order_id': orderId,
'theme': {'color': '#0D47A1'}, 'theme': {'color': '#0D47A1'},
'timeout': 120, // 2 minutes timeout 'timeout': 120, // seconds
}; };
try { try {
logSafe("🟠 Opening Razorpay with options: $options"); logSafe("🟠 Opening Razorpay checkout with options: $options");
_razorpay.open(options); _razorpay!.open(options);
return true;
} catch (e) { } catch (e) {
logSafe("❌ Error opening Razorpay: $e", level: LogLevel.error); logSafe("❌ Error opening Razorpay: $e", level: LogLevel.error);
_showError(context, "Error opening payment gateway."); _showError(context, "Error opening payment gateway.");
_cleanup();
isProcessing = false; isProcessing = false;
notifyListeners(); notifyListeners();
return false;
} }
} }
@ -81,15 +100,21 @@ class PaymentController with ChangeNotifier {
/// ============================== /// ==============================
void _handlePaymentSuccess(PaymentSuccessResponse response) async { void _handlePaymentSuccess(PaymentSuccessResponse response) async {
logSafe("✅ Payment Success: ${response.paymentId}"); logSafe("✅ Payment Success: ${response.paymentId}");
isProcessing = true; isProcessing = true;
notifyListeners(); notifyListeners();
final result = await _paymentService.verifyPayment( Map<String, dynamic>? result;
try {
result = await _paymentService
.verifyPayment(
paymentId: response.paymentId!, paymentId: response.paymentId!,
orderId: response.orderId!, orderId: response.orderId!,
signature: response.signature!, signature: response.signature!,
); )
.timeout(const Duration(seconds: 10));
} catch (e) {
logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error);
}
isProcessing = false; isProcessing = false;
notifyListeners(); notifyListeners();
@ -107,6 +132,8 @@ class PaymentController with ChangeNotifier {
success: false, success: false,
); );
} }
_cleanup();
} }
void _handlePaymentError(PaymentFailureResponse response) { void _handlePaymentError(PaymentFailureResponse response) {
@ -119,6 +146,8 @@ class PaymentController with ChangeNotifier {
message: "Reason: ${response.message ?? 'Unknown error'}", message: "Reason: ${response.message ?? 'Unknown error'}",
success: false, success: false,
); );
_cleanup();
} }
void _handleExternalWallet(ExternalWalletResponse response) { 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({ void _showDialog({
required String title, required String title,
@ -145,9 +192,14 @@ class PaymentController with ChangeNotifier {
child: const Text("OK"), child: const Text("OK"),
onPressed: () { onPressed: () {
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
if (success) { ScaffoldMessenger.of(_context!).showSnackBar(
Navigator.of(_context!).pop(true); // Return success SnackBar(
} content: Text(success
? "Payment verified successfully!"
: "Payment failed or could not be verified."),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}, },
), ),
], ],

View File

@ -102,6 +102,6 @@ class ApiEndpoints {
static const String getAssignedServices = "/Project/get/assigned/services"; static const String getAssignedServices = "/Project/get/assigned/services";
// Payment Module API Endpoints // Payment Module API Endpoints
static const String createOrder = "/api/payment/create-order"; static const String createOrder = "/payment/create-order";
static const String verifyPayment = "/api/payment/verify-payment"; static const String verifyPayment = "/payment/verify-payment";
} }

View File

@ -75,11 +75,18 @@ class ApiService {
return token; return token;
} }
static Map<String, String> _headers(String token) => { static Map<String, String> _headers(String? token) {
final headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
}; };
// 👇 Only add Authorization header if token is available
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
return headers;
}
static void _log(String message) { static void _log(String message) {
if (enableLogs) logSafe(message); if (enableLogs) logSafe(message);
} }
@ -123,32 +130,49 @@ class ApiService {
String endpoint, { String endpoint, {
Map<String, String>? queryParams, Map<String, String>? queryParams,
bool hasRetried = false, bool hasRetried = false,
bool requireAuth = true,
}) async { }) async {
String? token = await _getToken(); // 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) { if (token == null) {
logSafe("Token is null. Forcing logout from GET request.", logSafe("Token is null. Forcing logout from GET request.",
level: LogLevel.error); level: LogLevel.error);
await LocalStorage.logout(); await LocalStorage.logout();
return null; return null;
} }
}
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
.replace(queryParameters: queryParams); .replace(queryParameters: queryParams);
logSafe("Initiating GET request", level: LogLevel.debug); logSafe("🌐 Initiating GET request", level: LogLevel.debug);
logSafe("URL: $uri", level: LogLevel.debug); logSafe("URL: $uri", level: LogLevel.debug);
logSafe("Query Parameters: ${queryParams ?? {}}", level: LogLevel.debug); logSafe("Query Parameters: ${queryParams ?? {}}", level: LogLevel.debug);
logSafe("Headers: ${_headers(token)}", level: LogLevel.debug); logSafe("Headers: ${_headers(token)}", level: LogLevel.debug);
try { try {
final response = await http final response = await http
.get(uri, headers: _headers(token)) .get(
uri,
headers: (isPublicEndpoint || !requireAuth)
? _headers(null)
: _headers(token),
)
.timeout(extendedTimeout); .timeout(extendedTimeout);
logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug); logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug);
logSafe("Response Body: ${response.body}", 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...", logSafe("Unauthorized (401). Attempting token refresh...",
level: LogLevel.warning); level: LogLevel.warning);
@ -159,6 +183,7 @@ class ApiService {
endpoint, endpoint,
queryParams: queryParams, queryParams: queryParams,
hasRetried: true, hasRetried: true,
requireAuth: requireAuth,
); );
} }
@ -176,33 +201,68 @@ class ApiService {
static Future<http.Response?> _postRequest( static Future<http.Response?> _postRequest(
String endpoint, String endpoint,
dynamic body, { dynamic data, {
Duration customTimeout = extendedTimeout, // <-- changed from Map<String, dynamic> to dynamic
Map<String, String>? queryParams,
bool hasRetried = false, bool hasRetried = false,
bool requireAuth = true,
Duration? customTimeout,
}) async { }) async {
String? token = await _getToken(); // Allow public (no-login) API calls for subscription & payment
if (token == null) return null; final isPublicEndpoint =
endpoint.contains("/subscription") || endpoint.contains("/payment");
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); String? token = await _getToken();
logSafe(
"POST $uri\nHeaders: ${_headers(token)}\nBody: $body", 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 { try {
final response = await http final response = await http
.post(uri, headers: _headers(token), body: jsonEncode(body)) .post(
.timeout(customTimeout); 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("Response ${response.statusCode}: ${response.body}",
logSafe("Unauthorized POST. Attempting token refresh..."); level: LogLevel.debug);
// Retry token refresh for private routes only
if (response.statusCode == 401 &&
!hasRetried &&
requireAuth &&
!isPublicEndpoint) {
if (await AuthService.refreshToken()) { if (await AuthService.refreshToken()) {
return await _postRequest(endpoint, body, return await _postRequest(
customTimeout: customTimeout, hasRetried: true); endpoint,
data,
queryParams: queryParams,
hasRetried: true,
requireAuth: requireAuth,
customTimeout: customTimeout,
);
} }
await LocalStorage.logout();
} }
return response; return response;
} catch (e) { } catch (e) {
logSafe("HTTP POST Exception: $e", level: LogLevel.error); logSafe("HTTP POST Exception: $e", level: LogLevel.error);
return null; return null;
} }
} }
@ -1963,11 +2023,12 @@ class ApiService {
static Future<Map<String, dynamic>?> getSubscriptionPlans( static Future<Map<String, dynamic>?> getSubscriptionPlans(
String frequency) async { String frequency) async {
try { try {
final endpoint = final endpoint = "/market/list/subscription-plan?frequency=$frequency";
"/api/market/list/subscription-plan?frequency=$frequency";
logSafe("Fetching subscription plans for 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) { if (response == null) {
logSafe("Subscription plans request failed: null response", logSafe("Subscription plans request failed: null response",
level: LogLevel.error); level: LogLevel.error);
@ -2135,11 +2196,18 @@ class ApiService {
static Future<Map<String, dynamic>?> createPaymentOrder(double amount) async { static Future<Map<String, dynamic>?> createPaymentOrder(double amount) async {
const endpoint = ApiEndpoints.createOrder; // endpoint for order creation const endpoint = ApiEndpoints.createOrder; // endpoint for order creation
try { 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; if (response == null) return null;
return _parseResponse(response, label: "Create Payment Order"); return _parseResponse(response, label: "Create Payment Order");
} catch (e) { } catch (e) {
logSafe("Exception during createPaymentOrder: $e", level: LogLevel.error); logSafe("❌ Exception during createPaymentOrder: $e",
level: LogLevel.error);
return null; return null;
} }
} }
@ -2152,11 +2220,15 @@ class ApiService {
}) async { }) async {
const endpoint = ApiEndpoints.verifyPayment; const endpoint = ApiEndpoints.verifyPayment;
try { try {
final response = await _postRequest(endpoint, { final response = await _postRequest(
endpoint,
{
'orderId': orderId, 'orderId': orderId,
'paymentId': paymentId, 'paymentId': paymentId,
'signature': signature, 'signature': signature,
}); },
requireAuth: false,
);
if (response == null) return null; if (response == null) return null;
return _parseResponse(response, label: "Verify Payment"); return _parseResponse(response, label: "Verify Payment");
} catch (e) { } catch (e) {

View File

@ -1,4 +1,3 @@
// payment_service.dart
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';

View File

@ -27,15 +27,26 @@ import 'package:marco/view/subscriptions/subscriptions_screen.dart';
class AuthMiddleware extends GetMiddleware { class AuthMiddleware extends GetMiddleware {
@override @override
RouteSettings? redirect(String? route) { 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) { 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') { if (!TenantService.isTenantSelected) {
return const RouteSettings(name: '/select-tenant'); return const RouteSettings(name: '/select-tenant');
} }
}
return null; return null;
} }
} }
@ -61,10 +72,10 @@ GetPage(
name: '/dashboard/attendance', name: '/dashboard/attendance',
page: () => AttendanceScreen(), page: () => AttendanceScreen(),
middlewares: [AuthMiddleware()]), middlewares: [AuthMiddleware()]),
GetPage( // GetPage(
name: '/dashboard', // name: '/dashboard',
page: () => DashboardScreen(), // page: () => DashboardScreen(),
middlewares: [AuthMiddleware()]), // middlewares: [AuthMiddleware()]),
GetPage( GetPage(
name: '/dashboard/employees', name: '/dashboard/employees',
page: () => EmployeesScreen(), page: () => EmployeesScreen(),
@ -93,14 +104,11 @@ name: '/dashboard/document-main-page',
page: () => UserDocumentsPage(), page: () => UserDocumentsPage(),
middlewares: [AuthMiddleware()]), middlewares: [AuthMiddleware()]),
// Payment // Payment
GetPage( GetPage(name: '/payment', page: () => PaymentScreen()),
name: '/dashboard/payment',
page: () => PaymentScreen(),
middlewares: [AuthMiddleware()]),
GetPage( GetPage(
name: '/subscription', name: '/subscription',
page: () => SubscriptionScreen(), page: () => SubscriptionScreen(),
middlewares: [AuthMiddleware()]), ),
// Authentication // Authentication
GetPage(name: '/auth/login', page: () => LoginScreen()), GetPage(name: '/auth/login', page: () => LoginScreen()),
GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()), GetPage(name: '/auth/login-option', page: () => LoginOptionScreen()),

View File

@ -135,6 +135,13 @@ class _WelcomeScreenState extends State<WelcomeScreen>
option: LoginOption.otp, option: LoginOption.otp,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildActionButton(
context,
label: "Subscribe",
icon: LucideIcons.bell,
option: null,
),
const SizedBox(height: 16),
_buildActionButton( _buildActionButton(
context, context,
label: "Request a Demo", label: "Request a Demo",
@ -243,6 +250,10 @@ class _WelcomeScreenState extends State<WelcomeScreen>
shadowColor: Colors.black26, shadowColor: Colors.black26,
), ),
onPressed: () { onPressed: () {
if (label == "Subscribe") {
Get.toNamed('/subscription'); // Navigate to Subscription screen
return;
}
if (option == null) { if (option == null) {
OrganizationFormBottomSheet.show(context); OrganizationFormBottomSheet.show(context);
} else { } else {

View File

@ -316,14 +316,14 @@ class _UserProfileBarState extends State<UserProfileBar>
onTap: _onProfileTap, onTap: _onProfileTap,
), ),
SizedBox(height: spacingHeight), SizedBox(height: spacingHeight),
_menuItemRow( // _menuItemRow(
icon: LucideIcons.bell, // icon: LucideIcons.bell,
label: 'Subscribe', // label: 'Subscribe',
onTap: _onSubscribeTap, // onTap: _onSubscribeTap,
iconColor: Colors.redAccent, // iconColor: Colors.redAccent,
textColor: Colors.redAccent, // textColor: Colors.redAccent,
), // ),
SizedBox(height: spacingHeight), // SizedBox(height: spacingHeight),
_menuItemRow( _menuItemRow(
icon: LucideIcons.settings, icon: LucideIcons.settings,
label: 'Settings', label: 'Settings',
@ -393,9 +393,9 @@ class _UserProfileBarState extends State<UserProfileBar>
)); ));
} }
void _onSubscribeTap() { // void _onSubscribeTap() {
Get.toNamed("/subscription"); // Get.toNamed("/subscription");
} // }
void _onMpinTap() { void _onMpinTap() {

View File

@ -66,8 +66,8 @@ class _PaymentScreenState extends State<PaymentScreen> {
final controller = _controller!; final controller = _controller!;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Payment"), title: const Text("Payment", style: TextStyle(color: Colors.black),),
backgroundColor: Colors.blueAccent, backgroundColor: Colors.white,
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),