285 lines
8.6 KiB
Dart
285 lines
8.6 KiB
Dart
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/tenant_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();
|
||
final TenantService _tenantService = TenantService();
|
||
|
||
bool isProcessing = false;
|
||
|
||
// Pending values to use after payment verification
|
||
String? _pendingTenantEnquireId;
|
||
String? _pendingPlanId;
|
||
|
||
/// ==============================
|
||
/// START PAYMENT (Safe init)
|
||
/// ==============================
|
||
Future<void> startPayment({
|
||
required double amount,
|
||
required String description,
|
||
required BuildContext context,
|
||
String? tenantEnquireId,
|
||
String? planId,
|
||
}) async {
|
||
try {
|
||
isProcessing = true;
|
||
notifyListeners();
|
||
|
||
// Save pending ids for post-payment subscription call
|
||
_pendingTenantEnquireId = tenantEnquireId;
|
||
_pendingPlanId = planId;
|
||
|
||
// Step 1: Create payment order
|
||
final result = await _paymentService.createOrder(amount);
|
||
logSafe("🧩 Raw response in PaymentController: $result");
|
||
|
||
// Step 2: Validate result before accessing keys
|
||
if (result == null) {
|
||
_showError("Failed to create order. Server returned null response.");
|
||
isProcessing = false;
|
||
notifyListeners();
|
||
return;
|
||
}
|
||
|
||
// Step 3: Handle both wrapped and unwrapped formats
|
||
final data = result['data'] ?? result;
|
||
final orderId = data?['orderId'];
|
||
final key = data?['key'];
|
||
|
||
if (orderId == null || key == null) {
|
||
_showError("Invalid response from server. Missing orderId or key.");
|
||
logSafe("💥 Invalid response structure: $result");
|
||
isProcessing = false;
|
||
notifyListeners();
|
||
return;
|
||
}
|
||
|
||
// Step 4: Initialize Razorpay if needed
|
||
_razorpay ??= Razorpay();
|
||
_razorpay!.on(Razorpay.EVENT_PAYMENT_SUCCESS, _handlePaymentSuccess);
|
||
_razorpay!.on(Razorpay.EVENT_PAYMENT_ERROR, _handlePaymentError);
|
||
_razorpay!.on(Razorpay.EVENT_EXTERNAL_WALLET, _handleExternalWallet);
|
||
|
||
// Step 5: Open Razorpay checkout
|
||
final options = {
|
||
'key': key,
|
||
'amount': (amount * 100).toInt(), // Razorpay expects amount in paise
|
||
'name': 'Marco',
|
||
'description': description,
|
||
'order_id': orderId,
|
||
'prefill': {'contact': '9999999999', 'email': 'test@marco.com'},
|
||
};
|
||
|
||
logSafe("🟠 Opening Razorpay checkout with options: $options");
|
||
_razorpay!.open(options);
|
||
} catch (e, s) {
|
||
_showError("Payment initialization failed. Please try again.");
|
||
logSafe("💥 Exception in startPayment: $e\n$s");
|
||
} finally {
|
||
isProcessing = false;
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
/// ==============================
|
||
/// EVENT HANDLERS
|
||
/// ==============================
|
||
void _handlePaymentSuccess(PaymentSuccessResponse response) async {
|
||
logSafe("✅ Payment Success: ${response.paymentId}");
|
||
isProcessing = true;
|
||
notifyListeners();
|
||
|
||
Map<String, dynamic>? verificationResult;
|
||
|
||
try {
|
||
logSafe("🟢 Verifying payment via backend...");
|
||
verificationResult = await _paymentService
|
||
.verifyPayment(
|
||
paymentId: response.paymentId!,
|
||
orderId: response.orderId!,
|
||
signature: response.signature!,
|
||
)
|
||
.timeout(const Duration(seconds: 15));
|
||
logSafe("🧩 Verification result: $verificationResult");
|
||
} catch (e) {
|
||
logSafe("⏱️ Verification timeout/error: $e", level: LogLevel.error);
|
||
}
|
||
|
||
isProcessing = false;
|
||
notifyListeners();
|
||
|
||
// Handle backend verification response properly
|
||
if (verificationResult != null) {
|
||
final isVerified = verificationResult['verified'] == true;
|
||
final msg = verificationResult['message'] ?? '';
|
||
|
||
if (isVerified) {
|
||
// If we have pending tenant and plan IDs, call subscription API
|
||
await _maybeSubscribeTenantAfterPayment(
|
||
verificationResult: verificationResult,
|
||
razorpayResponse: response,
|
||
);
|
||
|
||
_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 backend verification returned null.",
|
||
success: false,
|
||
);
|
||
}
|
||
|
||
_cleanup();
|
||
}
|
||
|
||
Future<void> _maybeSubscribeTenantAfterPayment({
|
||
required Map<String, dynamic> verificationResult,
|
||
required PaymentSuccessResponse razorpayResponse,
|
||
}) async {
|
||
if (_pendingTenantEnquireId == null || _pendingPlanId == null) {
|
||
logSafe("ℹ️ No pending tenant/plan id to subscribe.");
|
||
return;
|
||
}
|
||
|
||
// Determine paymentDetailId — prefer backend value if provided
|
||
final paymentDetailId = verificationResult['paymentDetailId'] ?? verificationResult['paymentId'] ?? razorpayResponse.paymentId;
|
||
|
||
final subscribePayload = {
|
||
"tenantEnquireId": _pendingTenantEnquireId,
|
||
"paymentDetailId": paymentDetailId,
|
||
"planId": _pendingPlanId,
|
||
};
|
||
|
||
logSafe("🟢 Subscribing tenant automatically: $subscribePayload");
|
||
|
||
final subResp = await _tenantService.subscribeTenant(subscribePayload);
|
||
|
||
// Clear pending values immediately to avoid double-invoke
|
||
_pendingTenantEnquireId = null;
|
||
_pendingPlanId = null;
|
||
|
||
if (subResp == null) {
|
||
logSafe("❌ subscribeTenant returned null");
|
||
_showDialog(
|
||
title: "Subscription Failed",
|
||
message: "Failed to call subscription API. Please contact support.",
|
||
success: false,
|
||
);
|
||
return;
|
||
}
|
||
|
||
final data = subResp['data'] ?? subResp;
|
||
// backend success flag might be in different places; check robustly
|
||
final bool success = (data is Map && (data['success'] == true || subResp['success'] == true)) || (subResp['statusCode'] == 200);
|
||
|
||
if (success) {
|
||
_showDialog(
|
||
title: "Subscription Active ✅",
|
||
message: data['message'] ?? "Tenant subscribed successfully.",
|
||
success: true,
|
||
);
|
||
} else {
|
||
_showDialog(
|
||
title: "Subscription Failed",
|
||
message: data['message'] ?? "Subscription API did not confirm success.",
|
||
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,
|
||
);
|
||
|
||
_cleanup();
|
||
}
|
||
|
||
void _handleExternalWallet(ExternalWalletResponse response) {
|
||
logSafe("ℹ️ External Wallet Used: ${response.walletName}");
|
||
}
|
||
|
||
/// ==============================
|
||
/// CLEANUP / DISPOSE
|
||
/// ==============================
|
||
void _cleanup() {
|
||
try {
|
||
_razorpay?.clear();
|
||
} catch (_) {}
|
||
_razorpay = null;
|
||
logSafe("🧹 Razorpay instance cleaned up.");
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_cleanup();
|
||
super.dispose();
|
||
}
|
||
|
||
void disposeController() => _cleanup();
|
||
|
||
/// ==============================
|
||
/// HELPER UI FUNCTIONS
|
||
/// ==============================
|
||
void _showDialog({
|
||
required String title,
|
||
required String message,
|
||
required bool success,
|
||
}) {
|
||
if (Get.isDialogOpen == true) Get.back(); // Close any existing dialogs
|
||
|
||
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(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,
|
||
);
|
||
}
|
||
}
|