feature/payment #77

Closed
manish.zure wants to merge 25 commits from feature/payment into main
5 changed files with 101 additions and 62 deletions
Showing only changes of commit 70fcc2e662 - Show all commits

View File

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

View File

@ -21,12 +21,13 @@ class PaymentService {
required String signature,
}) async {
try {
logSafe("🟢 Calling verifyPayment API: orderId=$orderId, paymentId=$paymentId");
logSafe("🟢 Calling verifyPayment API...");
final response = await ApiService.verifyPayment(
orderId: orderId,
paymentId: paymentId,
signature: signature,
);
logSafe("✅ VerifyPayment API response: $response");
return response;
} catch (e) {
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 {
@override
RouteSettings? redirect(String? route) {
// Public routes (no auth required)
// Public routes that don't require authentication
const publicRoutes = [
'/auth/login-option',
'/auth/login',
'/subscription', // 👈 Allow this route without auth
'/subscription',
'/payment',
'/select-tenant',
];
// Skip auth checks for public routes
if (publicRoutes.contains(route)) return null;
// Allow any route that starts with these public routes
if (route != null &&
publicRoutes.any((public) => route.startsWith(public))) {
return null;
}
// Block others if not logged in
if (!AuthService.isLoggedIn) {
return const RouteSettings(name: '/auth/login-option');
}
// Ensure tenant is selected after login
if (!TenantService.isTenantSelected) {
return const RouteSettings(name: '/select-tenant');
}

View File

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