subscription

This commit is contained in:
Manish 2025-10-30 16:02:14 +05:30
parent f70138238b
commit 4a1bd85435
5 changed files with 101 additions and 62 deletions

View File

@ -2,13 +2,14 @@ import 'package:flutter/material.dart';
import 'package:razorpay_flutter/razorpay_flutter.dart'; import 'package:razorpay_flutter/razorpay_flutter.dart';
import 'package:marco/helpers/services/payment_service.dart'; import 'package:marco/helpers/services/payment_service.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:get/get.dart';
class PaymentController with ChangeNotifier { class PaymentController with ChangeNotifier {
Razorpay? _razorpay; Razorpay? _razorpay;
final PaymentService _paymentService = PaymentService(); final PaymentService _paymentService = PaymentService();
bool isProcessing = false; bool isProcessing = false;
BuildContext? _context; //BuildContext? _context;
/// ============================== /// ==============================
/// START PAYMENT (Safe init) /// START PAYMENT (Safe init)
@ -18,7 +19,7 @@ class PaymentController with ChangeNotifier {
required String description, required String description,
required BuildContext context, required BuildContext context,
}) async { }) async {
_context = context; //_context = context;
isProcessing = true; isProcessing = true;
notifyListeners(); notifyListeners();
@ -39,7 +40,7 @@ class PaymentController with ChangeNotifier {
} }
if (result == null) { if (result == null) {
_showError(context, "Failed to connect to server or timeout."); _showError("Failed to connect to server or timeout.");
isProcessing = false; isProcessing = false;
notifyListeners(); notifyListeners();
return false; return false;
@ -48,7 +49,9 @@ class PaymentController with ChangeNotifier {
final orderId = result['data']?['orderId']; final orderId = result['data']?['orderId'];
final key = result['data']?['key']; final key = result['data']?['key'];
if (orderId == null || key == null) { if (orderId == null || key == null) {
_showError(context, "Invalid response from server."); _showError("Invalid response from server.");
logSafe("Invalid response from server.");
isProcessing = false; isProcessing = false;
notifyListeners(); notifyListeners();
return false; return false;
@ -64,7 +67,7 @@ class PaymentController with ChangeNotifier {
logSafe("✅ Razorpay instance initialized successfully."); logSafe("✅ Razorpay instance initialized successfully.");
} catch (e) { } catch (e) {
logSafe("❌ Razorpay init failed: $e", level: LogLevel.error); logSafe("❌ Razorpay init failed: $e", level: LogLevel.error);
_showError(context, "Payment system initialization failed."); _showError("Payment system initialization failed.");
isProcessing = false; isProcessing = false;
notifyListeners(); notifyListeners();
return false; return false;
@ -87,7 +90,7 @@ class PaymentController with ChangeNotifier {
return true; 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("Error opening payment gateway.");
_cleanup(); _cleanup();
isProcessing = false; isProcessing = false;
notifyListeners(); notifyListeners();
@ -103,15 +106,17 @@ class PaymentController with ChangeNotifier {
isProcessing = true; isProcessing = true;
notifyListeners(); notifyListeners();
Map<String, dynamic>? result; Map<String, dynamic>? verificationResult;
try { try {
result = await _paymentService logSafe("🟢 Verifying payment via backend...");
verificationResult = await _paymentService
.verifyPayment( .verifyPayment(
paymentId: response.paymentId!, paymentId: response.paymentId!,
orderId: response.orderId!, orderId: response.orderId!,
signature: response.signature!, signature: response.signature!,
) )
.timeout(const Duration(seconds: 10)); .timeout(const Duration(seconds: 15));
} catch (e) { } catch (e) {
logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error); logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error);
} }
@ -119,16 +124,32 @@ class PaymentController with ChangeNotifier {
isProcessing = false; isProcessing = false;
notifyListeners(); notifyListeners();
if (result != null && result['verified'] == true) { // Handle backend verification response properly
_showDialog( if (verificationResult != null) {
title: "Payment Successful 🎉", // Example backend response: { "verified": true, "message": "Payment verified" }
message: "Your payment was verified successfully.", final isVerified = verificationResult['verified'] == true;
success: true, final msg = verificationResult['message'] ?? '';
);
if (isVerified) {
_showDialog(
title: "Payment Successful 🎉",
message:
msg.isNotEmpty ? msg : "Your payment was verified successfully.",
success: true,
);
} else {
_showDialog(
title: "Verification Failed ❌",
message: msg.isNotEmpty
? msg
: "Payment completed but verification failed.",
success: false,
);
}
} else { } else {
_showDialog( _showDialog(
title: "Verification Failed ❌", title: "Verification Failed ❌",
message: "Payment completed but could not be verified.", message: "Payment completed but backend verification returned null.",
success: false, success: false,
); );
} }
@ -162,6 +183,7 @@ class PaymentController with ChangeNotifier {
_razorpay?.clear(); _razorpay?.clear();
} catch (_) {} } catch (_) {}
_razorpay = null; _razorpay = null;
logSafe("🧹 Razorpay instance cleaned up.");
} }
@override @override
@ -180,36 +202,37 @@ class PaymentController with ChangeNotifier {
required String message, required String message,
required bool success, required bool success,
}) { }) {
if (_context == null) return; if (Get.isDialogOpen == true) Get.back(); // Close any existing dialogs
showDialog( Get.defaultDialog(
context: _context!, title: title,
builder: (ctx) => AlertDialog( middleText: message,
title: Text(title), confirm: ElevatedButton(
content: Text(message), onPressed: () {
actions: [ Get.back(); // close dialog
TextButton( Get.snackbar(
child: const Text("OK"), success ? "Payment Successful 🎉" : "Payment Failed ❌",
onPressed: () { success
Navigator.of(ctx).pop(); ? "Payment verified successfully!"
ScaffoldMessenger.of(_context!).showSnackBar( : "Payment failed or could not be verified.",
SnackBar( backgroundColor: success ? Colors.green : Colors.red,
content: Text(success colorText: Colors.white,
? "Payment verified successfully!" snackPosition: SnackPosition.BOTTOM,
: "Payment failed or could not be verified."), );
backgroundColor: success ? Colors.green : Colors.red, },
), child: const Text("OK"),
);
},
),
],
), ),
); );
} }
void _showError(BuildContext context, String message) { void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar( if (Get.isDialogOpen == true) Get.back(); // Close any open dialog
SnackBar(content: Text(message), backgroundColor: Colors.red), Get.snackbar(
"Payment Error",
message,
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
); );
} }
} }

View File

@ -93,16 +93,26 @@ class ApiService {
static dynamic _parseResponse(http.Response response, {String label = ''}) { static dynamic _parseResponse(http.Response response, {String label = ''}) {
_log("$label Response: ${response.body}"); _log("$label Response: ${response.body}");
try { try {
final json = jsonDecode(response.body); final Map<String, dynamic> json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) {
return json['data']; // Treat 200299 range as success
final isHttpOk = response.statusCode >= 200 && response.statusCode < 300;
final isSuccess = json['success'] == true;
if (isHttpOk && isSuccess) {
return json['data']; // return full data block for use in payment
} }
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
// Log any API-level error
_log(
"API Error [$label]: ${json['message'] ?? 'Unknown error'} (code ${response.statusCode})");
return null;
} catch (e) { } catch (e) {
_log("Response parsing error [$label]: $e"); _log("Response parsing error [$label]: $e");
return null;
} }
return null;
} }
static dynamic _parseResponseForAllData(http.Response response, static dynamic _parseResponseForAllData(http.Response response,

View File

@ -21,12 +21,13 @@ class PaymentService {
required String signature, required String signature,
}) async { }) async {
try { try {
logSafe("🟢 Calling verifyPayment API: orderId=$orderId, paymentId=$paymentId"); logSafe("🟢 Calling verifyPayment API...");
final response = await ApiService.verifyPayment( final response = await ApiService.verifyPayment(
orderId: orderId, orderId: orderId,
paymentId: paymentId, paymentId: paymentId,
signature: signature, signature: signature,
); );
logSafe("✅ VerifyPayment API response: $response");
return response; return response;
} catch (e) { } catch (e) {
logSafe("❌ Error in verifyPayment: $e", level: LogLevel.error); logSafe("❌ Error in verifyPayment: $e", level: LogLevel.error);

View File

@ -27,22 +27,27 @@ 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) // Public routes that don't require authentication
const publicRoutes = [ const publicRoutes = [
'/auth/login-option', '/auth/login-option',
'/auth/login', '/auth/login',
'/subscription', // 👈 Allow this route without auth '/subscription',
'/payment', '/payment',
'/select-tenant', '/select-tenant',
]; ];
// Skip auth checks for public routes // Allow any route that starts with these public routes
if (publicRoutes.contains(route)) return null; if (route != null &&
publicRoutes.any((public) => route.startsWith(public))) {
return null;
}
// Block others if not logged in
if (!AuthService.isLoggedIn) { if (!AuthService.isLoggedIn) {
return const RouteSettings(name: '/auth/login-option'); return const RouteSettings(name: '/auth/login-option');
} }
// Ensure tenant is selected after login
if (!TenantService.isTenantSelected) { if (!TenantService.isTenantSelected) {
return const RouteSettings(name: '/select-tenant'); return const RouteSettings(name: '/select-tenant');
} }

View File

@ -303,14 +303,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',
@ -380,9 +380,9 @@ class _UserProfileBarState extends State<UserProfileBar>
)); ));
} }
// void _onSubscribeTap() { void _onSubscribeTap() {
// Get.toNamed("/subscription"); Get.toNamed("/subscription");
// } }
void _onMpinTap() { void _onMpinTap() {