From e0aec6e84017adb919a4405720c4fad72f8b0e70 Mon Sep 17 00:00:00 2001 From: riz081 Date: Fri, 1 Aug 2025 09:48:52 +0700 Subject: [PATCH] menambah interval waktu ke 15 menit --- .../bdkipoc/qris/QrisResultActivity.java | 807 ++++++++++-------- 1 file changed, 458 insertions(+), 349 deletions(-) diff --git a/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java b/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java index 1d08fba..f49d380 100644 --- a/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java +++ b/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java @@ -61,11 +61,13 @@ public class QrisResultActivity extends AppCompatActivity { private Button checkStatusButton; private Button returnMainButton; - // QR Refresh Components + // QR Management Components private Handler qrRefreshHandler; private Runnable qrRefreshRunnable; - private int countdownSeconds = 60; + private Handler paymentMonitorHandler; + private Runnable paymentMonitorRunnable; private boolean isQrRefreshActive = true; + private boolean isPaymentMonitorActive = true; // Success screen views private View successScreen; @@ -84,33 +86,46 @@ public class QrisResultActivity extends AppCompatActivity { private String currentQrImageUrl; private int originalAmount; - // ✅ QR String untuk validasi QRIS + // ✅ QR Management Variables private String currentQrString = ""; private String qrStringFromMidtrans = ""; - - // ✅ Store actual issuer/acquirer from Midtrans response private String actualIssuerFromMidtrans = ""; private String actualAcquirerFromMidtrans = ""; - - // ✅ Track QR refresh transaction for payment monitoring private String currentQrTransactionId = ""; private boolean isMonitoringQrRefreshTransaction = false; + // ✅ QR Expiration Management + private long qrCreationTime = 0; + private int qrExpirationMinutes = 15; // Default 15 minutes + private String detectedProvider = "others"; // Default provider + + // ✅ Payment Status Tracking + private boolean paymentProcessed = false; + private String lastKnownStatus = "pending"; + private String backendBase = "https://be-edc.msvc.app"; private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans"; // Sandbox and Production server keys private static final String MIDTRANS_SANDBOX_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc="; - private static final String MIDTRANS_PRODUCTION_AUTH = "TWlkLXNlcnZlci1sMlZPalotdVlVanpvNnU4VzAtYmF1a2o="; // Base64 of "Mid-server-l2VOjZ-uYUjzo6u8W0-baukj:" - - // Currently active server key (switch by commenting/uncommenting) - // private static final String MIDTRANS_AUTH = MIDTRANS_PRODUCTION_AUTH; + private static final String MIDTRANS_PRODUCTION_AUTH = "TWlkLXNlcnZlci1sMlZPalotdVlVanpvNnU4VzAtYmF1a2o="; private static final String MIDTRANS_AUTH = MIDTRANS_SANDBOX_AUTH; // Default to sandbox - - // Midtrans charge URL private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge"; - // private static final String MIDTRANS_CHARGE_URL = "https://api.midtrans.com/v2/charge"; + // ✅ Provider-specific expiration times (in minutes) + private static final Map PROVIDER_EXPIRATION_MAP = new HashMap() {{ + put("shopeepay", 5); + put("shopee", 5); + put("airpay shopee", 5); + put("gopay", 15); + put("dana", 15); + put("ovo", 15); + put("linkaja", 15); + put("link aja", 15); + put("jenius", 15); + put("qris", 15); + put("others", 15); + }}; // ✅ Mapping dari technical issuer ke display name private static final Map ISSUER_DISPLAY_MAP = new HashMap() {{ @@ -140,8 +155,6 @@ public class QrisResultActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - // ✅ TETAP MENGGUNAKAN LAYOUT ASLI UNTUK QR DISPLAY setContentView(R.layout.activity_qris_result); // Initialize views @@ -150,29 +163,27 @@ public class QrisResultActivity extends AppCompatActivity { // Get intent data getIntentData(); - // ✅ Initialize parent transaction tracking - currentQrTransactionId = transactionId; // Initially monitor parent transaction + // ✅ Initialize QR expiration tracking + initializeQrExpiration(); + + // ✅ Initialize transaction tracking + currentQrTransactionId = transactionId; isMonitoringQrRefreshTransaction = false; Log.d("QrisResultFlow", "🆔 Initial monitoring - parent transaction ID: " + transactionId); - Log.d("QrisResultFlow", "🆔 Parent order ID: " + orderId); // Setup UI setupUI(); - - // Setup success screen setupSuccessScreen(); + setupClickListeners(); - // Start QR refresh timer - startQrRefreshTimer(); + // ✅ Start enhanced QR management + startEnhancedQrManagement(); + + // ✅ Start enhanced payment monitoring + startEnhancedPaymentMonitoring(); // Start polling for pending payment log pollPendingPaymentLog(orderId); - - // Set up click listeners - setupClickListeners(); - - // Start continuous payment monitoring - startContinuousPaymentMonitoring(); } private void initializeViews() { @@ -199,16 +210,15 @@ public class QrisResultActivity extends AppCompatActivity { successScreen = findViewById(R.id.success_screen); successIcon = findViewById(R.id.success_icon); successMessage = findViewById(R.id.success_message); - qrUrlTextView = findViewById(R.id.qrUrlTextView); simulatorButton = findViewById(R.id.simulatorButton); - // Initialize handler for QR refresh + // Initialize handlers qrRefreshHandler = new Handler(Looper.getMainLooper()); + paymentMonitorHandler = new Handler(Looper.getMainLooper()); } private void setupSuccessScreen() { - // Initially hide success screen if (successScreen != null) { successScreen.setVisibility(View.GONE); } @@ -226,22 +236,59 @@ public class QrisResultActivity extends AppCompatActivity { acquirer = intent.getStringExtra("acquirer"); merchantId = intent.getStringExtra("merchantId"); - // ✅ GET QR STRING from intent if available + // ✅ Get QR String from intent qrStringFromMidtrans = intent.getStringExtra("qrString"); if (qrStringFromMidtrans != null) { currentQrString = qrStringFromMidtrans; } - // Enhanced logging for debugging Log.d("QrisResultFlow", "=== QRIS RESULT ACTIVITY STARTED ==="); Log.d("QrisResultFlow", "QR Image URL: " + currentQrImageUrl); Log.d("QrisResultFlow", "QR String: " + (currentQrString.length() > 50 ? currentQrString.substring(0, 50) + "..." : currentQrString)); - Log.d("QrisResultFlow", "Amount (int): " + originalAmount); - Log.d("QrisResultFlow", "Gross Amount (string): " + grossAmount); - Log.d("QrisResultFlow", "Reference ID: " + referenceId); + Log.d("QrisResultFlow", "Amount: " + originalAmount); + Log.d("QrisResultFlow", "Acquirer: " + acquirer); Log.d("QrisResultFlow", "Order ID: " + orderId); Log.d("QrisResultFlow", "Transaction ID: " + transactionId); - Log.d("QrisResultFlow", "======================================"); + } + + // ✅ NEW: Initialize QR expiration based on provider + private void initializeQrExpiration() { + qrCreationTime = System.currentTimeMillis(); + + // ✅ Detect provider from acquirer or QR string + detectedProvider = detectProviderFromData(); + qrExpirationMinutes = PROVIDER_EXPIRATION_MAP.get(detectedProvider.toLowerCase()); + + Log.d("QrisResultFlow", "🕒 QR Expiration initialized:"); + Log.d("QrisResultFlow", " Detected Provider: " + detectedProvider); + Log.d("QrisResultFlow", " Expiration Time: " + qrExpirationMinutes + " minutes"); + Log.d("QrisResultFlow", " Created At: " + new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date(qrCreationTime))); + } + + // ✅ NEW: Detect provider from available data + private String detectProviderFromData() { + // Try to detect from acquirer first + if (acquirer != null && !acquirer.isEmpty()) { + String lowerAcquirer = acquirer.toLowerCase().trim(); + if (PROVIDER_EXPIRATION_MAP.containsKey(lowerAcquirer)) { + Log.d("QrisResultFlow", "🔍 Provider detected from acquirer: " + acquirer + " -> " + lowerAcquirer); + return lowerAcquirer; + } + } + + // Try to detect from QR string content + if (currentQrString != null && !currentQrString.isEmpty()) { + String lowerQrString = currentQrString.toLowerCase(); + for (String provider : PROVIDER_EXPIRATION_MAP.keySet()) { + if (lowerQrString.contains(provider.toLowerCase())) { + Log.d("QrisResultFlow", "🔍 Provider detected from QR string: " + provider); + return provider; + } + } + } + + Log.d("QrisResultFlow", "🔍 No specific provider detected, using 'others' (15 min expiration)"); + return "others"; } private void setupUI() { @@ -253,27 +300,23 @@ public class QrisResultActivity extends AppCompatActivity { return; } - // Display formatted amount in rupiah format + // Display formatted amount String formattedAmount = formatRupiahAmount(grossAmount != null ? grossAmount : String.valueOf(originalAmount)); amountTextView.setText(formattedAmount); - // Set reference data to hidden field + // Set reference data if (referenceTextView != null) { referenceTextView.setText("Reference ID: " + referenceId); } // Load initial QR image loadQrImage(currentQrImageUrl); - - // Initialize timer display - timerTextView.setText("60"); - // Set initial status in hidden field + // Set initial status if (statusTextView != null) { statusTextView.setText("Waiting for payment..."); } - // Enable simulate payment functionality if (checkStatusButton != null) { checkStatusButton.setEnabled(false); } @@ -285,17 +328,14 @@ public class QrisResultActivity extends AppCompatActivity { setupUrlCopyFunctionality(); setupSimulatorButton(); - // ✅ VALIDATE QR STRING + // ✅ Validate QR String validateQrString(currentQrString); } private String formatRupiahAmount(String amount) { try { - // Remove any existing currency symbols and formatting String cleanAmount = amount.replaceAll("[^0-9]", ""); long amountLong = Long.parseLong(cleanAmount); - - // Format with dots as thousand separators return "RP." + String.format("%,d", amountLong).replace(',', '.'); } catch (NumberFormatException e) { Log.w("QrisResultFlow", "Error formatting rupiah amount: " + e.getMessage()); @@ -303,7 +343,7 @@ public class QrisResultActivity extends AppCompatActivity { } } - // ✅ NEW: Validate QR String format + // ✅ ENHANCED: QR String validation private void validateQrString(String qrString) { if (qrString == null || qrString.isEmpty()) { Log.w("QrisResultFlow", "⚠️ QR String is empty - QR might be unparsable"); @@ -312,71 +352,102 @@ public class QrisResultActivity extends AppCompatActivity { Log.d("QrisResultFlow", "🔍 Validating QR String..."); Log.d("QrisResultFlow", "QR String length: " + qrString.length()); - Log.d("QrisResultFlow", "QR String preview: " + (qrString.length() > 100 ? qrString.substring(0, 100) + "..." : qrString)); - // ✅ BASIC QRIS FORMAT VALIDATION + // Basic QRIS format validation if (qrString.startsWith("00020101") || qrString.startsWith("00020102")) { Log.d("QrisResultFlow", "✅ QR String has valid QRIS header"); } else { - Log.w("QrisResultFlow", "⚠️ QR String might not be valid QRIS format - missing standard header"); + Log.w("QrisResultFlow", "⚠️ QR String might not be valid QRIS format"); } - // ✅ CHECK FOR REQUIRED FIELDS + // Check for required fields if (qrString.contains("ID.CO.QRIS.WWW")) { Log.d("QrisResultFlow", "✅ QR String contains QRIS Indonesia identifier"); - } else { - Log.w("QrisResultFlow", "⚠️ QR String missing Indonesia QRIS identifier"); } - // ✅ CHECK FOR AMOUNT if (qrString.contains("54")) { // Field 54 is transaction amount Log.d("QrisResultFlow", "✅ QR String contains amount field"); - } else { - Log.w("QrisResultFlow", "⚠️ QR String missing amount field"); } } - private void startQrRefreshTimer() { - countdownSeconds = 60; - isQrRefreshActive = true; + // ✅ NEW: Enhanced QR Management with proper expiration + private void startEnhancedQrManagement() { + Log.d("QrisResultFlow", "🚀 Starting enhanced QR management"); qrRefreshRunnable = new Runnable() { + private int countdownSeconds = qrExpirationMinutes * 60; // Convert minutes to seconds + @Override public void run() { - if (!isQrRefreshActive) { + if (!isQrRefreshActive || paymentProcessed) { + return; + } + + // ✅ Check if QR has expired + long currentTime = System.currentTimeMillis(); + long elapsedMinutes = (currentTime - qrCreationTime) / (1000 * 60); + + if (elapsedMinutes >= qrExpirationMinutes) { + Log.w("QrisResultFlow", "⏰ QR Code has expired after " + elapsedMinutes + " minutes"); + handleQrExpiration(); return; } if (countdownSeconds > 0) { // Update countdown display - timerTextView.setText(String.valueOf(countdownSeconds)); + int displayMinutes = countdownSeconds / 60; + int displaySeconds = countdownSeconds % 60; + String timeDisplay = String.format("%d:%02d", displayMinutes, displaySeconds); + timerTextView.setText(timeDisplay); countdownSeconds--; // Schedule next update in 1 second qrRefreshHandler.postDelayed(this, 1000); } else { - // ✅ Time to refresh QR code - CREATE NEW QR TRANSACTION - Log.d("QrisResultFlow", "🔄 QR Code refresh time reached - creating new QR transaction"); + // Time to refresh QR code + Log.d("QrisResultFlow", "🔄 QR Code refresh time reached"); refreshQrCode(); } } }; qrRefreshHandler.post(qrRefreshRunnable); - Log.d("QrisResultFlow", "🕒 QR refresh timer started - 60 seconds countdown"); + Log.d("QrisResultFlow", "🕒 QR management started - " + qrExpirationMinutes + " minutes expiration"); } + // ✅ NEW: Handle QR expiration + private void handleQrExpiration() { + Log.w("QrisResultFlow", "⏰ Handling QR expiration for provider: " + detectedProvider); + + runOnUiThread(() -> { + // Disable the expired QR + if (qrImageView != null) { + qrImageView.setAlpha(0.5f); // Make it semi-transparent + } + + timerTextView.setText("EXPIRED"); + timerTextView.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + + Toast.makeText(this, "QR Code expired! Generating new QR...", Toast.LENGTH_LONG).show(); + + // Force refresh QR code + refreshQrCode(); + }); + } + + // ✅ ENHANCED: QR Code refresh with proper expiration handling private void refreshQrCode() { - if (!isQrRefreshActive) { + if (!isQrRefreshActive || paymentProcessed) { return; } Log.d("QrisResultFlow", "🔄 Starting QR code refresh..."); - // Show loading state - timerTextView.setText("..."); + runOnUiThread(() -> { + timerTextView.setText("Refreshing..."); + timerTextView.setTextColor(getResources().getColor(android.R.color.holo_orange_dark)); + }); - // Generate new QR code in background new Thread(() -> { try { QrRefreshResult result = generateNewQrCode(); @@ -385,40 +456,190 @@ public class QrisResultActivity extends AppCompatActivity { if (result != null && result.qrUrl != null && !result.qrUrl.isEmpty()) { // ✅ Successfully refreshed QR currentQrImageUrl = result.qrUrl; - currentQrString = result.qrString; // ✅ UPDATE QR STRING - loadQrImage(result.qrUrl); + currentQrString = result.qrString; - // ✅ VALIDATE NEW QR STRING + // ✅ Reset QR creation time and expiration + qrCreationTime = System.currentTimeMillis(); + Log.d("QrisResultFlow", "🔄 QR refreshed at: " + + new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date(qrCreationTime))); + + // Reset QR image appearance + if (qrImageView != null) { + qrImageView.setAlpha(1.0f); + } + timerTextView.setTextColor(getResources().getColor(android.R.color.black)); + + loadQrImage(result.qrUrl); validateQrString(currentQrString); - // ✅ IMPORTANT: Now monitoring QR refresh transaction for payment - Log.d("QrisResultFlow", "🔄 QR refreshed - now monitoring new QR transaction"); - Log.d("QrisResultFlow", "🔄 Parent transaction ID: " + transactionId + " (reference only)"); - Log.d("QrisResultFlow", "🔄 Monitoring QR transaction ID: " + currentQrTransactionId); + // ✅ Update monitoring to new QR transaction + if (!result.transactionId.isEmpty()) { + currentQrTransactionId = result.transactionId; + isMonitoringQrRefreshTransaction = true; + Log.d("QrisResultFlow", "🔄 Now monitoring QR refresh transaction: " + result.transactionId); + } - // Restart timer - countdownSeconds = 60; Log.d("QrisResultFlow", "✅ QR code refreshed successfully"); - Toast.makeText(QrisResultActivity.this, "QR Code refreshed with valid format", Toast.LENGTH_SHORT).show(); + Toast.makeText(QrisResultActivity.this, "QR Code refreshed", Toast.LENGTH_SHORT).show(); + + // Restart QR management with new expiration + startEnhancedQrManagement(); + } else { - // Failed to generate new QR Log.e("QrisResultFlow", "❌ Failed to refresh QR code"); - countdownSeconds = 30; - Toast.makeText(QrisResultActivity.this, "QR refresh failed, retrying...", Toast.LENGTH_SHORT).show(); + runOnUiThread(() -> { + timerTextView.setText("Refresh Failed"); + timerTextView.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + Toast.makeText(QrisResultActivity.this, "Failed to refresh QR. Please try again.", Toast.LENGTH_LONG).show(); + }); + + // Retry after 30 seconds + qrRefreshHandler.postDelayed(() -> refreshQrCode(), 30000); } - - // Continue timer - qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000); }); } catch (Exception e) { Log.e("QrisResultFlow", "❌ QR refresh error: " + e.getMessage(), e); runOnUiThread(() -> { - countdownSeconds = 30; - qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000); + timerTextView.setText("Error"); + timerTextView.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); Toast.makeText(QrisResultActivity.this, "QR refresh error", Toast.LENGTH_SHORT).show(); }); + + qrRefreshHandler.postDelayed(() -> refreshQrCode(), 30000); + } + }).start(); + } + + // ✅ NEW: Enhanced Payment Monitoring with better real-time detection + private void startEnhancedPaymentMonitoring() { + Log.d("QrisResultFlow", "🔍 Starting enhanced payment monitoring"); + + paymentMonitorRunnable = new Runnable() { + @Override + public void run() { + if (!isPaymentMonitorActive || paymentProcessed) { + return; + } + + checkPaymentStatusEnhanced(); + + if (!isFinishing() && isPaymentMonitorActive && !paymentProcessed) { + // Check every 3 seconds for faster detection + paymentMonitorHandler.postDelayed(this, 3000); + } + } + }; + + paymentMonitorHandler.post(paymentMonitorRunnable); + } + + // ✅ NEW: Enhanced payment status checking + private void checkPaymentStatusEnhanced() { + new Thread(() -> { + try { + // ✅ Monitor current QR transaction (parent or refresh) + String monitoringTransactionId = !currentQrTransactionId.isEmpty() ? currentQrTransactionId : transactionId; + String statusUrl = "https://api.sandbox.midtrans.com/v2/" + monitoringTransactionId + "/status"; + + String transactionType = isMonitoringQrRefreshTransaction ? "QR refresh transaction" : "parent transaction"; + Log.d("QrisResultFlow", "🔍 Enhanced status check for " + transactionType + ": " + monitoringTransactionId); + + URL url = new URI(statusUrl).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("Authorization", MIDTRANS_AUTH); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + conn.setConnectTimeout(8000); + conn.setReadTimeout(8000); + + int responseCode = conn.getResponseCode(); + + if (responseCode == 200) { + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + + JSONObject statusResponse = new JSONObject(response.toString()); + String transactionStatus = statusResponse.optString("transaction_status", ""); + String paymentType = statusResponse.optString("payment_type", ""); + String grossAmount = statusResponse.optString("gross_amount", ""); + + // ✅ Extract and store actual issuer/acquirer + String actualIssuer = statusResponse.optString("issuer", ""); + String actualAcquirer = statusResponse.optString("acquirer", ""); + + // ✅ Update QR string if available + String qrStringFromStatus = statusResponse.optString("qr_string", ""); + if (!qrStringFromStatus.isEmpty()) { + currentQrString = qrStringFromStatus; + } + + Log.d("QrisResultFlow", "💳 Enhanced status check result:"); + Log.d("QrisResultFlow", " Status: " + transactionStatus + " (was: " + lastKnownStatus + ")"); + Log.d("QrisResultFlow", " Payment Type: " + paymentType); + Log.d("QrisResultFlow", " Actual Issuer: " + actualIssuer); + Log.d("QrisResultFlow", " Actual Acquirer: " + actualAcquirer); + + // ✅ Store actual values from Midtrans + if (!actualIssuer.isEmpty() && !actualIssuer.equalsIgnoreCase("qris")) { + actualIssuerFromMidtrans = actualIssuer; + } + + if (!actualAcquirer.isEmpty() && !actualAcquirer.equalsIgnoreCase("qris")) { + actualAcquirerFromMidtrans = actualAcquirer; + } + + if (!actualIssuer.isEmpty()) { + acquirer = actualIssuer; + } + + // ✅ Handle status changes + if (!transactionStatus.equals(lastKnownStatus)) { + Log.d("QrisResultFlow", "📊 Transaction status changed: " + lastKnownStatus + " -> " + transactionStatus); + lastKnownStatus = transactionStatus; + + // ✅ Handle successful payment + if (transactionStatus.equals("settlement") || + transactionStatus.equals("capture") || + transactionStatus.equals("success")) { + + if (!paymentProcessed) { + paymentProcessed = true; + Log.d("QrisResultFlow", "🎉 Payment SUCCESS detected! Status: " + transactionStatus); + + runOnUiThread(() -> { + stopAllMonitoring(); + syncTransactionStatusToBackend("PAID"); + showPaymentSuccess(); + Toast.makeText(QrisResultActivity.this, "Payment Successful! 🎉", Toast.LENGTH_LONG).show(); + }); + } + + } else if (transactionStatus.equals("expire") || transactionStatus.equals("cancel")) { + Log.w("QrisResultFlow", "⚠️ Payment expired or cancelled: " + transactionStatus); + syncTransactionStatusToBackend("FAILED"); + + runOnUiThread(() -> { + Toast.makeText(QrisResultActivity.this, "Payment " + transactionStatus, Toast.LENGTH_LONG).show(); + }); + + } else if (transactionStatus.equals("pending")) { + Log.d("QrisResultFlow", "⏳ Payment still pending"); + } + } + + } else { + Log.w("QrisResultFlow", "⚠️ Enhanced status check failed: HTTP " + responseCode); + } + + } catch (Exception e) { + Log.e("QrisResultFlow", "❌ Enhanced status check error: " + e.getMessage(), e); } }).start(); } @@ -445,7 +666,7 @@ public class QrisResultActivity extends AppCompatActivity { }); } - // ✅ NEW: QR Refresh Result class + // ✅ QR Refresh Result class private static class QrRefreshResult { String qrUrl; String qrString; @@ -458,26 +679,24 @@ public class QrisResultActivity extends AppCompatActivity { } } - // ✅ ENHANCED: Return both QR URL and QR String + // ✅ ENHANCED: Generate new QR code with proper validation private QrRefreshResult generateNewQrCode() { try { - Log.d("QrisResultFlow", "🔧 Refreshing QR code for existing transaction"); - Log.d("QrisResultFlow", "🔄 Parent Transaction ID: " + transactionId); - Log.d("QrisResultFlow", "🔄 Parent Order ID: " + orderId); + Log.d("QrisResultFlow", "🔧 Generating new QR code with enhanced validation"); - // ✅ GENERATE SHORT ORDER ID to avoid 50 character limit + // ✅ Generate unique order ID String shortTimestamp = String.valueOf(System.currentTimeMillis()).substring(7); String newOrderId = orderId.substring(0, Math.min(orderId.length(), 43)) + "-q" + shortTimestamp; Log.d("QrisResultFlow", "🆕 New QR Order ID: " + newOrderId + " (Length: " + newOrderId.length() + ")"); - // ✅ VALIDATE ORDER ID LENGTH + // ✅ Validate order ID length if (newOrderId.length() > 50) { newOrderId = orderId.substring(0, 36) + "-q" + shortTimestamp.substring(0, Math.min(shortTimestamp.length(), 7)); - Log.w("QrisResultFlow", "⚠️ Order ID too long, using fallback: " + newOrderId + " (Length: " + newOrderId.length() + ")"); + Log.w("QrisResultFlow", "⚠️ Order ID too long, using fallback: " + newOrderId); } - // ✅ CREATE LINK TO PARENT TRANSACTION + // ✅ Create enhanced payload with expiration tracking JSONObject customField1 = new JSONObject(); customField1.put("parent_transaction_id", transactionId); customField1.put("parent_order_id", orderId); @@ -485,8 +704,10 @@ public class QrisResultActivity extends AppCompatActivity { customField1.put("qr_refresh_time", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date())); customField1.put("qr_refresh_count", System.currentTimeMillis()); customField1.put("is_qr_refresh", true); + customField1.put("detected_provider", detectedProvider); + customField1.put("expiration_minutes", qrExpirationMinutes); - // ✅ CREATE QRIS PAYLOAD WITH NEW ORDER ID + // ✅ Create QRIS payload JSONObject payload = new JSONObject(); payload.put("payment_type", "qris"); @@ -508,7 +729,7 @@ public class QrisResultActivity extends AppCompatActivity { item.put("price", originalAmount); item.put("quantity", 1); item.put("name", "QRIS Payment QR Refresh - " + new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date()) + - " (Parent Ref: " + referenceId + ")"); + " (" + detectedProvider.toUpperCase() + " - " + qrExpirationMinutes + "min)"); itemDetails.put(item); payload.put("item_details", itemDetails); @@ -519,11 +740,13 @@ public class QrisResultActivity extends AppCompatActivity { qrisDetails.put("qr_refresh", true); qrisDetails.put("parent_transaction_id", transactionId); qrisDetails.put("refresh_timestamp", System.currentTimeMillis()); + qrisDetails.put("provider", detectedProvider); + qrisDetails.put("expiration_minutes", qrExpirationMinutes); payload.put("qris", qrisDetails); - Log.d("QrisResultFlow", "📤 QR Refresh payload: " + payload.toString()); + Log.d("QrisResultFlow", "📤 Enhanced QR refresh payload ready"); - // ✅ MAKE API CALL TO MIDTRANS + // ✅ Make API call to Midtrans URL url = new URI(MIDTRANS_CHARGE_URL).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); @@ -531,9 +754,11 @@ public class QrisResultActivity extends AppCompatActivity { conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Authorization", MIDTRANS_AUTH); conn.setRequestProperty("X-Override-Notification", webhookUrl); - conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0 QR-Refresh"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0 QR-Refresh-Enhanced"); conn.setRequestProperty("X-QR-Refresh", "true"); conn.setRequestProperty("X-Parent-Transaction", transactionId); + conn.setRequestProperty("X-Provider", detectedProvider); + conn.setRequestProperty("X-Expiration-Minutes", String.valueOf(qrExpirationMinutes)); conn.setDoOutput(true); conn.setConnectTimeout(30000); conn.setReadTimeout(30000); @@ -544,7 +769,7 @@ public class QrisResultActivity extends AppCompatActivity { } int responseCode = conn.getResponseCode(); - Log.d("QrisResultFlow", "📥 QR refresh response code: " + responseCode); + Log.d("QrisResultFlow", "📥 Enhanced QR refresh response code: " + responseCode); if (responseCode == 200 || responseCode == 201) { BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); @@ -554,21 +779,18 @@ public class QrisResultActivity extends AppCompatActivity { response.append(responseLine.trim()); } - // ✅ LOG FULL RESPONSE FOR DEBUGGING - Log.d("QrisResultFlow", "📋 Full QR Refresh Response: " + response.toString()); - JSONObject jsonResponse = new JSONObject(response.toString()); if (jsonResponse.has("status_code")) { String statusCode = jsonResponse.getString("status_code"); if (!statusCode.equals("201")) { String statusMessage = jsonResponse.optString("status_message", "Unknown error"); - Log.e("QrisResultFlow", "❌ QR refresh failed with status: " + statusCode + " - " + statusMessage); + Log.e("QrisResultFlow", "❌ Enhanced QR refresh failed: " + statusCode + " - " + statusMessage); return null; } } - // ✅ EXTRACT QR URL AND QR STRING + // ✅ Extract QR URL and QR String String newQrUrl = null; String newQrString = null; String newTransactionId = jsonResponse.optString("transaction_id", ""); @@ -582,24 +804,18 @@ public class QrisResultActivity extends AppCompatActivity { } } - // ✅ GET QR STRING (CRITICAL FOR QRIS VALIDATION) + // ✅ Get QR String if (jsonResponse.has("qr_string")) { newQrString = jsonResponse.getString("qr_string"); - Log.d("QrisResultFlow", "✅ Found QR String in response: " + (newQrString.length() > 50 ? newQrString.substring(0, 50) + "..." : newQrString)); + Log.d("QrisResultFlow", "✅ Enhanced QR String obtained: " + (newQrString.length() > 50 ? newQrString.substring(0, 50) + "..." : newQrString)); } else { - Log.w("QrisResultFlow", "⚠️ No QR String found in response - QR might be unparsable"); + Log.w("QrisResultFlow", "⚠️ No QR String in enhanced response"); } - if (!newTransactionId.isEmpty()) { - currentQrTransactionId = newTransactionId; - isMonitoringQrRefreshTransaction = true; - Log.d("QrisResultFlow", "🔄 Now monitoring QR refresh transaction: " + newTransactionId); - } - - Log.d("QrisResultFlow", "✅ QR refresh successful!"); + Log.d("QrisResultFlow", "✅ Enhanced QR refresh successful!"); Log.d("QrisResultFlow", "🆕 New QR URL: " + newQrUrl); - Log.d("QrisResultFlow", "🆕 New QR String Length: " + (newQrString != null ? newQrString.length() : "null")); Log.d("QrisResultFlow", "🆕 New QR Transaction ID: " + newTransactionId); + Log.d("QrisResultFlow", "🆕 Provider: " + detectedProvider + " (" + qrExpirationMinutes + " min expiration)"); return new QrRefreshResult(newQrUrl, newQrString, newTransactionId); @@ -617,22 +833,21 @@ public class QrisResultActivity extends AppCompatActivity { errorResponse = errorBuilder.toString(); } - Log.e("QrisResultFlow", "❌ QR refresh HTTP error " + responseCode + ": " + errorResponse); + Log.e("QrisResultFlow", "❌ Enhanced QR refresh HTTP error " + responseCode + ": " + errorResponse); return null; } } catch (Exception e) { - Log.e("QrisResultFlow", "❌ QR refresh exception: " + e.getMessage(), e); + Log.e("QrisResultFlow", "❌ Enhanced QR refresh exception: " + e.getMessage(), e); return null; } } - // ✅ ENHANCED: Load QR image with better error handling + // ✅ ENHANCED: Load QR image with validation private void loadQrImage(String qrImageUrl) { if (qrImageUrl != null && !qrImageUrl.isEmpty()) { Log.d("QrisResultFlow", "🖼️ Loading QR image from: " + qrImageUrl); - // ✅ VALIDATE URL FORMAT if (!qrImageUrl.startsWith("http")) { Log.e("QrisResultFlow", "❌ Invalid QR URL format: " + qrImageUrl); qrImageView.setVisibility(View.GONE); @@ -648,20 +863,27 @@ public class QrisResultActivity extends AppCompatActivity { } } - private void stopQrRefresh() { + // ✅ NEW: Stop all monitoring activities + private void stopAllMonitoring() { isQrRefreshActive = false; + isPaymentMonitorActive = false; + if (qrRefreshHandler != null && qrRefreshRunnable != null) { qrRefreshHandler.removeCallbacks(qrRefreshRunnable); } - Log.d("QrisResultFlow", "🛑 QR refresh timer stopped"); + if (paymentMonitorHandler != null && paymentMonitorRunnable != null) { + paymentMonitorHandler.removeCallbacks(paymentMonitorRunnable); + } + + Log.d("QrisResultFlow", "🛑 All monitoring stopped"); } private void setupClickListeners() { // Cancel button cancelButton.setOnClickListener(v -> { addClickAnimation(v); - stopQrRefresh(); + stopAllMonitoring(); finish(); }); @@ -669,7 +891,7 @@ public class QrisResultActivity extends AppCompatActivity { if (backNavigation != null) { backNavigation.setOnClickListener(v -> { addClickAnimation(v); - stopQrRefresh(); + stopAllMonitoring(); finish(); }); } @@ -677,8 +899,8 @@ public class QrisResultActivity extends AppCompatActivity { // Hidden check status button for testing if (checkStatusButton != null) { checkStatusButton.setOnClickListener(v -> { - Log.d("QrisResultFlow", "Check status button clicked"); - stopQrRefresh(); + Log.d("QrisResultFlow", "Manual payment simulation triggered"); + stopAllMonitoring(); simulateWebhook(); }); } @@ -686,7 +908,7 @@ public class QrisResultActivity extends AppCompatActivity { // Hidden return main button if (returnMainButton != null) { returnMainButton.setOnClickListener(v -> { - stopQrRefresh(); + stopAllMonitoring(); Intent intent = new Intent(QrisResultActivity.this, com.example.bdkipoc.MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); @@ -709,7 +931,7 @@ public class QrisResultActivity extends AppCompatActivity { // Double tap detected - simulate payment clickCount = 0; Log.d("QrisResultFlow", "Double tap detected - simulating payment"); - stopQrRefresh(); + stopAllMonitoring(); simulateWebhook(); } } @@ -742,7 +964,6 @@ public class QrisResultActivity extends AppCompatActivity { Bitmap bitmap = null; try { - // ✅ VALIDATE URL if (urlDisplay == null || urlDisplay.isEmpty()) { Log.e("QrisResultFlow", "❌ Empty QR URL provided"); errorMessage = "QR URL is empty"; @@ -760,12 +981,11 @@ public class QrisResultActivity extends AppCompatActivity { URL url = new URI(urlDisplay).toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setDoInput(true); - connection.setConnectTimeout(15000); // Increase timeout + connection.setConnectTimeout(15000); connection.setReadTimeout(15000); connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); connection.setRequestProperty("Accept", "image/*"); - // ✅ ADD AUTHORIZATION if needed for Midtrans QR if (urlDisplay.contains("midtrans.com")) { connection.setRequestProperty("Authorization", MIDTRANS_AUTH); } @@ -788,19 +1008,6 @@ public class QrisResultActivity extends AppCompatActivity { } } else { Log.e("QrisResultFlow", "❌ Failed to download image. HTTP code: " + responseCode); - - // ✅ READ ERROR RESPONSE - InputStream errorStream = connection.getErrorStream(); - if (errorStream != null) { - BufferedReader br = new BufferedReader(new InputStreamReader(errorStream)); - StringBuilder errorResponse = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - errorResponse.append(line); - } - Log.e("QrisResultFlow", "❌ Error response: " + errorResponse.toString()); - } - errorMessage = "Failed to download QR code (HTTP " + responseCode + ")"; } @@ -827,7 +1034,7 @@ public class QrisResultActivity extends AppCompatActivity { } } - // ✅ HELPER: Convert technical issuer name ke display name + // ✅ Helper: Convert technical issuer name to display name private String getDisplayName(String technicalName) { if (technicalName == null || technicalName.isEmpty()) { return "QRIS"; @@ -856,35 +1063,31 @@ public class QrisResultActivity extends AppCompatActivity { return fallbackName; } - // ✅ ENHANCED: Use actual issuer from Midtrans response + // ✅ ENHANCED: Sync transaction status with proper issuer data private void syncTransactionStatusToBackend(String finalStatus) { - Log.d("QrisResultFlow", "🔄 Syncing status '" + finalStatus + "' to backend for reference: " + referenceId); + Log.d("QrisResultFlow", "🔄 Syncing enhanced status '" + finalStatus + "' to backend"); new Thread(() -> { try { - // ✅ GET FINAL ISSUER AND ACQUIRER VALUES - String finalIssuer = actualIssuerFromMidtrans; - String finalAcquirer = actualAcquirerFromMidtrans; + // ✅ Get final issuer and acquirer values + String finalIssuer = actualIssuerFromMidtrans.isEmpty() ? + (acquirer != null ? acquirer : "qris") : actualIssuerFromMidtrans; + String finalAcquirer = actualAcquirerFromMidtrans.isEmpty() ? + (acquirer != null ? acquirer : "gopay") : actualAcquirerFromMidtrans; - // ✅ FALLBACK IF NOT SET - if (finalIssuer.isEmpty()) { - finalIssuer = acquirer != null ? acquirer : "qris"; - } - if (finalAcquirer.isEmpty()) { - finalAcquirer = acquirer != null ? acquirer : "gopay"; - } - - // ✅ USE MONITORING TRANSACTION FOR WEBHOOK (could be parent or QR refresh) + // ✅ Use monitoring transaction for webhook String webhookTransactionId = !currentQrTransactionId.isEmpty() ? currentQrTransactionId : transactionId; String transactionType = isMonitoringQrRefreshTransaction ? "QR refresh transaction" : "parent transaction"; - Log.d("QrisResultFlow", "🏷️ Final webhook values:"); + Log.d("QrisResultFlow", "🏷️ Enhanced webhook values:"); Log.d("QrisResultFlow", " Transaction ID: '" + webhookTransactionId + "' (" + transactionType + ")"); Log.d("QrisResultFlow", " Order ID: '" + orderId + "'"); Log.d("QrisResultFlow", " Issuer: '" + finalIssuer + "'"); Log.d("QrisResultFlow", " Acquirer: '" + finalAcquirer + "'"); + Log.d("QrisResultFlow", " Provider: '" + detectedProvider + "'"); + Log.d("QrisResultFlow", " Expiration: " + qrExpirationMinutes + " minutes"); - // ✅ FORMAT WEBHOOK MIDTRANS STANDARD + // ✅ Create enhanced webhook payload JSONObject payload = new JSONObject(); payload.put("status_code", "200"); payload.put("status_message", "Success, transaction is found"); @@ -903,28 +1106,35 @@ public class QrisResultActivity extends AppCompatActivity { payload.put("reference_id", referenceId); payload.put("shopeepay_reference_number", ""); - // ✅ ADD QR STRING if available + // ✅ Add enhanced fields + payload.put("detected_provider", detectedProvider); + payload.put("qr_expiration_minutes", qrExpirationMinutes); + payload.put("is_qr_refresh_transaction", isMonitoringQrRefreshTransaction); + + // ✅ Add QR string if available if (!currentQrString.isEmpty()) { payload.put("qr_string", currentQrString); - Log.d("QrisResultFlow", "📋 Added QR String to webhook payload"); + Log.d("QrisResultFlow", "📋 Added QR String to enhanced webhook payload"); } - // ✅ SIGNATURE untuk validasi + // ✅ Generate signature String serverKey = getServerKey(); String signature = generateSignature(orderId, "200", grossAmount != null ? grossAmount : String.valueOf(originalAmount), serverKey); payload.put("signature_key", signature); - Log.d("QrisResultFlow", "📤 Webhook payload: " + payload.toString()); + Log.d("QrisResultFlow", "📤 Enhanced webhook payload ready"); - // ✅ KIRIM KE WEBHOOK ENDPOINT + // ✅ Send to webhook endpoint String webhookUrl = backendBase + "/webhooks/midtrans"; URL url = new URI(webhookUrl).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Accept", "application/json"); - conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0-Enhanced"); + conn.setRequestProperty("X-Provider", detectedProvider); + conn.setRequestProperty("X-Expiration-Minutes", String.valueOf(qrExpirationMinutes)); conn.setDoOutput(true); conn.setConnectTimeout(15000); conn.setReadTimeout(15000); @@ -935,7 +1145,7 @@ public class QrisResultActivity extends AppCompatActivity { } int responseCode = conn.getResponseCode(); - Log.d("QrisResultFlow", "📥 Webhook response: " + responseCode); + Log.d("QrisResultFlow", "📥 Enhanced webhook response: " + responseCode); if (responseCode == 200 || responseCode == 201) { BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); @@ -944,7 +1154,7 @@ public class QrisResultActivity extends AppCompatActivity { while ((line = br.readLine()) != null) { response.append(line); } - Log.d("QrisResultFlow", "✅ Webhook successful: " + response.toString()); + Log.d("QrisResultFlow", "✅ Enhanced webhook successful: " + response.toString()); } else { InputStream errorStream = conn.getErrorStream(); if (errorStream != null) { @@ -954,12 +1164,12 @@ public class QrisResultActivity extends AppCompatActivity { while ((line = br.readLine()) != null) { errorResponse.append(line); } - Log.e("QrisResultFlow", "❌ Webhook failed: " + responseCode + " - " + errorResponse.toString()); + Log.e("QrisResultFlow", "❌ Enhanced webhook failed: " + responseCode + " - " + errorResponse.toString()); } } } catch (Exception e) { - Log.e("QrisResultFlow", "❌ Webhook error: " + e.getMessage(), e); + Log.e("QrisResultFlow", "❌ Enhanced webhook error: " + e.getMessage(), e); } }).start(); } @@ -969,14 +1179,11 @@ public class QrisResultActivity extends AppCompatActivity { .format(new java.util.Date()); } - // ✅ MODIFIED: Show full screen success instead of simple text display + // ✅ ENHANCED: Show payment success with proper cleanup private void showPaymentSuccess() { - Log.d("QrisResultFlow", "Showing payment success screen"); + Log.d("QrisResultFlow", "🎉 Showing enhanced payment success screen"); - stopQrRefresh(); - - // ✅ SYNC STATUS KE BACKEND - syncTransactionStatusToBackend("PAID"); + stopAllMonitoring(); // Stop all monitoring first // Show full screen success showSuccessScreen(); @@ -986,10 +1193,10 @@ public class QrisResultActivity extends AppCompatActivity { launchReceiptActivity(); }, 2500); - Toast.makeText(this, "Payment simulation completed successfully!", Toast.LENGTH_LONG).show(); + Toast.makeText(this, "Payment completed successfully! 🎉", Toast.LENGTH_LONG).show(); } - // ✅ NEW: Show full screen success overlay with animations + // ✅ ENHANCED: Show full screen success overlay with animations private void showSuccessScreen() { if (successScreen != null) { // Hide all main UI components first @@ -997,7 +1204,8 @@ public class QrisResultActivity extends AppCompatActivity { // Set success message if (successMessage != null) { - successMessage.setText("Pembayaran Berhasil"); + String providerName = getDisplayName(actualIssuerFromMidtrans.isEmpty() ? acquirer : actualIssuerFromMidtrans); + successMessage.setText("Pembayaran " + providerName + " Berhasil"); } // Show success screen with fade in animation @@ -1011,12 +1219,10 @@ public class QrisResultActivity extends AppCompatActivity { // Add scale and bounce animation to success icon if (successIcon != null) { - // Start with invisible icon successIcon.setScaleX(0f); successIcon.setScaleY(0f); successIcon.setAlpha(0f); - // Scale animation with bounce effect ObjectAnimator scaleX = ObjectAnimator.ofFloat(successIcon, "scaleX", 0f, 1.2f, 1f); ObjectAnimator scaleY = ObjectAnimator.ofFloat(successIcon, "scaleY", 0f, 1.2f, 1f); ObjectAnimator iconFadeIn = ObjectAnimator.ofFloat(successIcon, "alpha", 0f, 1f); @@ -1047,14 +1253,12 @@ public class QrisResultActivity extends AppCompatActivity { } } - // ✅ NEW: Hide main UI components for clean success screen + // ✅ Hide main UI components for clean success screen private void hideMainUIComponents() { - // Hide main content if (mainCard != null) { mainCard.setVisibility(View.GONE); } - // Hide header elements if (headerBackground != null) { headerBackground.setVisibility(View.GONE); } @@ -1062,7 +1266,6 @@ public class QrisResultActivity extends AppCompatActivity { backNavigation.setVisibility(View.GONE); } - // Hide cancel button if (cancelButton != null) { cancelButton.setVisibility(View.GONE); } @@ -1071,7 +1274,7 @@ public class QrisResultActivity extends AppCompatActivity { @Override protected void onDestroy() { super.onDestroy(); - stopQrRefresh(); + stopAllMonitoring(); if (animationHandler != null) { animationHandler.removeCallbacksAndMessages(null); } @@ -1084,31 +1287,31 @@ public class QrisResultActivity extends AppCompatActivity { return; } - stopQrRefresh(); + stopAllMonitoring(); finish(); super.onBackPressed(); } private void pollPendingPaymentLog(final String orderId) { - Log.d("QrisResultFlow", "Starting polling for orderId: " + orderId); + Log.d("QrisResultFlow", "Starting enhanced polling for orderId: " + orderId); new Thread(() -> { - int maxAttempts = 12; - int intervalMs = 2000; + int maxAttempts = 15; // Increased attempts + int intervalMs = 2000; // Check every 2 seconds int attempt = 0; boolean found = false; - while (attempt < maxAttempts && !found) { + while (attempt < maxAttempts && !found && !paymentProcessed) { try { String currentOrderId = this.orderId; String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + currentOrderId + "\"}"; - Log.d("QrisResultFlow", "Polling attempt " + (attempt + 1) + "/" + maxAttempts + " for parent order: " + currentOrderId); + Log.d("QrisResultFlow", "Enhanced polling attempt " + (attempt + 1) + "/" + maxAttempts + " for order: " + currentOrderId); URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Accept", "application/json"); - conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0-Enhanced"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); @@ -1140,24 +1343,27 @@ public class QrisResultActivity extends AppCompatActivity { ", transaction_status=" + transactionStatus); if (currentOrderId.equals(logOrderId) && - (transactionStatus.equals("pending") || - transactionStatus.equals("settlement") || - transactionStatus.equals("capture") || - transactionStatus.equals("success"))) { + (transactionStatus.equals("pending") || + transactionStatus.equals("settlement") || + transactionStatus.equals("capture") || + transactionStatus.equals("success"))) { found = true; if (transactionStatus.equals("settlement") || transactionStatus.equals("capture") || transactionStatus.equals("success")) { - Log.d("QrisResultFlow", "🎉 Payment already completed with status: " + transactionStatus); - - new Handler(Looper.getMainLooper()).post(() -> { - stopQrRefresh(); - showPaymentSuccess(); - Toast.makeText(QrisResultActivity.this, "Payment completed!", Toast.LENGTH_LONG).show(); - }); - return; + if (!paymentProcessed) { + paymentProcessed = true; + Log.d("QrisResultFlow", "🎉 Payment already completed via polling with status: " + transactionStatus); + + new Handler(Looper.getMainLooper()).post(() -> { + stopAllMonitoring(); + showPaymentSuccess(); + Toast.makeText(QrisResultActivity.this, "Payment completed!", Toast.LENGTH_LONG).show(); + }); + return; + } } Log.d("QrisResultFlow", "Found matching payment log with status: " + transactionStatus); @@ -1167,13 +1373,13 @@ public class QrisResultActivity extends AppCompatActivity { } } } else { - Log.w("QrisResultFlow", "Polling failed with HTTP code: " + responseCode); + Log.w("QrisResultFlow", "Enhanced polling failed with HTTP code: " + responseCode); } } catch (Exception e) { - Log.e("QrisResultFlow", "Polling error on attempt " + (attempt + 1) + ": " + e.getMessage()); + Log.e("QrisResultFlow", "Enhanced polling error on attempt " + (attempt + 1) + ": " + e.getMessage()); } - if (!found) { + if (!found && !paymentProcessed) { attempt++; if (attempt < maxAttempts) { try { @@ -1187,10 +1393,10 @@ public class QrisResultActivity extends AppCompatActivity { final boolean logFound = found; new Handler(Looper.getMainLooper()).post(() -> { - if (logFound && checkStatusButton != null) { + if (logFound && checkStatusButton != null && !paymentProcessed) { checkStatusButton.setEnabled(true); Toast.makeText(QrisResultActivity.this, "Payment log found!", Toast.LENGTH_SHORT).show(); - } else if (checkStatusButton != null) { + } else if (checkStatusButton != null && !paymentProcessed) { Toast.makeText(QrisResultActivity.this, "Payment log not found. Manual simulation available.", Toast.LENGTH_LONG).show(); checkStatusButton.setEnabled(true); } @@ -1228,146 +1434,33 @@ public class QrisResultActivity extends AppCompatActivity { } } - private void startContinuousPaymentMonitoring() { - Log.d("QrisResultFlow", "🔍 Starting continuous payment monitoring"); - - Handler paymentMonitorHandler = new Handler(Looper.getMainLooper()); - - Runnable paymentMonitorRunnable = new Runnable() { - @Override - public void run() { - checkCurrentPaymentStatus(); - - if (!isFinishing() && isQrRefreshActive) { - paymentMonitorHandler.postDelayed(this, 5000); - } - } - }; - - paymentMonitorHandler.post(paymentMonitorRunnable); - } - - private void checkCurrentPaymentStatus() { - new Thread(() -> { - try { - // ✅ CRITICAL: Monitor current QR transaction (could be parent or QR refresh) - String monitoringTransactionId = !currentQrTransactionId.isEmpty() ? currentQrTransactionId : transactionId; - String statusUrl = "https://api.sandbox.midtrans.com/v2/" + monitoringTransactionId + "/status"; - - String transactionType = isMonitoringQrRefreshTransaction ? "QR refresh transaction" : "parent transaction"; - Log.d("QrisResultFlow", "🔍 Checking payment status for " + transactionType + ": " + monitoringTransactionId); - - URL url = new URI(statusUrl).toURL(); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - conn.setRequestProperty("Accept", "application/json"); - conn.setRequestProperty("Authorization", MIDTRANS_AUTH); - conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); - conn.setConnectTimeout(10000); - conn.setReadTimeout(10000); - - int responseCode = conn.getResponseCode(); - - if (responseCode == 200) { - BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); - StringBuilder response = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - response.append(line); - } - - JSONObject statusResponse = new JSONObject(response.toString()); - String transactionStatus = statusResponse.optString("transaction_status", ""); - String paymentType = statusResponse.optString("payment_type", ""); - String grossAmount = statusResponse.optString("gross_amount", ""); - - // ✅ PROPERLY EXTRACT AND STORE ACTUAL ISSUER/ACQUIRER - String actualIssuer = statusResponse.optString("issuer", ""); - String actualAcquirer = statusResponse.optString("acquirer", ""); - - // ✅ UPDATE QR STRING if available in status response - String qrStringFromStatus = statusResponse.optString("qr_string", ""); - if (!qrStringFromStatus.isEmpty()) { - currentQrString = qrStringFromStatus; - Log.d("QrisResultFlow", "🔄 Updated QR String from status check"); - } - - Log.d("QrisResultFlow", "💳 Payment status check result (" + transactionType + "):"); - Log.d("QrisResultFlow", " Status: " + transactionStatus); - Log.d("QrisResultFlow", " Payment Type: " + paymentType); - Log.d("QrisResultFlow", " Amount: " + grossAmount); - Log.d("QrisResultFlow", " Actual Issuer: " + actualIssuer); - Log.d("QrisResultFlow", " Actual Acquirer: " + actualAcquirer); - Log.d("QrisResultFlow", " QR String Available: " + !qrStringFromStatus.isEmpty()); - - // ✅ CRITICAL FIX: Store the actual values from Midtrans - if (!actualIssuer.isEmpty() && !actualIssuer.equalsIgnoreCase("qris")) { - actualIssuerFromMidtrans = actualIssuer; - Log.d("QrisResultFlow", "✅ Updated issuer from Midtrans: " + actualIssuer); - } - - if (!actualAcquirer.isEmpty() && !actualAcquirer.equalsIgnoreCase("qris")) { - actualAcquirerFromMidtrans = actualAcquirer; - Log.d("QrisResultFlow", "✅ Updated acquirer from Midtrans: " + actualAcquirer); - } - - // Update backward compatibility variable - if (!actualIssuer.isEmpty()) { - acquirer = actualIssuer; - } - - if (transactionStatus.equals("settlement") || - transactionStatus.equals("capture") || - transactionStatus.equals("success")) { - - Log.d("QrisResultFlow", "🎉 Payment detected as PAID! Status: " + transactionStatus + " (" + transactionType + ")"); - - runOnUiThread(() -> { - stopQrRefresh(); - syncTransactionStatusToBackend("PAID"); - showPaymentSuccess(); - Toast.makeText(QrisResultActivity.this, "Payment Successful! 🎉", Toast.LENGTH_LONG).show(); - }); - - } else if (transactionStatus.equals("pending")) { - Log.d("QrisResultFlow", "⏳ Payment still pending (" + transactionType + ")"); - } else if (transactionStatus.equals("expire") || transactionStatus.equals("cancel")) { - Log.w("QrisResultFlow", "⚠️ Payment expired or cancelled: " + transactionStatus + " (" + transactionType + ")"); - syncTransactionStatusToBackend("FAILED"); - } else { - Log.d("QrisResultFlow", "📊 Payment status: " + transactionStatus + " (" + transactionType + ")"); - } - - } else { - Log.w("QrisResultFlow", "⚠️ Payment status check failed: HTTP " + responseCode + " (" + transactionType + ")"); - } - - } catch (Exception e) { - Log.e("QrisResultFlow", "❌ Payment status check error: " + e.getMessage(), e); - } - }).start(); - } - + // ✅ ENHANCED: Simulate webhook with proper validation private void simulateWebhook() { - Log.d("QrisResultFlow", "🚀 Starting webhook simulation"); + Log.d("QrisResultFlow", "🚀 Starting enhanced webhook simulation"); - stopQrRefresh(); + if (paymentProcessed) { + Log.w("QrisResultFlow", "⚠️ Payment already processed, skipping simulation"); + return; + } + + paymentProcessed = true; + stopAllMonitoring(); new Thread(() -> { try { String serverKey = getServerKey(); - // ✅ USE MONITORING TRANSACTION FOR SIMULATION + // ✅ Use monitoring transaction for simulation String currentOrderId = this.orderId; String currentTransactionId = !this.currentQrTransactionId.isEmpty() ? this.currentQrTransactionId : this.transactionId; String currentGrossAmount = this.grossAmount; String transactionType = isMonitoringQrRefreshTransaction ? "QR refresh transaction" : "parent transaction"; - Log.d("QrisResultFlow", "🚀 Simulating webhook for " + transactionType + ": " + currentTransactionId); + Log.d("QrisResultFlow", "🚀 Enhanced webhook simulation for " + transactionType + ": " + currentTransactionId); String signatureKey = generateSignature(currentOrderId, "200", currentGrossAmount, serverKey); - // ✅ GET ACTUAL ISSUER AND ACQUIRER + // ✅ Get actual issuer and acquirer String finalIssuer = actualIssuerFromMidtrans.isEmpty() ? (acquirer != null ? acquirer : "qris") : actualIssuerFromMidtrans; String finalAcquirer = actualAcquirerFromMidtrans.isEmpty() ? @@ -1393,17 +1486,25 @@ public class QrisResultActivity extends AppCompatActivity { payload.put("shopeepay_reference_number", ""); payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID"); - // ✅ ADD QR STRING to webhook if available + // ✅ Add enhanced simulation fields + payload.put("detected_provider", detectedProvider); + payload.put("qr_expiration_minutes", qrExpirationMinutes); + payload.put("is_simulation", true); + payload.put("simulation_type", "enhanced_manual"); + + // ✅ Add QR string to webhook simulation if (!currentQrString.isEmpty()) { payload.put("qr_string", currentQrString); - Log.d("QrisResultFlow", "📋 Added QR String to webhook simulation"); + Log.d("QrisResultFlow", "📋 Added QR String to enhanced webhook simulation"); } URL url = new URL(webhookUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); - conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0-Enhanced-Simulation"); + conn.setRequestProperty("X-Simulation", "true"); + conn.setRequestProperty("X-Provider", detectedProvider); conn.setDoOutput(true); conn.setConnectTimeout(15000); conn.setReadTimeout(15000); @@ -1414,7 +1515,7 @@ public class QrisResultActivity extends AppCompatActivity { os.close(); int responseCode = conn.getResponseCode(); - Log.d("QrisResultFlow", "📥 Webhook response code: " + responseCode); + Log.d("QrisResultFlow", "📥 Enhanced webhook simulation response code: " + responseCode); BufferedReader br = new BufferedReader(new InputStreamReader( responseCode < 400 ? conn.getInputStream() : conn.getErrorStream())); @@ -1424,13 +1525,13 @@ public class QrisResultActivity extends AppCompatActivity { response.append(line); } - Log.d("QrisResultFlow", "📥 Webhook response: " + + Log.d("QrisResultFlow", "📥 Enhanced webhook simulation response: " + (response.length() > 200 ? response.substring(0, 200) + "..." : response.toString())); Thread.sleep(2000); } catch (Exception e) { - Log.e("QrisResultFlow", "❌ Webhook simulation error: " + e.getMessage(), e); + Log.e("QrisResultFlow", "❌ Enhanced webhook simulation error: " + e.getMessage(), e); } new Handler(Looper.getMainLooper()).post(() -> { @@ -1439,23 +1540,26 @@ public class QrisResultActivity extends AppCompatActivity { }).start(); } + // ✅ ENHANCED: Launch receipt with comprehensive data private void launchReceiptActivity() { Intent intent = new Intent(this, ReceiptActivity.class); intent.putExtra("calling_activity", "QrisResultActivity"); - // ✅ GET FINAL ISSUER FOR RECEIPT + // ✅ Get final issuer for receipt String finalIssuer = actualIssuerFromMidtrans.isEmpty() ? (acquirer != null ? acquirer : "qris") : actualIssuerFromMidtrans; String displayCardType = getDisplayName(finalIssuer); - Log.d("QrisResultFlow", "Launching receipt with data:"); + Log.d("QrisResultFlow", "Launching enhanced receipt with data:"); Log.d("QrisResultFlow", " Reference ID: " + referenceId); Log.d("QrisResultFlow", " Transaction ID: " + transactionId); Log.d("QrisResultFlow", " Amount: " + originalAmount); Log.d("QrisResultFlow", " Actual Issuer: " + finalIssuer); Log.d("QrisResultFlow", " Display Card Type: " + displayCardType); + Log.d("QrisResultFlow", " Provider: " + detectedProvider); + Log.d("QrisResultFlow", " Expiration: " + qrExpirationMinutes + " minutes"); Log.d("QrisResultFlow", " QR String Available: " + !currentQrString.isEmpty()); intent.putExtra("transaction_id", transactionId); @@ -1475,7 +1579,12 @@ public class QrisResultActivity extends AppCompatActivity { intent.putExtra("mid", "71000026521"); intent.putExtra("tid", "73001500"); - // ✅ ADD QR STRING to receipt if available + // ✅ Add enhanced receipt data + intent.putExtra("detected_provider", detectedProvider); + intent.putExtra("qr_expiration_minutes", qrExpirationMinutes); + intent.putExtra("was_qr_refresh_transaction", isMonitoringQrRefreshTransaction); + + // ✅ Add QR string to receipt if (!currentQrString.isEmpty()) { intent.putExtra("qr_string", currentQrString); }