diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a6c6dc..d1ad159 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -59,8 +59,9 @@ android:name=".QrisActivity" android:exported="false" /> + - \ No newline at end of file + + diff --git a/app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java b/app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java index 997a236..bb12cf0 100644 --- a/app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java +++ b/app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java @@ -1,4 +1,5 @@ package com.example.bdkipoc; +import com.example.bdkipoc.qris.view.QrisResultActivity; import android.content.Context; import android.content.Intent; diff --git a/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java b/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java deleted file mode 100644 index f49d380..0000000 --- a/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java +++ /dev/null @@ -1,1604 +0,0 @@ -package com.example.bdkipoc; - -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.cardview.widget.CardView; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.text.NumberFormat; -import java.util.Locale; -import java.util.HashMap; -import java.util.Map; - -public class QrisResultActivity extends AppCompatActivity { - // Main UI Components - private ImageView qrImageView; - private TextView amountTextView; - private TextView timerTextView; - private Button cancelButton; - private TextView qrisLogo; - private CardView mainCard; - private View headerBackground; - private View backNavigation; - - // Hidden components for functionality - private TextView referenceTextView; - private TextView statusTextView; - private TextView qrStatusTextView; - private ProgressBar progressBar; - private Button downloadQrisButton; - private Button checkStatusButton; - private Button returnMainButton; - - // QR Management Components - private Handler qrRefreshHandler; - private Runnable qrRefreshRunnable; - private Handler paymentMonitorHandler; - private Runnable paymentMonitorRunnable; - private boolean isQrRefreshActive = true; - private boolean isPaymentMonitorActive = true; - - // Success screen views - private View successScreen; - private ImageView successIcon; - private TextView successMessage; - private TextView qrUrlTextView; - private Button simulatorButton; - - private String orderId; - private String grossAmount; - private String referenceId; - private String transactionId; - private String transactionTime; - private String acquirer; - private String merchantId; - private String currentQrImageUrl; - private int originalAmount; - - // ✅ QR Management Variables - private String currentQrString = ""; - private String qrStringFromMidtrans = ""; - private String actualIssuerFromMidtrans = ""; - private String actualAcquirerFromMidtrans = ""; - 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="; - private static final String MIDTRANS_AUTH = MIDTRANS_SANDBOX_AUTH; // Default to sandbox - private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.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() {{ - put("airpay shopee", "ShopeePay"); - put("shopeepay", "ShopeePay"); - put("shopee", "ShopeePay"); - put("linkaja", "LinkAja"); - put("link aja", "LinkAja"); - put("dana", "DANA"); - put("ovo", "OVO"); - put("gopay", "GoPay"); - put("jenius", "Jenius"); - put("sakuku", "Sakuku"); - put("bni", "BNI"); - put("bca", "BCA"); - put("mandiri", "Mandiri"); - put("bri", "BRI"); - put("cimb", "CIMB Niaga"); - put("permata", "Permata"); - put("maybank", "Maybank"); - put("qris", "QRIS"); - }}; - - // Animation handler - private Handler animationHandler = new Handler(Looper.getMainLooper()); - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_qris_result); - - // Initialize views - initializeViews(); - - // Get intent data - getIntentData(); - - // ✅ Initialize QR expiration tracking - initializeQrExpiration(); - - // ✅ Initialize transaction tracking - currentQrTransactionId = transactionId; - isMonitoringQrRefreshTransaction = false; - Log.d("QrisResultFlow", "🆔 Initial monitoring - parent transaction ID: " + transactionId); - - // Setup UI - setupUI(); - setupSuccessScreen(); - setupClickListeners(); - - // ✅ Start enhanced QR management - startEnhancedQrManagement(); - - // ✅ Start enhanced payment monitoring - startEnhancedPaymentMonitoring(); - - // Start polling for pending payment log - pollPendingPaymentLog(orderId); - } - - private void initializeViews() { - // Main visible components - qrImageView = findViewById(R.id.qrImageView); - amountTextView = findViewById(R.id.amountTextView); - timerTextView = findViewById(R.id.timerTextView); - cancelButton = findViewById(R.id.cancel_button); - qrisLogo = findViewById(R.id.qris_logo); - mainCard = findViewById(R.id.main_card); - headerBackground = findViewById(R.id.header_background); - backNavigation = findViewById(R.id.back_navigation); - - // Hidden components for functionality - referenceTextView = findViewById(R.id.referenceTextView); - statusTextView = findViewById(R.id.statusTextView); - qrStatusTextView = findViewById(R.id.qrStatusTextView); - progressBar = findViewById(R.id.progressBar); - downloadQrisButton = findViewById(R.id.downloadQrisButton); - checkStatusButton = findViewById(R.id.checkStatusButton); - returnMainButton = findViewById(R.id.returnMainButton); - - // Success screen views - 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 handlers - qrRefreshHandler = new Handler(Looper.getMainLooper()); - paymentMonitorHandler = new Handler(Looper.getMainLooper()); - } - - private void setupSuccessScreen() { - if (successScreen != null) { - successScreen.setVisibility(View.GONE); - } - } - - private void getIntentData() { - Intent intent = getIntent(); - currentQrImageUrl = intent.getStringExtra("qrImageUrl"); - originalAmount = intent.getIntExtra("amount", 0); - referenceId = intent.getStringExtra("referenceId"); - orderId = intent.getStringExtra("orderId"); - grossAmount = intent.getStringExtra("grossAmount"); - transactionId = intent.getStringExtra("transactionId"); - transactionTime = intent.getStringExtra("transactionTime"); - acquirer = intent.getStringExtra("acquirer"); - merchantId = intent.getStringExtra("merchantId"); - - // ✅ Get QR String from intent - qrStringFromMidtrans = intent.getStringExtra("qrString"); - if (qrStringFromMidtrans != null) { - currentQrString = qrStringFromMidtrans; - } - - 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: " + originalAmount); - Log.d("QrisResultFlow", "Acquirer: " + acquirer); - Log.d("QrisResultFlow", "Order ID: " + orderId); - Log.d("QrisResultFlow", "Transaction ID: " + transactionId); - } - - // ✅ 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() { - // Validate required data - if (orderId == null || transactionId == null) { - Log.e("QrisResultFlow", "Critical error: orderId or transactionId is null!"); - Toast.makeText(this, "Missing transaction details! Cannot proceed.", Toast.LENGTH_LONG).show(); - finish(); - return; - } - - // Display formatted amount - String formattedAmount = formatRupiahAmount(grossAmount != null ? grossAmount : String.valueOf(originalAmount)); - amountTextView.setText(formattedAmount); - - // Set reference data - if (referenceTextView != null) { - referenceTextView.setText("Reference ID: " + referenceId); - } - - // Load initial QR image - loadQrImage(currentQrImageUrl); - - // Set initial status - if (statusTextView != null) { - statusTextView.setText("Waiting for payment..."); - } - - if (checkStatusButton != null) { - checkStatusButton.setEnabled(false); - } - - if (currentQrImageUrl != null) { - qrUrlTextView.setText(currentQrImageUrl); - } - - setupUrlCopyFunctionality(); - setupSimulatorButton(); - - // ✅ Validate QR String - validateQrString(currentQrString); - } - - private String formatRupiahAmount(String amount) { - try { - String cleanAmount = amount.replaceAll("[^0-9]", ""); - long amountLong = Long.parseLong(cleanAmount); - return "RP." + String.format("%,d", amountLong).replace(',', '.'); - } catch (NumberFormatException e) { - Log.w("QrisResultFlow", "Error formatting rupiah amount: " + e.getMessage()); - return "RP." + amount; - } - } - - // ✅ 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"); - return; - } - - Log.d("QrisResultFlow", "🔍 Validating QR String..."); - Log.d("QrisResultFlow", "QR String length: " + qrString.length()); - - // 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"); - } - - // Check for required fields - if (qrString.contains("ID.CO.QRIS.WWW")) { - Log.d("QrisResultFlow", "✅ QR String contains QRIS Indonesia identifier"); - } - - if (qrString.contains("54")) { // Field 54 is transaction amount - Log.d("QrisResultFlow", "✅ QR String contains amount field"); - } - } - - // ✅ 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 || 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 - 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 - Log.d("QrisResultFlow", "🔄 QR Code refresh time reached"); - refreshQrCode(); - } - } - }; - - qrRefreshHandler.post(qrRefreshRunnable); - 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 || paymentProcessed) { - return; - } - - Log.d("QrisResultFlow", "🔄 Starting QR code refresh..."); - - runOnUiThread(() -> { - timerTextView.setText("Refreshing..."); - timerTextView.setTextColor(getResources().getColor(android.R.color.holo_orange_dark)); - }); - - new Thread(() -> { - try { - QrRefreshResult result = generateNewQrCode(); - - runOnUiThread(() -> { - if (result != null && result.qrUrl != null && !result.qrUrl.isEmpty()) { - // ✅ Successfully refreshed QR - currentQrImageUrl = result.qrUrl; - currentQrString = result.qrString; - - // ✅ 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); - - // ✅ 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); - } - - Log.d("QrisResultFlow", "✅ QR code refreshed successfully"); - Toast.makeText(QrisResultActivity.this, "QR Code refreshed", Toast.LENGTH_SHORT).show(); - - // Restart QR management with new expiration - startEnhancedQrManagement(); - - } else { - Log.e("QrisResultFlow", "❌ Failed to refresh QR code"); - 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); - } - }); - - } catch (Exception e) { - Log.e("QrisResultFlow", "❌ QR refresh error: " + e.getMessage(), e); - - runOnUiThread(() -> { - 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(); - } - - private void setupUrlCopyFunctionality() { - qrUrlTextView.setOnClickListener(v -> { - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("QR URL", currentQrImageUrl); - clipboard.setPrimaryClip(clip); - Toast.makeText(this, "URL copied to clipboard", Toast.LENGTH_SHORT).show(); - }); - } - - private void setupSimulatorButton() { - simulatorButton.setOnClickListener(v -> { - try { - String simulatorUrl = "https://simulator.sandbox.midtrans.com/v2/qris/index"; - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(simulatorUrl)); - startActivity(browserIntent); - } catch (Exception e) { - Toast.makeText(this, "Could not open browser", Toast.LENGTH_SHORT).show(); - Log.e("QrisResultFlow", "Error opening simulator URL", e); - } - }); - } - - // ✅ QR Refresh Result class - private static class QrRefreshResult { - String qrUrl; - String qrString; - String transactionId; - - QrRefreshResult(String qrUrl, String qrString, String transactionId) { - this.qrUrl = qrUrl; - this.qrString = qrString; - this.transactionId = transactionId; - } - } - - // ✅ ENHANCED: Generate new QR code with proper validation - private QrRefreshResult generateNewQrCode() { - try { - Log.d("QrisResultFlow", "🔧 Generating new QR code with enhanced validation"); - - // ✅ 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 - 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); - } - - // ✅ Create enhanced payload with expiration tracking - JSONObject customField1 = new JSONObject(); - customField1.put("parent_transaction_id", transactionId); - customField1.put("parent_order_id", orderId); - customField1.put("parent_reference_id", referenceId); - 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 - JSONObject payload = new JSONObject(); - payload.put("payment_type", "qris"); - - JSONObject transactionDetails = new JSONObject(); - transactionDetails.put("order_id", newOrderId); - transactionDetails.put("gross_amount", originalAmount); - payload.put("transaction_details", transactionDetails); - - JSONObject customerDetails = new JSONObject(); - customerDetails.put("first_name", "Test"); - customerDetails.put("last_name", "Customer"); - customerDetails.put("email", "test@example.com"); - customerDetails.put("phone", "081234567890"); - payload.put("customer_details", customerDetails); - - JSONArray itemDetails = new JSONArray(); - JSONObject item = new JSONObject(); - item.put("id", "item1_qr_refresh_" + System.currentTimeMillis()); - 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()) + - " (" + detectedProvider.toUpperCase() + " - " + qrExpirationMinutes + "min)"); - itemDetails.put(item); - payload.put("item_details", itemDetails); - - payload.put("custom_field1", customField1.toString()); - - JSONObject qrisDetails = new JSONObject(); - qrisDetails.put("acquirer", "gopay"); - 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", "📤 Enhanced QR refresh payload ready"); - - // ✅ Make API call to Midtrans - URL url = new URI(MIDTRANS_CHARGE_URL).toURL(); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("POST"); - conn.setRequestProperty("Accept", "application/json"); - 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-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); - - try (OutputStream os = conn.getOutputStream()) { - byte[] input = payload.toString().getBytes("utf-8"); - os.write(input, 0, input.length); - } - - int responseCode = conn.getResponseCode(); - Log.d("QrisResultFlow", "📥 Enhanced QR refresh response code: " + responseCode); - - if (responseCode == 200 || responseCode == 201) { - BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); - StringBuilder response = new StringBuilder(); - String responseLine; - while ((responseLine = br.readLine()) != null) { - response.append(responseLine.trim()); - } - - 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", "❌ Enhanced QR refresh failed: " + statusCode + " - " + statusMessage); - return null; - } - } - - // ✅ Extract QR URL and QR String - String newQrUrl = null; - String newQrString = null; - String newTransactionId = jsonResponse.optString("transaction_id", ""); - - // Get QR URL from actions - if (jsonResponse.has("actions")) { - JSONArray actionsArray = jsonResponse.getJSONArray("actions"); - if (actionsArray.length() > 0) { - JSONObject actions = actionsArray.getJSONObject(0); - newQrUrl = actions.getString("url"); - } - } - - // ✅ Get QR String - if (jsonResponse.has("qr_string")) { - newQrString = jsonResponse.getString("qr_string"); - Log.d("QrisResultFlow", "✅ Enhanced QR String obtained: " + (newQrString.length() > 50 ? newQrString.substring(0, 50) + "..." : newQrString)); - } else { - Log.w("QrisResultFlow", "⚠️ No QR String in enhanced response"); - } - - Log.d("QrisResultFlow", "✅ Enhanced QR refresh successful!"); - Log.d("QrisResultFlow", "🆕 New QR URL: " + newQrUrl); - Log.d("QrisResultFlow", "🆕 New QR Transaction ID: " + newTransactionId); - Log.d("QrisResultFlow", "🆕 Provider: " + detectedProvider + " (" + qrExpirationMinutes + " min expiration)"); - - return new QrRefreshResult(newQrUrl, newQrString, newTransactionId); - - } else { - InputStream errorStream = conn.getErrorStream(); - String errorResponse = ""; - - if (errorStream != null) { - BufferedReader br = new BufferedReader(new InputStreamReader(errorStream, "utf-8")); - StringBuilder errorBuilder = new StringBuilder(); - String responseLine; - while ((responseLine = br.readLine()) != null) { - errorBuilder.append(responseLine.trim()); - } - errorResponse = errorBuilder.toString(); - } - - Log.e("QrisResultFlow", "❌ Enhanced QR refresh HTTP error " + responseCode + ": " + errorResponse); - return null; - } - - } catch (Exception e) { - Log.e("QrisResultFlow", "❌ Enhanced QR refresh exception: " + e.getMessage(), e); - return null; - } - } - - // ✅ ENHANCED: Load QR image with validation - private void loadQrImage(String qrImageUrl) { - if (qrImageUrl != null && !qrImageUrl.isEmpty()) { - Log.d("QrisResultFlow", "🖼️ Loading QR image from: " + qrImageUrl); - - if (!qrImageUrl.startsWith("http")) { - Log.e("QrisResultFlow", "❌ Invalid QR URL format: " + qrImageUrl); - qrImageView.setVisibility(View.GONE); - Toast.makeText(this, "Invalid QR code URL format", Toast.LENGTH_SHORT).show(); - return; - } - - new EnhancedDownloadImageTask(qrImageView).execute(qrImageUrl); - } else { - Log.w("QrisResultFlow", "⚠️ QR image URL is not available"); - qrImageView.setVisibility(View.GONE); - Toast.makeText(this, "QR code URL not available", Toast.LENGTH_SHORT).show(); - } - } - - // ✅ NEW: Stop all monitoring activities - private void stopAllMonitoring() { - isQrRefreshActive = false; - isPaymentMonitorActive = false; - - if (qrRefreshHandler != null && qrRefreshRunnable != null) { - qrRefreshHandler.removeCallbacks(qrRefreshRunnable); - } - - if (paymentMonitorHandler != null && paymentMonitorRunnable != null) { - paymentMonitorHandler.removeCallbacks(paymentMonitorRunnable); - } - - Log.d("QrisResultFlow", "🛑 All monitoring stopped"); - } - - private void setupClickListeners() { - // Cancel button - cancelButton.setOnClickListener(v -> { - addClickAnimation(v); - stopAllMonitoring(); - finish(); - }); - - // Back navigation - if (backNavigation != null) { - backNavigation.setOnClickListener(v -> { - addClickAnimation(v); - stopAllMonitoring(); - finish(); - }); - } - - // Hidden check status button for testing - if (checkStatusButton != null) { - checkStatusButton.setOnClickListener(v -> { - Log.d("QrisResultFlow", "Manual payment simulation triggered"); - stopAllMonitoring(); - simulateWebhook(); - }); - } - - // Hidden return main button - if (returnMainButton != null) { - returnMainButton.setOnClickListener(v -> { - 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); - finishAffinity(); - }); - } - - // Enable testing via double tap on QR logo - qrisLogo.setOnClickListener(new View.OnClickListener() { - private int clickCount = 0; - private Handler handler = new Handler(); - private final int DOUBLE_TAP_TIMEOUT = 300; - - @Override - public void onClick(View v) { - clickCount++; - if (clickCount == 1) { - handler.postDelayed(() -> clickCount = 0, DOUBLE_TAP_TIMEOUT); - } else if (clickCount == 2) { - // Double tap detected - simulate payment - clickCount = 0; - Log.d("QrisResultFlow", "Double tap detected - simulating payment"); - stopAllMonitoring(); - simulateWebhook(); - } - } - }); - } - - // Animation methods - private void addClickAnimation(View view) { - ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f, 1f); - ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f, 1f); - - AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(scaleX, scaleY); - animatorSet.setDuration(150); - animatorSet.start(); - } - - // ✅ ENHANCED: Download Image Task with better validation - private static class EnhancedDownloadImageTask extends AsyncTask { - private ImageView bmImage; - private String errorMessage; - - EnhancedDownloadImageTask(ImageView bmImage) { - this.bmImage = bmImage; - } - - @Override - protected Bitmap doInBackground(String... urls) { - String urlDisplay = urls[0]; - Bitmap bitmap = null; - - try { - if (urlDisplay == null || urlDisplay.isEmpty()) { - Log.e("QrisResultFlow", "❌ Empty QR URL provided"); - errorMessage = "QR URL is empty"; - return null; - } - - if (!urlDisplay.startsWith("http")) { - Log.e("QrisResultFlow", "❌ Invalid QR URL format: " + urlDisplay); - errorMessage = "Invalid QR URL format"; - return null; - } - - Log.d("QrisResultFlow", "📥 Downloading image from: " + urlDisplay); - - URL url = new URI(urlDisplay).toURL(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setDoInput(true); - connection.setConnectTimeout(15000); - connection.setReadTimeout(15000); - connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); - connection.setRequestProperty("Accept", "image/*"); - - if (urlDisplay.contains("midtrans.com")) { - connection.setRequestProperty("Authorization", MIDTRANS_AUTH); - } - - connection.connect(); - - int responseCode = connection.getResponseCode(); - Log.d("QrisResultFlow", "📥 Image download response code: " + responseCode); - - if (responseCode == 200) { - InputStream input = connection.getInputStream(); - bitmap = BitmapFactory.decodeStream(input); - - if (bitmap != null) { - Log.d("QrisResultFlow", "✅ Image downloaded successfully. Size: " + - bitmap.getWidth() + "x" + bitmap.getHeight()); - } else { - Log.e("QrisResultFlow", "❌ Failed to decode bitmap from stream"); - errorMessage = "Failed to decode QR code image"; - } - } else { - Log.e("QrisResultFlow", "❌ Failed to download image. HTTP code: " + responseCode); - errorMessage = "Failed to download QR code (HTTP " + responseCode + ")"; - } - - } catch (Exception e) { - Log.e("QrisResultFlow", "❌ Exception downloading image: " + e.getMessage(), e); - errorMessage = "Error downloading QR code: " + e.getMessage(); - } - - return bitmap; - } - - @Override - protected void onPostExecute(Bitmap result) { - if (result != null) { - bmImage.setImageBitmap(result); - Log.d("QrisResultFlow", "✅ QR code image displayed successfully"); - } else { - Log.e("QrisResultFlow", "❌ Failed to display QR code image"); - bmImage.setImageResource(android.R.drawable.ic_menu_report_image); - if (errorMessage != null && bmImage.getContext() != null) { - Toast.makeText(bmImage.getContext(), "QR Error: " + errorMessage, Toast.LENGTH_LONG).show(); - } - } - } - } - - // ✅ Helper: Convert technical issuer name to display name - private String getDisplayName(String technicalName) { - if (technicalName == null || technicalName.isEmpty()) { - return "QRIS"; - } - - String lowerTechnicalName = technicalName.toLowerCase().trim(); - String displayName = ISSUER_DISPLAY_MAP.get(lowerTechnicalName); - - if (displayName != null) { - Log.d("QrisResultFlow", "🏷️ Mapped '" + technicalName + "' -> '" + displayName + "'"); - return displayName; - } - - // Fallback: capitalize first letter of each word - String[] words = technicalName.split("\\s+"); - StringBuilder result = new StringBuilder(); - for (String word : words) { - if (word.length() > 0) { - result.append(Character.toUpperCase(word.charAt(0))) - .append(word.substring(1).toLowerCase()) - .append(" "); - } - } - String fallbackName = result.toString().trim(); - Log.d("QrisResultFlow", "🏷️ No mapping found for '" + technicalName + "', using fallback: '" + fallbackName + "'"); - return fallbackName; - } - - // ✅ ENHANCED: Sync transaction status with proper issuer data - private void syncTransactionStatusToBackend(String finalStatus) { - Log.d("QrisResultFlow", "🔄 Syncing enhanced status '" + finalStatus + "' to backend"); - - new Thread(() -> { - try { - // ✅ Get final issuer and acquirer values - String finalIssuer = actualIssuerFromMidtrans.isEmpty() ? - (acquirer != null ? acquirer : "qris") : actualIssuerFromMidtrans; - String finalAcquirer = actualAcquirerFromMidtrans.isEmpty() ? - (acquirer != null ? acquirer : "gopay") : actualAcquirerFromMidtrans; - - // ✅ Use monitoring transaction for webhook - String webhookTransactionId = !currentQrTransactionId.isEmpty() ? currentQrTransactionId : transactionId; - String transactionType = isMonitoringQrRefreshTransaction ? "QR refresh transaction" : "parent transaction"; - - 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"); - - // ✅ Create enhanced webhook payload - JSONObject payload = new JSONObject(); - payload.put("status_code", "200"); - payload.put("status_message", "Success, transaction is found"); - payload.put("transaction_id", webhookTransactionId); - payload.put("order_id", orderId); - payload.put("merchant_id", merchantId != null ? merchantId : "G616299250"); - payload.put("gross_amount", grossAmount != null ? grossAmount : String.valueOf(originalAmount)); - payload.put("currency", "IDR"); - payload.put("payment_type", "qris"); - payload.put("transaction_time", transactionTime != null ? transactionTime : getCurrentDateTime()); - payload.put("transaction_status", finalStatus.equals("PAID") ? "settlement" : finalStatus.toLowerCase()); - payload.put("fraud_status", "accept"); - payload.put("acquirer", finalAcquirer); - payload.put("issuer", finalIssuer); - payload.put("settlement_time", getCurrentISOTime()); - payload.put("reference_id", referenceId); - payload.put("shopeepay_reference_number", ""); - - // ✅ 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 enhanced webhook payload"); - } - - // ✅ 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", "📤 Enhanced webhook payload ready"); - - // ✅ 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-Enhanced"); - conn.setRequestProperty("X-Provider", detectedProvider); - conn.setRequestProperty("X-Expiration-Minutes", String.valueOf(qrExpirationMinutes)); - conn.setDoOutput(true); - conn.setConnectTimeout(15000); - conn.setReadTimeout(15000); - - try (OutputStream os = conn.getOutputStream()) { - byte[] input = payload.toString().getBytes("utf-8"); - os.write(input, 0, input.length); - } - - int responseCode = conn.getResponseCode(); - Log.d("QrisResultFlow", "📥 Enhanced webhook response: " + responseCode); - - if (responseCode == 200 || responseCode == 201) { - BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); - StringBuilder response = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - response.append(line); - } - Log.d("QrisResultFlow", "✅ Enhanced webhook successful: " + response.toString()); - } else { - InputStream errorStream = conn.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", "❌ Enhanced webhook failed: " + responseCode + " - " + errorResponse.toString()); - } - } - - } catch (Exception e) { - Log.e("QrisResultFlow", "❌ Enhanced webhook error: " + e.getMessage(), e); - } - }).start(); - } - - private String getCurrentISOTime() { - return new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") - .format(new java.util.Date()); - } - - // ✅ ENHANCED: Show payment success with proper cleanup - private void showPaymentSuccess() { - Log.d("QrisResultFlow", "🎉 Showing enhanced payment success screen"); - - stopAllMonitoring(); // Stop all monitoring first - - // Show full screen success - showSuccessScreen(); - - // Navigate to receipt after delay - animationHandler.postDelayed(() -> { - launchReceiptActivity(); - }, 2500); - - Toast.makeText(this, "Payment completed successfully! 🎉", Toast.LENGTH_LONG).show(); - } - - // ✅ ENHANCED: Show full screen success overlay with animations - private void showSuccessScreen() { - if (successScreen != null) { - // Hide all main UI components first - hideMainUIComponents(); - - // Set success message - if (successMessage != null) { - String providerName = getDisplayName(actualIssuerFromMidtrans.isEmpty() ? acquirer : actualIssuerFromMidtrans); - successMessage.setText("Pembayaran " + providerName + " Berhasil"); - } - - // Show success screen with fade in animation - successScreen.setVisibility(View.VISIBLE); - successScreen.setAlpha(0f); - - // Fade in the background - ObjectAnimator backgroundFadeIn = ObjectAnimator.ofFloat(successScreen, "alpha", 0f, 1f); - backgroundFadeIn.setDuration(500); - backgroundFadeIn.start(); - - // Add scale and bounce animation to success icon - if (successIcon != null) { - successIcon.setScaleX(0f); - successIcon.setScaleY(0f); - successIcon.setAlpha(0f); - - 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); - - AnimatorSet iconAnimation = new AnimatorSet(); - iconAnimation.playTogether(scaleX, scaleY, iconFadeIn); - iconAnimation.setDuration(800); - iconAnimation.setStartDelay(300); - iconAnimation.setInterpolator(new android.view.animation.OvershootInterpolator(1.2f)); - iconAnimation.start(); - } - - // Add slide up animation to success message - if (successMessage != null) { - successMessage.setAlpha(0f); - successMessage.setTranslationY(50f); - - ObjectAnimator messageSlideUp = ObjectAnimator.ofFloat(successMessage, "translationY", 50f, 0f); - ObjectAnimator messageFadeIn = ObjectAnimator.ofFloat(successMessage, "alpha", 0f, 1f); - - AnimatorSet messageAnimation = new AnimatorSet(); - messageAnimation.playTogether(messageSlideUp, messageFadeIn); - messageAnimation.setDuration(600); - messageAnimation.setStartDelay(600); - messageAnimation.setInterpolator(new android.view.animation.DecelerateInterpolator()); - messageAnimation.start(); - } - } - } - - // ✅ Hide main UI components for clean success screen - private void hideMainUIComponents() { - if (mainCard != null) { - mainCard.setVisibility(View.GONE); - } - - if (headerBackground != null) { - headerBackground.setVisibility(View.GONE); - } - if (backNavigation != null) { - backNavigation.setVisibility(View.GONE); - } - - if (cancelButton != null) { - cancelButton.setVisibility(View.GONE); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - stopAllMonitoring(); - if (animationHandler != null) { - animationHandler.removeCallbacksAndMessages(null); - } - } - - @Override - public void onBackPressed() { - // Prevent back press when success screen is showing - if (successScreen != null && successScreen.getVisibility() == View.VISIBLE) { - return; - } - - stopAllMonitoring(); - finish(); - super.onBackPressed(); - } - - private void pollPendingPaymentLog(final String orderId) { - Log.d("QrisResultFlow", "Starting enhanced polling for orderId: " + orderId); - - new Thread(() -> { - int maxAttempts = 15; // Increased attempts - int intervalMs = 2000; // Check every 2 seconds - int attempt = 0; - boolean found = false; - - 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", "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-Enhanced"); - conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); - - 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 json = new JSONObject(response.toString()); - JSONArray results = json.optJSONArray("results"); - - if (results != null && results.length() > 0) { - Log.d("QrisResultFlow", "Found " + results.length() + " log entries"); - - for (int i = 0; i < results.length(); i++) { - JSONObject log = results.getJSONObject(i); - JSONObject reqBody = log.optJSONObject("request_body"); - - if (reqBody != null) { - String transactionStatus = reqBody.optString("transaction_status"); - String logOrderId = reqBody.optString("order_id"); - - Log.d("QrisResultFlow", "Log entry " + i + ": order_id=" + logOrderId + - ", transaction_status=" + transactionStatus); - - if (currentOrderId.equals(logOrderId) && - (transactionStatus.equals("pending") || - transactionStatus.equals("settlement") || - transactionStatus.equals("capture") || - transactionStatus.equals("success"))) { - found = true; - - if (transactionStatus.equals("settlement") || - transactionStatus.equals("capture") || - transactionStatus.equals("success")) { - - 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); - break; - } - } - } - } - } else { - Log.w("QrisResultFlow", "Enhanced polling failed with HTTP code: " + responseCode); - } - } catch (Exception e) { - Log.e("QrisResultFlow", "Enhanced polling error on attempt " + (attempt + 1) + ": " + e.getMessage()); - } - - if (!found && !paymentProcessed) { - attempt++; - if (attempt < maxAttempts) { - try { - Thread.sleep(intervalMs); - } catch (InterruptedException ignored) { - break; - } - } - } - } - - final boolean logFound = found; - new Handler(Looper.getMainLooper()).post(() -> { - if (logFound && checkStatusButton != null && !paymentProcessed) { - checkStatusButton.setEnabled(true); - Toast.makeText(QrisResultActivity.this, "Payment log found!", Toast.LENGTH_SHORT).show(); - } else if (checkStatusButton != null && !paymentProcessed) { - Toast.makeText(QrisResultActivity.this, "Payment log not found. Manual simulation available.", Toast.LENGTH_LONG).show(); - checkStatusButton.setEnabled(true); - } - }); - }).start(); - } - - private String getServerKey() { - try { - String base64 = MIDTRANS_AUTH.replace("Basic ", ""); - byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT); - String decodedString = new String(decoded); - return decodedString.replace(":", ""); - } catch (Exception e) { - Log.e("QrisResultFlow", "Error decoding server key: " + e.getMessage()); - return ""; - } - } - - private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) { - String input = orderId + statusCode + grossAmount + serverKey; - try { - java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-512"); - byte[] messageDigest = md.digest(input.getBytes()); - StringBuilder hexString = new StringBuilder(); - for (byte b : messageDigest) { - String hex = Integer.toHexString(0xff & b); - if (hex.length() == 1) hexString.append('0'); - hexString.append(hex); - } - return hexString.toString(); - } catch (java.security.NoSuchAlgorithmException e) { - Log.e("QrisResultFlow", "Error generating signature: " + e.getMessage()); - return "dummy_signature"; - } - } - - // ✅ ENHANCED: Simulate webhook with proper validation - private void simulateWebhook() { - Log.d("QrisResultFlow", "🚀 Starting enhanced webhook simulation"); - - 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 - 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", "🚀 Enhanced webhook simulation for " + transactionType + ": " + currentTransactionId); - - String signatureKey = generateSignature(currentOrderId, "200", currentGrossAmount, serverKey); - - // ✅ Get actual issuer and acquirer - String finalIssuer = actualIssuerFromMidtrans.isEmpty() ? - (acquirer != null ? acquirer : "qris") : actualIssuerFromMidtrans; - String finalAcquirer = actualAcquirerFromMidtrans.isEmpty() ? - (acquirer != null ? acquirer : "gopay") : actualAcquirerFromMidtrans; - - JSONObject payload = new JSONObject(); - payload.put("transaction_type", "on-us"); - payload.put("transaction_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z"); - payload.put("transaction_status", "settlement"); - payload.put("transaction_id", currentTransactionId); - payload.put("status_message", "midtrans payment notification"); - payload.put("status_code", "200"); - payload.put("signature_key", signatureKey); - payload.put("settlement_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z"); - payload.put("payment_type", "qris"); - payload.put("order_id", currentOrderId); - payload.put("merchant_id", merchantId != null ? merchantId : "G616299250"); - payload.put("issuer", finalIssuer); - payload.put("gross_amount", currentGrossAmount); - payload.put("fraud_status", "accept"); - payload.put("currency", "IDR"); - payload.put("acquirer", finalAcquirer); - payload.put("shopeepay_reference_number", ""); - payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID"); - - // ✅ 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 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-Enhanced-Simulation"); - conn.setRequestProperty("X-Simulation", "true"); - conn.setRequestProperty("X-Provider", detectedProvider); - conn.setDoOutput(true); - conn.setConnectTimeout(15000); - conn.setReadTimeout(15000); - - OutputStream os = conn.getOutputStream(); - os.write(payload.toString().getBytes()); - os.flush(); - os.close(); - - int responseCode = conn.getResponseCode(); - Log.d("QrisResultFlow", "📥 Enhanced webhook simulation response code: " + responseCode); - - BufferedReader br = new BufferedReader(new InputStreamReader( - responseCode < 400 ? conn.getInputStream() : conn.getErrorStream())); - StringBuilder response = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - response.append(line); - } - - 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", "❌ Enhanced webhook simulation error: " + e.getMessage(), e); - } - - new Handler(Looper.getMainLooper()).post(() -> { - showPaymentSuccess(); - }); - }).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 - String finalIssuer = actualIssuerFromMidtrans.isEmpty() ? - (acquirer != null ? acquirer : "qris") : actualIssuerFromMidtrans; - - String displayCardType = getDisplayName(finalIssuer); - - 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); - intent.putExtra("reference_id", referenceId); - intent.putExtra("order_id", orderId); - intent.putExtra("transaction_amount", String.valueOf(originalAmount)); - intent.putExtra("gross_amount", grossAmount != null ? grossAmount : String.valueOf(originalAmount)); - intent.putExtra("created_at", getReceiptDateTime()); - intent.putExtra("transaction_date", getReceiptDateTime()); - intent.putExtra("payment_method", "QRIS"); - intent.putExtra("channel_code", "QRIS"); - intent.putExtra("channel_category", "RETAIL_OUTLET"); - intent.putExtra("card_type", displayCardType); - intent.putExtra("merchant_name", "Marcel Panjaitan"); - intent.putExtra("merchant_location", "Jakarta, Indonesia"); - intent.putExtra("acquirer", finalIssuer); - intent.putExtra("mid", "71000026521"); - intent.putExtra("tid", "73001500"); - - // ✅ 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); - } - - startActivity(intent); - } - - private String getCurrentDateTime() { - java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("d/M/y H:m:s", new java.util.Locale("id", "ID")); - return sdf.format(new java.util.Date()); - } - - private String getReceiptDateTime() { - java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("d/M/y H:m:s", new java.util.Locale("id", "ID")); - return sdf.format(new java.util.Date()); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/qris/model/QrisRepository.java b/app/src/main/java/com/example/bdkipoc/qris/model/QrisRepository.java new file mode 100644 index 0000000..dffeb0c --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/model/QrisRepository.java @@ -0,0 +1,187 @@ +package com.example.bdkipoc.qris.model; + +import android.util.Log; +import com.example.bdkipoc.qris.network.QrisApiService; +import org.json.JSONObject; + +/** + * Repository class untuk menghandle semua data access terkait QRIS + * Mengabstraksi sumber data (API, local storage, etc.) + */ +public class QrisRepository { + + private static final String TAG = "QrisRepository"; + private QrisApiService apiService; + + // Singleton pattern + private static QrisRepository instance; + + private QrisRepository() { + this.apiService = new QrisApiService(); + } + + public static QrisRepository getInstance() { + if (instance == null) { + instance = new QrisRepository(); + } + return instance; + } + + /** + * Interface untuk callback hasil operasi + */ + public interface RepositoryCallback { + void onSuccess(T result); + void onError(String errorMessage); + } + + /** + * Refresh QR Code + */ + public void refreshQrCode(QrisTransaction transaction, RepositoryCallback callback) { + Log.d(TAG, "🔄 Refreshing QR code for transaction: " + transaction.getOrderId()); + + new Thread(() -> { + try { + QrRefreshResult result = apiService.generateNewQrCode(transaction); + + if (result != null && result.qrUrl != null && !result.qrUrl.isEmpty()) { + Log.d(TAG, "✅ QR refresh successful"); + callback.onSuccess(result); + } else { + Log.e(TAG, "❌ QR refresh failed - empty result"); + callback.onError("Failed to generate new QR code"); + } + + } catch (Exception e) { + Log.e(TAG, "❌ QR refresh exception: " + e.getMessage(), e); + callback.onError("QR refresh error: " + e.getMessage()); + } + }).start(); + } + + /** + * Check payment status + */ + public void checkPaymentStatus(QrisTransaction transaction, RepositoryCallback callback) { + Log.d(TAG, "🔍 Checking payment status for: " + transaction.getCurrentQrTransactionId()); + + new Thread(() -> { + try { + // Gunakan current transaction ID, bukan original + PaymentStatusResult result = apiService.checkTransactionStatus(transaction); + + if (result != null) { + Log.d(TAG, "✅ Payment status check successful: " + result.status); + + // Update transaction ID jika berbeda + if (result.transactionId != null && + !result.transactionId.equals(transaction.getCurrentQrTransactionId())) { + transaction.setCurrentQrTransactionId(result.transactionId); + } + + callback.onSuccess(result); + } else { + Log.w(TAG, "⚠️ Payment status check returned null"); + callback.onError("Failed to check payment status"); + } + + } catch (Exception e) { + Log.e(TAG, "❌ Payment status check exception: " + e.getMessage(), e); + callback.onError("Payment status error: " + e.getMessage()); + } + }).start(); + } + + /** + * Send webhook simulation + */ + public void simulatePayment(QrisTransaction transaction, RepositoryCallback callback) { + Log.d(TAG, "🚀 Simulating payment for: " + transaction.getOrderId()); + + new Thread(() -> { + try { + boolean success = apiService.simulateWebhook(transaction); + + if (success) { + Log.d(TAG, "✅ Payment simulation successful"); + callback.onSuccess(true); + } else { + Log.e(TAG, "❌ Payment simulation failed"); + callback.onError("Payment simulation failed"); + } + + } catch (Exception e) { + Log.e(TAG, "❌ Payment simulation exception: " + e.getMessage(), e); + callback.onError("Payment simulation error: " + e.getMessage()); + } + }).start(); + } + + /** + * Poll for payment logs + */ + public void pollPaymentLogs(String orderId, RepositoryCallback callback) { + Log.d(TAG, "📊 Polling payment logs for: " + orderId); + + new Thread(() -> { + try { + PaymentLogResult result = apiService.pollPendingPaymentLog(orderId); + + if (result != null) { + Log.d(TAG, "✅ Payment log polling successful"); + callback.onSuccess(result); + } else { + Log.w(TAG, "⚠️ No payment logs found"); + callback.onError("No payment logs found"); + } + + } catch (Exception e) { + Log.e(TAG, "❌ Payment log polling exception: " + e.getMessage(), e); + callback.onError("Payment log polling error: " + e.getMessage()); + } + }).start(); + } + + /** + * Result classes + */ + public static class QrRefreshResult { + public String qrUrl; + public String qrString; + public String transactionId; + + public QrRefreshResult(String qrUrl, String qrString, String transactionId) { + this.qrUrl = qrUrl; + this.qrString = qrString; + this.transactionId = transactionId; + } + } + + public static class PaymentStatusResult { + public String status; + public String paymentType; + public String issuer; + public String acquirer; + public String qrString; + public boolean statusChanged; + public String transactionId; + + public PaymentStatusResult(String status) { + this.status = status; + this.statusChanged = false; + } + } + + public static class PaymentLogResult { + public boolean found; + public String status; + public String orderId; + + public PaymentLogResult(boolean found, String status, String orderId) { + this.found = found; + this.status = status; + this.orderId = orderId; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/qris/model/QrisResponse.java b/app/src/main/java/com/example/bdkipoc/qris/model/QrisResponse.java new file mode 100644 index 0000000..3979e6d --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/model/QrisResponse.java @@ -0,0 +1,3 @@ +public class QrisResponse { + +} diff --git a/app/src/main/java/com/example/bdkipoc/qris/model/QrisTransaction.java b/app/src/main/java/com/example/bdkipoc/qris/model/QrisTransaction.java new file mode 100644 index 0000000..9396f88 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/model/QrisTransaction.java @@ -0,0 +1,288 @@ +package com.example.bdkipoc.qris.model; + +import android.util.Log; +import java.util.HashMap; +import java.util.Map; + +/** + * Model class untuk data transaksi QRIS + * Menampung semua data yang dibutuhkan untuk transaksi + */ +public class QrisTransaction { + private static final String TAG = "QrisTransaction"; + + // Transaction identifiers + private String orderId; + private String transactionId; + private String referenceId; + private String merchantId; + + // Amount information + private int originalAmount; + private String grossAmount; + private String formattedAmount; + + // QR Code information + private String qrImageUrl; + private String qrString; + private long qrCreationTime; + private int qrExpirationMinutes; + + // Provider information + private String acquirer; + private String detectedProvider; + private String actualIssuer; + private String actualAcquirer; + + // Transaction timing + private String transactionTime; + private long creationTimestamp; + + // Status tracking + private String currentStatus; + private boolean paymentProcessed; + private boolean isQrRefreshTransaction; + private String currentQrTransactionId; + + // Provider expiration mapping + 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); + }}; + + // Provider display name mapping + private static final Map ISSUER_DISPLAY_MAP = new HashMap() {{ + put("airpay shopee", "ShopeePay"); + put("shopeepay", "ShopeePay"); + put("shopee", "ShopeePay"); + put("linkaja", "LinkAja"); + put("link aja", "LinkAja"); + put("dana", "DANA"); + put("ovo", "OVO"); + put("gopay", "GoPay"); + put("jenius", "Jenius"); + put("sakuku", "Sakuku"); + put("bni", "BNI"); + put("bca", "BCA"); + put("mandiri", "Mandiri"); + put("bri", "BRI"); + put("cimb", "CIMB Niaga"); + put("permata", "Permata"); + put("maybank", "Maybank"); + put("qris", "QRIS"); + }}; + + // Constructor + public QrisTransaction() { + this.creationTimestamp = System.currentTimeMillis(); + this.currentStatus = "pending"; + this.paymentProcessed = false; + this.isQrRefreshTransaction = false; + } + + // Initialization method + public void initialize(String orderId, String transactionId, int amount, + String qrImageUrl, String qrString, String acquirer) { + this.orderId = orderId; + this.transactionId = transactionId; + this.currentQrTransactionId = transactionId; + this.originalAmount = amount; + this.qrImageUrl = qrImageUrl; + this.qrString = qrString; + this.acquirer = acquirer; + + // Detect provider and set expiration + this.detectedProvider = detectProviderFromData(); + this.qrExpirationMinutes = PROVIDER_EXPIRATION_MAP.get(detectedProvider.toLowerCase()); + this.qrCreationTime = System.currentTimeMillis(); + + // Format amount + this.formattedAmount = formatRupiahAmount(String.valueOf(amount)); + } + + /** + * Detect provider dari acquirer atau QR string + */ + 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)) { + return lowerAcquirer; + } + } + + // Try to detect from QR string content + if (qrString != null && !qrString.isEmpty()) { + String lowerQrString = qrString.toLowerCase(); + for (String provider : PROVIDER_EXPIRATION_MAP.keySet()) { + if (lowerQrString.contains(provider.toLowerCase())) { + return provider; + } + } + } + + return "others"; + } + + /** + * Format amount ke format Rupiah + */ + private String formatRupiahAmount(String amount) { + try { + String cleanAmount = amount.replaceAll("[^0-9]", ""); + long amountLong = Long.parseLong(cleanAmount); + return "RP." + String.format("%,d", amountLong).replace(',', '.'); + } catch (NumberFormatException e) { + return "RP." + amount; + } + } + + /** + * Check apakah QR sudah expired + */ + public boolean isQrExpired() { + long currentTime = System.currentTimeMillis(); + long elapsedMinutes = (currentTime - qrCreationTime) / (1000 * 60); + boolean expired = elapsedMinutes >= qrExpirationMinutes; + Log.d(TAG, "QR expired check: " + elapsedMinutes + "/" + qrExpirationMinutes + " = " + expired); + return expired; + } + + + /** + * Get remaining time dalam detik + */ + public int getRemainingTimeInSeconds() { + long currentTime = System.currentTimeMillis(); + long elapsedMs = currentTime - qrCreationTime; + long totalExpirationMs = qrExpirationMinutes * 60 * 1000; + long remainingMs = totalExpirationMs - elapsedMs; + + return (int) Math.max(0, remainingMs / 1000); + } + + /** + * Update QR code dengan data baru + */ + public void updateQrCode(String newQrUrl, String newQrString, String newTransactionId) { + this.qrImageUrl = newQrUrl; + this.qrString = newQrString; + this.qrCreationTime = System.currentTimeMillis(); + + if (newTransactionId != null && !newTransactionId.isEmpty()) { + this.currentQrTransactionId = newTransactionId; + this.isQrRefreshTransaction = true; + } + } + + /** + * Get display name untuk provider + */ + public String getDisplayProviderName() { + String issuerToCheck = actualIssuer != null && !actualIssuer.isEmpty() + ? actualIssuer : acquirer; + + if (issuerToCheck == null || issuerToCheck.isEmpty()) { + return "QRIS"; + } + + String lowerName = issuerToCheck.toLowerCase().trim(); + String displayName = ISSUER_DISPLAY_MAP.get(lowerName); + + if (displayName != null) { + return displayName; + } + + // Fallback: capitalize first letter + String[] words = issuerToCheck.split("\\s+"); + StringBuilder result = new StringBuilder(); + for (String word : words) { + if (word.length() > 0) { + result.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1).toLowerCase()) + .append(" "); + } + } + return result.toString().trim(); + } + + // Getters and Setters + public String getOrderId() { return orderId; } + public void setOrderId(String orderId) { this.orderId = orderId; } + + public String getTransactionId() { return transactionId; } + public void setTransactionId(String transactionId) { this.transactionId = transactionId; } + + public String getCurrentQrTransactionId() { return currentQrTransactionId; } + public void setCurrentQrTransactionId(String currentQrTransactionId) { + this.currentQrTransactionId = currentQrTransactionId; + } + + public String getReferenceId() { return referenceId; } + public void setReferenceId(String referenceId) { this.referenceId = referenceId; } + + public String getMerchantId() { return merchantId; } + public void setMerchantId(String merchantId) { this.merchantId = merchantId; } + + public int getOriginalAmount() { return originalAmount; } + public void setOriginalAmount(int originalAmount) { this.originalAmount = originalAmount; } + + public String getGrossAmount() { return grossAmount; } + public void setGrossAmount(String grossAmount) { this.grossAmount = grossAmount; } + + public String getFormattedAmount() { return formattedAmount; } + public void setFormattedAmount(String formattedAmount) { this.formattedAmount = formattedAmount; } + + public String getQrImageUrl() { return qrImageUrl; } + public void setQrImageUrl(String qrImageUrl) { this.qrImageUrl = qrImageUrl; } + + public String getQrString() { return qrString; } + public void setQrString(String qrString) { this.qrString = qrString; } + + public long getQrCreationTime() { return qrCreationTime; } + public void setQrCreationTime(long qrCreationTime) { this.qrCreationTime = qrCreationTime; } + + public int getQrExpirationMinutes() { return qrExpirationMinutes; } + public void setQrExpirationMinutes(int qrExpirationMinutes) { + this.qrExpirationMinutes = qrExpirationMinutes; + } + + public String getAcquirer() { return acquirer; } + public void setAcquirer(String acquirer) { this.acquirer = acquirer; } + + public String getDetectedProvider() { return detectedProvider; } + public void setDetectedProvider(String detectedProvider) { this.detectedProvider = detectedProvider; } + + public String getActualIssuer() { return actualIssuer; } + public void setActualIssuer(String actualIssuer) { this.actualIssuer = actualIssuer; } + + public String getActualAcquirer() { return actualAcquirer; } + public void setActualAcquirer(String actualAcquirer) { this.actualAcquirer = actualAcquirer; } + + public String getTransactionTime() { return transactionTime; } + public void setTransactionTime(String transactionTime) { this.transactionTime = transactionTime; } + + public long getCreationTimestamp() { return creationTimestamp; } + public void setCreationTimestamp(long creationTimestamp) { this.creationTimestamp = creationTimestamp; } + + public String getCurrentStatus() { return currentStatus; } + public void setCurrentStatus(String currentStatus) { this.currentStatus = currentStatus; } + + public boolean isPaymentProcessed() { return paymentProcessed; } + public void setPaymentProcessed(boolean paymentProcessed) { this.paymentProcessed = paymentProcessed; } + + public boolean isQrRefreshTransaction() { return isQrRefreshTransaction; } + public void setQrRefreshTransaction(boolean qrRefreshTransaction) { + this.isQrRefreshTransaction = qrRefreshTransaction; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/qris/network/MidtransApiClient.java b/app/src/main/java/com/example/bdkipoc/qris/network/MidtransApiClient.java new file mode 100644 index 0000000..be60cd4 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/network/MidtransApiClient.java @@ -0,0 +1,3 @@ +public class MidtransApiClient { + +} diff --git a/app/src/main/java/com/example/bdkipoc/qris/network/QrisApiService.java b/app/src/main/java/com/example/bdkipoc/qris/network/QrisApiService.java new file mode 100644 index 0000000..681fd88 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/network/QrisApiService.java @@ -0,0 +1,410 @@ +package com.example.bdkipoc.qris.network; + +import android.util.Log; +import com.example.bdkipoc.qris.model.QrisRepository; +import com.example.bdkipoc.qris.model.QrisTransaction; +import com.example.bdkipoc.qris.model.QrisRepository.PaymentStatusResult; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * API Service untuk handling semua network calls terkait QRIS + * Mengabstraksi implementasi detail dari repository + */ +public class QrisApiService { + + private static final String TAG = "QrisApiService"; + + // API Endpoints + private static final String MIDTRANS_SANDBOX_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc="; + private static final String MIDTRANS_PRODUCTION_AUTH = "TWlkLXNlcnZlci1sMlZPalotdVlVanpvNnU4VzAtYmF1a2o="; + private static final String MIDTRANS_AUTH = MIDTRANS_SANDBOX_AUTH; // Default to sandbox + private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge"; + private static final String MIDTRANS_STATUS_BASE_URL = "https://api.sandbox.midtrans.com/v2/"; + + private String backendBase = "https://be-edc.msvc.app"; + private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans"; + + /** + * Generate new QR code via Midtrans API + */ + public QrisRepository.QrRefreshResult generateNewQrCode(QrisTransaction transaction) throws Exception { + Log.d(TAG, "🔧 Generating new QR code for: " + transaction.getOrderId()); + + // Generate unique order ID untuk QR refresh + String shortTimestamp = String.valueOf(System.currentTimeMillis()).substring(7); + String newOrderId = transaction.getOrderId().substring(0, Math.min(transaction.getOrderId().length(), 43)) + "-q" + shortTimestamp; + + // Validate order ID length + if (newOrderId.length() > 50) { + newOrderId = transaction.getOrderId().substring(0, 36) + "-q" + shortTimestamp.substring(0, Math.min(shortTimestamp.length(), 7)); + } + + Log.d(TAG, "🆕 New QR Order ID: " + newOrderId); + + // Create enhanced payload + JSONObject payload = createQrRefreshPayload(transaction, newOrderId); + + // Make API call + URL url = new URI(MIDTRANS_CHARGE_URL).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Accept", "application/json"); + 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-Enhanced"); + conn.setRequestProperty("X-QR-Refresh", "true"); + conn.setRequestProperty("X-Parent-Transaction", transaction.getTransactionId()); + conn.setRequestProperty("X-Provider", transaction.getDetectedProvider()); + conn.setRequestProperty("X-Expiration-Minutes", String.valueOf(transaction.getQrExpirationMinutes())); + conn.setDoOutput(true); + conn.setConnectTimeout(30000); + conn.setReadTimeout(30000); + + try (OutputStream os = conn.getOutputStream()) { + byte[] input = payload.toString().getBytes("utf-8"); + os.write(input, 0, input.length); + } + + int responseCode = conn.getResponseCode(); + Log.d(TAG, "📥 QR refresh response code: " + responseCode); + + if (responseCode == 200 || responseCode == 201) { + String response = readResponse(conn.getInputStream()); + return parseQrRefreshResponse(response); + } else { + String errorResponse = readResponse(conn.getErrorStream()); + throw new Exception("QR refresh failed: HTTP " + responseCode + " - " + errorResponse); + } + } + + /** + * Check transaction status via Midtrans API + */ + public PaymentStatusResult checkTransactionStatus(QrisTransaction transaction) throws Exception { + String monitoringTransactionId = transaction.getCurrentQrTransactionId(); + String statusUrl = MIDTRANS_STATUS_BASE_URL + monitoringTransactionId + "/status"; + + Log.d(TAG, "🔍 Checking status for: " + 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) { + String response = readResponse(conn.getInputStream()); + PaymentStatusResult result = parseStatusResponse(response, transaction); + + JSONObject statusResponse = new JSONObject(response); + result.transactionId = statusResponse.optString("transaction_id", monitoringTransactionId); + + return result; + } else { + throw new Exception("Status check failed: HTTP " + responseCode); + } + } + + /** + * Simulate webhook payment + */ + public boolean simulateWebhook(QrisTransaction transaction) throws Exception { + Log.d(TAG, "🚀 Simulating webhook for: " + transaction.getOrderId()); + + String serverKey = getServerKey(); + String signatureKey = generateSignature( + transaction.getOrderId(), + "200", + transaction.getGrossAmount() != null ? transaction.getGrossAmount() : String.valueOf(transaction.getOriginalAmount()), + serverKey + ); + + JSONObject payload = createWebhookPayload(transaction, signatureKey); + + 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-Enhanced-Simulation"); + conn.setRequestProperty("X-Simulation", "true"); + conn.setRequestProperty("X-Provider", transaction.getDetectedProvider()); + conn.setDoOutput(true); + conn.setConnectTimeout(15000); + conn.setReadTimeout(15000); + + try (OutputStream os = conn.getOutputStream()) { + os.write(payload.toString().getBytes("utf-8")); + } + + int responseCode = conn.getResponseCode(); + Log.d(TAG, "📥 Webhook simulation response: " + responseCode); + + if (responseCode == 200 || responseCode == 201) { + String response = readResponse(conn.getInputStream()); + Log.d(TAG, "✅ Webhook simulation successful"); + return true; + } else { + String errorResponse = readResponse(conn.getErrorStream()); + throw new Exception("Webhook simulation failed: HTTP " + responseCode + " - " + errorResponse); + } + } + + /** + * Poll for pending payment logs + */ + public QrisRepository.PaymentLogResult pollPendingPaymentLog(String orderId) throws Exception { + Log.d(TAG, "📊 Polling payment logs for: " + orderId); + + String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}"; + + 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-Enhanced"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + int responseCode = conn.getResponseCode(); + + if (responseCode == 200) { + String response = readResponse(conn.getInputStream()); + return parsePaymentLogResponse(response, orderId); + } else { + throw new Exception("Payment log polling failed: HTTP " + responseCode); + } + } + + /** + * Helper methods + */ + + private JSONObject createQrRefreshPayload(QrisTransaction transaction, String newOrderId) throws Exception { + JSONObject customField1 = new JSONObject(); + customField1.put("parent_transaction_id", transaction.getTransactionId()); + customField1.put("parent_order_id", transaction.getOrderId()); + customField1.put("parent_reference_id", transaction.getReferenceId()); + customField1.put("qr_refresh_time", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()).format(new Date())); + customField1.put("qr_refresh_count", System.currentTimeMillis()); + customField1.put("is_qr_refresh", true); + customField1.put("detected_provider", transaction.getDetectedProvider()); + customField1.put("expiration_minutes", transaction.getQrExpirationMinutes()); + + JSONObject payload = new JSONObject(); + payload.put("payment_type", "qris"); + + JSONObject transactionDetails = new JSONObject(); + transactionDetails.put("order_id", newOrderId); + transactionDetails.put("gross_amount", transaction.getOriginalAmount()); + payload.put("transaction_details", transactionDetails); + + JSONObject customerDetails = new JSONObject(); + customerDetails.put("first_name", "Test"); + customerDetails.put("last_name", "Customer"); + customerDetails.put("email", "test@example.com"); + customerDetails.put("phone", "081234567890"); + payload.put("customer_details", customerDetails); + + JSONArray itemDetails = new JSONArray(); + JSONObject item = new JSONObject(); + item.put("id", "item1_qr_refresh_" + System.currentTimeMillis()); + item.put("price", transaction.getOriginalAmount()); + item.put("quantity", 1); + item.put("name", "QRIS Payment QR Refresh - " + new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(new Date()) + + " (" + transaction.getDetectedProvider().toUpperCase() + " - " + transaction.getQrExpirationMinutes() + "min)"); + itemDetails.put(item); + payload.put("item_details", itemDetails); + + payload.put("custom_field1", customField1.toString()); + + JSONObject qrisDetails = new JSONObject(); + qrisDetails.put("acquirer", "gopay"); + qrisDetails.put("qr_refresh", true); + qrisDetails.put("parent_transaction_id", transaction.getTransactionId()); + qrisDetails.put("refresh_timestamp", System.currentTimeMillis()); + qrisDetails.put("provider", transaction.getDetectedProvider()); + qrisDetails.put("expiration_minutes", transaction.getQrExpirationMinutes()); + payload.put("qris", qrisDetails); + + return payload; + } + + private JSONObject createWebhookPayload(QrisTransaction transaction, String signatureKey) throws Exception { + JSONObject payload = new JSONObject(); + payload.put("transaction_type", "on-us"); + payload.put("transaction_time", transaction.getTransactionTime() != null ? transaction.getTransactionTime() : getCurrentISOTime()); + payload.put("transaction_status", "settlement"); + payload.put("transaction_id", transaction.getCurrentQrTransactionId()); + payload.put("status_message", "midtrans payment notification"); + payload.put("status_code", "200"); + payload.put("signature_key", signatureKey); + payload.put("settlement_time", getCurrentISOTime()); + payload.put("payment_type", "qris"); + payload.put("order_id", transaction.getOrderId()); + payload.put("merchant_id", transaction.getMerchantId() != null ? transaction.getMerchantId() : "G616299250"); + payload.put("issuer", transaction.getActualIssuer() != null ? transaction.getActualIssuer() : transaction.getAcquirer()); + payload.put("gross_amount", transaction.getGrossAmount() != null ? transaction.getGrossAmount() : String.valueOf(transaction.getOriginalAmount())); + payload.put("fraud_status", "accept"); + payload.put("currency", "IDR"); + payload.put("acquirer", transaction.getActualAcquirer() != null ? transaction.getActualAcquirer() : transaction.getAcquirer()); + payload.put("shopeepay_reference_number", ""); + payload.put("reference_id", transaction.getReferenceId() != null ? transaction.getReferenceId() : "DUMMY_REFERENCE_ID"); + + // Enhanced fields + payload.put("detected_provider", transaction.getDetectedProvider()); + payload.put("qr_expiration_minutes", transaction.getQrExpirationMinutes()); + payload.put("is_simulation", true); + payload.put("simulation_type", "enhanced_manual"); + + if (transaction.getQrString() != null && !transaction.getQrString().isEmpty()) { + payload.put("qr_string", transaction.getQrString()); + } + + return payload; + } + + private QrisRepository.QrRefreshResult parseQrRefreshResponse(String response) throws Exception { + JSONObject jsonResponse = new JSONObject(response); + + if (jsonResponse.has("status_code")) { + String statusCode = jsonResponse.getString("status_code"); + if (!statusCode.equals("201")) { + String statusMessage = jsonResponse.optString("status_message", "Unknown error"); + throw new Exception("QR refresh failed: " + statusCode + " - " + statusMessage); + } + } + + String newQrUrl = null; + String newQrString = null; + String newTransactionId = jsonResponse.optString("transaction_id", ""); + + // Get QR URL from actions + if (jsonResponse.has("actions")) { + JSONArray actionsArray = jsonResponse.getJSONArray("actions"); + if (actionsArray.length() > 0) { + JSONObject actions = actionsArray.getJSONObject(0); + newQrUrl = actions.getString("url"); + } + } + + // Get QR String + if (jsonResponse.has("qr_string")) { + newQrString = jsonResponse.getString("qr_string"); + } + + return new QrisRepository.QrRefreshResult(newQrUrl, newQrString, newTransactionId); + } + + private QrisRepository.PaymentStatusResult parseStatusResponse(String response, QrisTransaction transaction) throws Exception { + JSONObject statusResponse = new JSONObject(response); + + String transactionStatus = statusResponse.optString("transaction_status", ""); + String paymentType = statusResponse.optString("payment_type", ""); + String actualIssuer = statusResponse.optString("issuer", ""); + String actualAcquirer = statusResponse.optString("acquirer", ""); + String qrStringFromStatus = statusResponse.optString("qr_string", ""); + + QrisRepository.PaymentStatusResult result = new QrisRepository.PaymentStatusResult(transactionStatus); + result.paymentType = paymentType; + result.issuer = actualIssuer; + result.acquirer = actualAcquirer; + result.qrString = qrStringFromStatus; + result.statusChanged = !transactionStatus.equals(transaction.getCurrentStatus()); + + return result; + } + + private QrisRepository.PaymentLogResult parsePaymentLogResponse(String response, String orderId) throws Exception { + JSONObject json = new JSONObject(response); + JSONArray results = json.optJSONArray("results"); + + if (results != null && results.length() > 0) { + for (int i = 0; i < results.length(); i++) { + JSONObject log = results.getJSONObject(i); + JSONObject reqBody = log.optJSONObject("request_body"); + + if (reqBody != null) { + String transactionStatus = reqBody.optString("transaction_status"); + String logOrderId = reqBody.optString("order_id"); + + if (orderId.equals(logOrderId) && + (transactionStatus.equals("pending") || + transactionStatus.equals("settlement") || + transactionStatus.equals("capture") || + transactionStatus.equals("success"))) { + + return new QrisRepository.PaymentLogResult(true, transactionStatus, logOrderId); + } + } + } + } + + return new QrisRepository.PaymentLogResult(false, "", orderId); + } + + private String readResponse(InputStream inputStream) throws Exception { + if (inputStream == null) return ""; + + BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8")); + StringBuilder response = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + return response.toString(); + } + + private String getServerKey() { + try { + String base64 = MIDTRANS_AUTH.replace("Basic ", ""); + byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT); + String decodedString = new String(decoded); + return decodedString.replace(":", ""); + } catch (Exception e) { + Log.e(TAG, "Error decoding server key: " + e.getMessage()); + return ""; + } + } + + private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) { + String input = orderId + statusCode + grossAmount + serverKey; + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + byte[] messageDigest = md.digest(input.getBytes()); + StringBuilder hexString = new StringBuilder(); + for (byte b : messageDigest) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return hexString.toString(); + } catch (Exception e) { + Log.e(TAG, "Error generating signature: " + e.getMessage()); + return "dummy_signature"; + } + } + + private String getCurrentISOTime() { + return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()) + .format(new Date()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/qris/presenter/QrisResultPresenter.java b/app/src/main/java/com/example/bdkipoc/qris/presenter/QrisResultPresenter.java new file mode 100644 index 0000000..5246575 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/presenter/QrisResultPresenter.java @@ -0,0 +1,543 @@ +package com.example.bdkipoc.qris.presenter; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import com.example.bdkipoc.qris.model.QrisRepository; +import com.example.bdkipoc.qris.model.QrisTransaction; +import com.example.bdkipoc.qris.view.QrisResultContract; + +/** + * Presenter untuk QrisResult module + * Menghandle semua business logic dan koordinasi antara Model dan View + */ +public class QrisResultPresenter implements QrisResultContract.Presenter { + + private static final String TAG = "QrisResultPresenter"; + + private QrisResultContract.View view; + private QrisRepository repository; + private QrisTransaction transaction; + + // Handlers untuk background tasks + private Handler timerHandler; + private Handler qrRefreshHandler; + private Handler paymentMonitorHandler; + + // Runnables untuk periodic tasks + private Runnable timerRunnable; + private Runnable qrRefreshRunnable; + private Runnable paymentMonitorRunnable; + + // State management + private boolean isTimerActive = false; + private boolean isQrRefreshActive = false; + private boolean isPaymentMonitorActive = false; + private String lastKnownStatus = "pending"; + private int refreshCounter = 0; + private static final int MAX_REFRESH_ATTEMPTS = 5; + + public QrisResultPresenter() { + this.repository = QrisRepository.getInstance(); + this.transaction = new QrisTransaction(); + + // Initialize handlers + this.timerHandler = new Handler(Looper.getMainLooper()); + this.qrRefreshHandler = new Handler(Looper.getMainLooper()); + this.paymentMonitorHandler = new Handler(Looper.getMainLooper()); + } + + @Override + public void attachView(QrisResultContract.View view) { + this.view = view; + Log.d(TAG, "📎 View attached to presenter"); + } + + @Override + public void detachView() { + this.view = null; + Log.d(TAG, "📎 View detached from presenter"); + } + + @Override + public void onDestroy() { + stopAllTimers(); + detachView(); + Log.d(TAG, "💀 Presenter destroyed"); + } + + @Override + public void initializeTransaction(String orderId, String transactionId, String amount, + String qrImageUrl, String qrString, String acquirer) { + Log.d(TAG, "🚀 Initializing transaction"); + + try { + int amountInt = Integer.parseInt(amount); + transaction.initialize(orderId, transactionId, amountInt, qrImageUrl, qrString, acquirer); + + if (view != null) { + view.showAmount(transaction.getFormattedAmount()); + view.showQrImage(transaction.getQrImageUrl()); + view.showProviderName(transaction.getDisplayProviderName()); + view.showStatus("Waiting for payment..."); + } + + Log.d(TAG, "✅ Transaction initialized successfully"); + Log.d(TAG, " Provider: " + transaction.getDetectedProvider()); + Log.d(TAG, " Expiration: " + transaction.getQrExpirationMinutes() + " minutes"); + + } catch (Exception e) { + Log.e(TAG, "❌ Failed to initialize transaction: " + e.getMessage(), e); + if (view != null) { + view.showError("Failed to initialize transaction: " + e.getMessage()); + } + } + } + + @Override + public void startQrManagement() { + Log.d(TAG, "🔄 Starting QR management"); + + isQrRefreshActive = true; + startTimer(); + startQrRefreshMonitoring(); + } + + @Override + public void stopQrManagement() { + Log.d(TAG, "🛑 Stopping QR management"); + + isQrRefreshActive = false; + stopTimer(); + + if (qrRefreshHandler != null && qrRefreshRunnable != null) { + qrRefreshHandler.removeCallbacks(qrRefreshRunnable); + } + } + + @Override + public void startTimer() { + if (isTimerActive) { + stopTimer(); + } + + Log.d(TAG, "⏰ Starting timer"); + isTimerActive = true; + + // Reset creation time + transaction.setQrCreationTime(System.currentTimeMillis()); + + timerRunnable = new Runnable() { + @Override + public void run() { + if (!isTimerActive || transaction.isPaymentProcessed()) { + return; + } + + int remainingSeconds = transaction.getRemainingTimeInSeconds(); + + if (remainingSeconds > 0) { + // Update UI di main thread + new Handler(Looper.getMainLooper()).post(() -> { + if (view != null) { + int displayMinutes = remainingSeconds / 60; + int displaySeconds = remainingSeconds % 60; + String timeDisplay = String.format("%d:%02d", displayMinutes, displaySeconds); + view.showTimer(timeDisplay); + } + }); + + // Schedule next update + timerHandler.postDelayed(this, 1000); + } else { + // Timer expired + Log.w(TAG, "⏰ Timer expired"); + isTimerActive = false; + onQrExpired(); + } + } + }; + + timerHandler.post(timerRunnable); + } + + @Override + public void stopTimer() { + Log.d(TAG, "⏰ Stopping timer"); + isTimerActive = false; + if (timerHandler != null && timerRunnable != null) { + timerHandler.removeCallbacks(timerRunnable); + } + } + + @Override + public void refreshQrCode() { + Log.d(TAG, "🔄 Refreshing QR code - Attempt " + refreshCounter); + + // Pastikan di Main Thread untuk UI updates + new Handler(Looper.getMainLooper()).post(() -> { + if (view != null) { + view.showQrRefreshing(); + view.showLoading(); + } + }); + + repository.refreshQrCode(transaction, new QrisRepository.RepositoryCallback() { + @Override + public void onSuccess(QrisRepository.QrRefreshResult result) { + Log.d(TAG, "✅ QR refresh successful"); + + // Update transaction data + transaction.updateQrCode(result.qrUrl, result.qrString, result.transactionId); + transaction.setQrCreationTime(System.currentTimeMillis()); // Reset creation time + + // Pastikan di Main Thread untuk UI updates + new Handler(Looper.getMainLooper()).post(() -> { + if (view != null) { + view.hideLoading(); + view.updateQrImage(result.qrUrl); + view.updateQrUrl(result.qrUrl); + view.showQrRefreshSuccess(); + view.showToast("QR Code berhasil diperbarui!"); + } + + // Stop dan restart timer dengan benar + stopTimer(); + startTimer(); + + // Restart monitoring + isQrRefreshActive = true; + startQrRefreshMonitoring(); + }); + } + + @Override + public void onError(String errorMessage) { + Log.e(TAG, "❌ QR refresh failed: " + errorMessage); + + new Handler(Looper.getMainLooper()).post(() -> { + if (view != null) { + view.hideLoading(); + view.showQrRefreshFailed(errorMessage); + + if (refreshCounter >= MAX_REFRESH_ATTEMPTS) { + view.navigateToMain(); + } + } + }); + } + }); + } + + @Override + public void onQrExpired() { + Log.w(TAG, "⏰ Handling QR expiration"); + + // Stop current timers to prevent race conditions + stopTimer(); + + if (view != null) { + view.showQrExpired(); + } + + // Cek apakah sudah mencapai limit refresh + if (refreshCounter >= MAX_REFRESH_ATTEMPTS) { + Log.w(TAG, "🛑 Maximum refresh attempts reached"); + if (view != null) { + view.showToast("Maksimum percobaan refresh QR tercapai"); + view.navigateToMain(); + } + return; + } + + // Increment counter + refreshCounter++; + Log.d(TAG, "🔄 Refresh attempt #" + refreshCounter); + + // Auto-refresh tanpa delay + refreshQrCode(); + } + + private void startQrRefreshMonitoring() { + Log.d(TAG, "🔄 Starting QR refresh monitoring"); + + qrRefreshRunnable = new Runnable() { + @Override + public void run() { + if (!isQrRefreshActive || transaction.isPaymentProcessed()) { + return; + } + + // Check if QR expired + if (transaction.isQrExpired()) { + Log.w(TAG, "⏰ QR Code expired during monitoring"); + onQrExpired(); + return; + } + + // Schedule next check in 30 seconds + if (isQrRefreshActive) { + qrRefreshHandler.postDelayed(this, 30000); + } + } + }; + + qrRefreshHandler.post(qrRefreshRunnable); + } + + @Override + public void startPaymentMonitoring() { + Log.d(TAG, "🔍 Starting payment monitoring"); + + isPaymentMonitorActive = true; + + paymentMonitorRunnable = new Runnable() { + @Override + public void run() { + if (!isPaymentMonitorActive || transaction.isPaymentProcessed()) { + return; + } + + checkPaymentStatus(); + + // Schedule next check in 3 seconds + if (isPaymentMonitorActive && !transaction.isPaymentProcessed()) { + paymentMonitorHandler.postDelayed(this, 3000); + } + } + }; + + paymentMonitorHandler.post(paymentMonitorRunnable); + } + + @Override + public void stopPaymentMonitoring() { + Log.d(TAG, "🔍 Stopping payment monitoring"); + + isPaymentMonitorActive = false; + if (paymentMonitorHandler != null && paymentMonitorRunnable != null) { + paymentMonitorHandler.removeCallbacks(paymentMonitorRunnable); + } + } + + @Override + public void checkPaymentStatus() { + repository.checkPaymentStatus(transaction, new QrisRepository.RepositoryCallback() { + @Override + public void onSuccess(QrisRepository.PaymentStatusResult result) { + handlePaymentStatusResult(result); + } + + @Override + public void onError(String errorMessage) { + Log.w(TAG, "⚠️ Payment status check failed: " + errorMessage); + // Don't show error to user untuk status check failures + } + }); + } + + private void handlePaymentStatusResult(QrisRepository.PaymentStatusResult result) { + Log.d(TAG, "💳 Payment status result: " + result.status); + + // Update transaction dengan actual issuer/acquirer + if (result.issuer != null && !result.issuer.isEmpty()) { + transaction.setActualIssuer(result.issuer); + } + + if (result.acquirer != null && !result.acquirer.isEmpty()) { + transaction.setActualAcquirer(result.acquirer); + } + + // Update QR string jika ada + if (result.qrString != null && !result.qrString.isEmpty()) { + transaction.setQrString(result.qrString); + } + + // Handle status changes + if (!result.status.equals(lastKnownStatus)) { + Log.d(TAG, "📊 Status changed: " + lastKnownStatus + " -> " + result.status); + lastKnownStatus = result.status; + transaction.setCurrentStatus(result.status); + + if (view != null) { + switch (result.status) { + case "settlement": + case "capture": + case "success": + if (!transaction.isPaymentProcessed()) { + handlePaymentSuccess(); + } + break; + + case "expire": + case "cancel": + view.showPaymentFailed("Payment " + result.status); + stopAllTimers(); + break; + + case "pending": + view.showPaymentPending(); + break; + + default: + view.showStatus("Status: " + result.status); + break; + } + } + } + } + + private void handlePaymentSuccess() { + Log.d(TAG, "🎉 Payment successful!"); + + transaction.setPaymentProcessed(true); + stopAllTimers(); + + if (view != null) { + String providerName = transaction.getDisplayProviderName(); + view.showPaymentSuccess(providerName); + view.startSuccessAnimation(); + view.showToast("Pembayaran " + providerName + " berhasil! 🎉"); + } + + // Don't auto-navigate here - let the view handle the navigation timing + } + + @Override + public void onCancelClicked() { + Log.d(TAG, "❌ Cancel clicked"); + + stopAllTimers(); + if (view != null) { + view.finishActivity(); + } + } + + @Override + public void onBackPressed() { + Log.d(TAG, "⬅️ Back pressed"); + + stopAllTimers(); + if (view != null) { + view.finishActivity(); + } + } + + @Override + public void onSimulatePayment() { + Log.d(TAG, "🚀 Simulating payment"); + + if (transaction.isPaymentProcessed()) { + Log.w(TAG, "⚠️ Payment already processed"); + if (view != null) { + view.showToast("Pembayaran sudah diproses"); + } + return; + } + + stopAllTimers(); + + if (view != null) { + view.showToast("Mensimulasikan pembayaran..."); + view.showLoading(); + } + + repository.simulatePayment(transaction, new QrisRepository.RepositoryCallback() { + @Override + public void onSuccess(Boolean result) { + Log.d(TAG, "✅ Payment simulation successful"); + + if (view != null) { + view.hideLoading(); + } + + // Wait a bit then trigger success + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (!transaction.isPaymentProcessed()) { + handlePaymentSuccess(); + } + }, 2000); + } + + @Override + public void onError(String errorMessage) { + Log.e(TAG, "❌ Payment simulation failed: " + errorMessage); + + if (view != null) { + view.hideLoading(); + view.showError("Simulasi gagal: " + errorMessage); + } + + // Restart monitoring after simulation failure + startPaymentMonitoring(); + } + }); + } + + /** + * Stop all timers dan background tasks + */ + private void stopAllTimers() { + Log.d(TAG, "🛑 Stopping all timers"); + + stopTimer(); + stopQrManagement(); + stopPaymentMonitoring(); + + // Clear all pending callbacks + if (timerHandler != null) { + timerHandler.removeCallbacksAndMessages(null); + } + if (qrRefreshHandler != null) { + qrRefreshHandler.removeCallbacksAndMessages(null); + } + if (paymentMonitorHandler != null) { + paymentMonitorHandler.removeCallbacksAndMessages(null); + } + } + + /** + * Public method untuk start semua monitoring + */ + public void startAllMonitoring() { + Log.d(TAG, "🚀 Starting all monitoring"); + + startQrManagement(); + startPaymentMonitoring(); + + // Start polling untuk payment logs + repository.pollPaymentLogs(transaction.getOrderId(), new QrisRepository.RepositoryCallback() { + @Override + public void onSuccess(QrisRepository.PaymentLogResult result) { + if (result.found) { + Log.d(TAG, "📊 Payment log found with status: " + result.status); + + if ("settlement".equals(result.status) || + "capture".equals(result.status) || + "success".equals(result.status)) { + + if (!transaction.isPaymentProcessed()) { + handlePaymentSuccess(); + } + } + + if (view != null) { + view.showToast("Payment log found!"); + } + } + } + + @Override + public void onError(String errorMessage) { + Log.w(TAG, "⚠️ Payment log polling failed: " + errorMessage); + // Don't show error to user + } + }); + } + + // Getter untuk transaction (untuk testing atau debugging) + public QrisTransaction getTransaction() { + return transaction; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/qris/utils/PaymentStatussMonitor.java b/app/src/main/java/com/example/bdkipoc/qris/utils/PaymentStatussMonitor.java new file mode 100644 index 0000000..1252ff2 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/utils/PaymentStatussMonitor.java @@ -0,0 +1,3 @@ +public class PaymentStatussMonitor { + +} diff --git a/app/src/main/java/com/example/bdkipoc/qris/utils/QrImageLoader.java b/app/src/main/java/com/example/bdkipoc/qris/utils/QrImageLoader.java new file mode 100644 index 0000000..c306a87 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/utils/QrImageLoader.java @@ -0,0 +1,162 @@ +package com.example.bdkipoc.qris.utils; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.AsyncTask; +import android.util.Log; +import android.widget.ImageView; +import android.widget.Toast; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; + +/** + * Utility class untuk loading QR images secara asynchronous + * Dengan error handling dan validation yang proper + */ +public class QrImageLoader { + + private static final String TAG = "QrImageLoader"; + private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc="; + + /** + * Interface untuk callback hasil loading image + */ + public interface ImageLoadCallback { + void onImageLoaded(Bitmap bitmap); + void onImageLoadFailed(String errorMessage); + } + + /** + * Load QR image dari URL dengan callback + */ + public static void loadQrImage(String qrImageUrl, ImageLoadCallback callback) { + if (qrImageUrl == null || qrImageUrl.isEmpty()) { + Log.w(TAG, "⚠️ QR image URL is empty"); + callback.onImageLoadFailed("QR image URL is empty"); + return; + } + + if (!qrImageUrl.startsWith("http")) { + Log.e(TAG, "❌ Invalid QR URL format: " + qrImageUrl); + callback.onImageLoadFailed("Invalid QR code URL format"); + return; + } + + Log.d(TAG, "🖼️ Loading QR image from: " + qrImageUrl); + new EnhancedDownloadImageTask(callback).execute(qrImageUrl); + } + + /** + * Load QR image langsung ke ImageView (legacy support) + */ + public static void loadQrImageToView(String qrImageUrl, ImageView imageView) { + loadQrImage(qrImageUrl, new ImageLoadCallback() { + @Override + public void onImageLoaded(Bitmap bitmap) { + if (imageView != null) { + imageView.setImageBitmap(bitmap); + Log.d(TAG, "✅ QR code image displayed successfully"); + } + } + + @Override + public void onImageLoadFailed(String errorMessage) { + Log.e(TAG, "❌ Failed to display QR code image: " + errorMessage); + if (imageView != null) { + imageView.setImageResource(android.R.drawable.ic_menu_report_image); + if (imageView.getContext() != null) { + Toast.makeText(imageView.getContext(), "QR Error: " + errorMessage, Toast.LENGTH_LONG).show(); + } + } + } + }); + } + + /** + * Enhanced AsyncTask untuk download image dengan proper error handling + */ + private static class EnhancedDownloadImageTask extends AsyncTask { + private ImageLoadCallback callback; + private String errorMessage; + + EnhancedDownloadImageTask(ImageLoadCallback callback) { + this.callback = callback; + } + + @Override + protected Bitmap doInBackground(String... urls) { + String urlDisplay = urls[0]; + Bitmap bitmap = null; + + try { + if (urlDisplay == null || urlDisplay.isEmpty()) { + Log.e(TAG, "❌ Empty QR URL provided"); + errorMessage = "QR URL is empty"; + return null; + } + + if (!urlDisplay.startsWith("http")) { + Log.e(TAG, "❌ Invalid QR URL format: " + urlDisplay); + errorMessage = "Invalid QR URL format"; + return null; + } + + Log.d(TAG, "📥 Downloading image from: " + urlDisplay); + + URL url = new URI(urlDisplay).toURL(); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoInput(true); + connection.setConnectTimeout(15000); + connection.setReadTimeout(15000); + connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + connection.setRequestProperty("Accept", "image/*"); + + // Add auth header untuk Midtrans URLs + if (urlDisplay.contains("midtrans.com")) { + connection.setRequestProperty("Authorization", MIDTRANS_AUTH); + } + + connection.connect(); + + int responseCode = connection.getResponseCode(); + Log.d(TAG, "📥 Image download response code: " + responseCode); + + if (responseCode == 200) { + InputStream input = connection.getInputStream(); + bitmap = BitmapFactory.decodeStream(input); + + if (bitmap != null) { + Log.d(TAG, "✅ Image downloaded successfully. Size: " + + bitmap.getWidth() + "x" + bitmap.getHeight()); + } else { + Log.e(TAG, "❌ Failed to decode bitmap from stream"); + errorMessage = "Failed to decode QR code image"; + } + } else { + Log.e(TAG, "❌ Failed to download image. HTTP code: " + responseCode); + errorMessage = "Failed to download QR code (HTTP " + responseCode + ")"; + } + + } catch (Exception e) { + Log.e(TAG, "❌ Exception downloading image: " + e.getMessage(), e); + errorMessage = "Error downloading QR code: " + e.getMessage(); + } + + return bitmap; + } + + @Override + protected void onPostExecute(Bitmap result) { + if (callback != null) { + if (result != null) { + callback.onImageLoaded(result); + } else { + callback.onImageLoadFailed(errorMessage != null ? errorMessage : "Unknown error"); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/qris/utils/QrisValidator.java b/app/src/main/java/com/example/bdkipoc/qris/utils/QrisValidator.java new file mode 100644 index 0000000..d73d02a --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/utils/QrisValidator.java @@ -0,0 +1,3 @@ +public class QrisValidator { + +} diff --git a/app/src/main/java/com/example/bdkipoc/qris/view/QrisResultActivity.java b/app/src/main/java/com/example/bdkipoc/qris/view/QrisResultActivity.java new file mode 100644 index 0000000..454085e --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/view/QrisResultActivity.java @@ -0,0 +1,757 @@ +package com.example.bdkipoc.qris.view; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.cardview.widget.CardView; + +import com.example.bdkipoc.R; +import com.example.bdkipoc.ReceiptActivity; +import com.example.bdkipoc.qris.model.QrisTransaction; +import com.example.bdkipoc.qris.presenter.QrisResultPresenter; +import com.example.bdkipoc.qris.utils.QrImageLoader; + +/** + * QrisResultActivity - refactored menggunakan MVP pattern + * Hanya menghandle UI logic, business logic ada di Presenter + */ +public class QrisResultActivity extends AppCompatActivity implements QrisResultContract.View { + + private static final String TAG = "QrisResultActivity"; + + // Presenter + private QrisResultPresenter presenter; + + // Main UI Components + private ImageView qrImageView; + private TextView amountTextView; + private TextView timerTextView; + private Button cancelButton; + private TextView qrisLogo; + private CardView mainCard; + private View headerBackground; + private View backNavigation; + + // Hidden components for functionality + private TextView referenceTextView; + private TextView statusTextView; + private TextView qrStatusTextView; + private ProgressBar progressBar; + private Button downloadQrisButton; + private Button checkStatusButton; + private Button returnMainButton; + + // Success screen views + private View successScreen; + private ImageView successIcon; + private TextView successMessage; + private TextView qrUrlTextView; + private Button simulatorButton; + + // Animation handler + private Handler animationHandler = new Handler(Looper.getMainLooper()); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_qris_result); + + Log.d(TAG, "=== QRIS RESULT ACTIVITY STARTED (MVP) ==="); + + // Initialize presenter + presenter = new QrisResultPresenter(); + presenter.attachView(this); + + // Initialize views + initializeViews(); + + // Setup UI components + setupUI(); + setupClickListeners(); + + // Get intent data dan initialize transaction + initializeFromIntent(); + + // Start monitoring + presenter.startAllMonitoring(); + } + + /** + * Initialize all view components + */ + private void initializeViews() { + // Main visible components + qrImageView = findViewById(R.id.qrImageView); + amountTextView = findViewById(R.id.amountTextView); + timerTextView = findViewById(R.id.timerTextView); + cancelButton = findViewById(R.id.cancel_button); + qrisLogo = findViewById(R.id.qris_logo); + mainCard = findViewById(R.id.main_card); + headerBackground = findViewById(R.id.header_background); + backNavigation = findViewById(R.id.back_navigation); + + // Hidden components for functionality + referenceTextView = findViewById(R.id.referenceTextView); + statusTextView = findViewById(R.id.statusTextView); + qrStatusTextView = findViewById(R.id.qrStatusTextView); + progressBar = findViewById(R.id.progressBar); + downloadQrisButton = findViewById(R.id.downloadQrisButton); + checkStatusButton = findViewById(R.id.checkStatusButton); + returnMainButton = findViewById(R.id.returnMainButton); + + // Success screen views + 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); + } + + /** + * Setup basic UI components + */ + private void setupUI() { + // Hide success screen initially + if (successScreen != null) { + successScreen.setVisibility(View.GONE); + } + + // Disable check status button initially + if (checkStatusButton != null) { + checkStatusButton.setEnabled(false); + } + + // Setup URL copy functionality + setupUrlCopyFunctionality(); + + // Setup simulator button + setupSimulatorButton(); + } + + /** + * Get data dari intent dan initialize transaction + */ + private void initializeFromIntent() { + Intent intent = getIntent(); + + String orderId = intent.getStringExtra("orderId"); + String transactionId = intent.getStringExtra("transactionId"); + String amount = String.valueOf(intent.getIntExtra("amount", 0)); + String qrImageUrl = intent.getStringExtra("qrImageUrl"); + String qrString = intent.getStringExtra("qrString"); + String acquirer = intent.getStringExtra("acquirer"); + + Log.d(TAG, "Initializing transaction with data:"); + Log.d(TAG, " Order ID: " + orderId); + Log.d(TAG, " Transaction ID: " + transactionId); + Log.d(TAG, " Amount: " + amount); + Log.d(TAG, " QR URL: " + qrImageUrl); + Log.d(TAG, " Acquirer: " + acquirer); + + // Validate required data + if (orderId == null || transactionId == null) { + Log.e(TAG, "❌ Critical error: orderId or transactionId is null!"); + showError("Missing transaction details! Cannot proceed."); + finish(); + return; + } + + // Initialize via presenter + presenter.initializeTransaction(orderId, transactionId, amount, qrImageUrl, qrString, acquirer); + + // Set additional data + if (referenceTextView != null) { + String referenceId = intent.getStringExtra("referenceId"); + referenceTextView.setText("Reference ID: " + referenceId); + } + } + + /** + * Setup click listeners untuk semua buttons dan views + */ + private void setupClickListeners() { + // Cancel button + if (cancelButton != null) { + cancelButton.setOnClickListener(v -> { + addClickAnimation(v); + presenter.onCancelClicked(); + }); + } + + // Back navigation + if (backNavigation != null) { + backNavigation.setOnClickListener(v -> { + addClickAnimation(v); + presenter.onBackPressed(); + }); + } + + // Hidden check status button untuk testing + if (checkStatusButton != null) { + checkStatusButton.setOnClickListener(v -> { + Log.d(TAG, "Manual payment simulation triggered"); + presenter.onSimulatePayment(); + }); + } + + // Hidden return main button + if (returnMainButton != null) { + returnMainButton.setOnClickListener(v -> { + navigateToMain(); + }); + } + + // Double tap pada QR logo untuk testing + if (qrisLogo != null) { + qrisLogo.setOnClickListener(new View.OnClickListener() { + private int clickCount = 0; + private Handler handler = new Handler(); + private final int DOUBLE_TAP_TIMEOUT = 300; + + @Override + public void onClick(View v) { + clickCount++; + if (clickCount == 1) { + handler.postDelayed(() -> clickCount = 0, DOUBLE_TAP_TIMEOUT); + } else if (clickCount == 2) { + // Double tap detected - simulate payment + clickCount = 0; + Log.d(TAG, "Double tap detected - simulating payment"); + presenter.onSimulatePayment(); + } + } + }); + } + } + + private void setupUrlCopyFunctionality() { + if (qrUrlTextView != null) { + qrUrlTextView.setOnClickListener(v -> { + if (presenter.getTransaction() != null) { + String qrUrl = presenter.getTransaction().getQrImageUrl(); + if (qrUrl != null) { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("QR URL", qrUrl); + clipboard.setPrimaryClip(clip); + showToast("URL copied to clipboard"); + } + } + }); + } + } + + private void setupSimulatorButton() { + if (simulatorButton != null) { + simulatorButton.setOnClickListener(v -> { + try { + String simulatorUrl = "https://simulator.sandbox.midtrans.com/v2/qris/index"; + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(simulatorUrl)); + startActivity(browserIntent); + } catch (Exception e) { + showToast("Could not open browser"); + Log.e(TAG, "Error opening simulator URL", e); + } + }); + } + } + + // ======================================================================================== + // MVP CONTRACT VIEW IMPLEMENTATIONS + // ======================================================================================== + + @Override + public void showQrImage(String qrImageUrl) { + Log.d(TAG, "🖼️ Showing QR image: " + qrImageUrl); + + if (qrImageUrl != null && !qrImageUrl.isEmpty()) { + QrImageLoader.loadQrImage(qrImageUrl, new QrImageLoader.ImageLoadCallback() { + @Override + public void onImageLoaded(Bitmap bitmap) { + if (qrImageView != null) { + qrImageView.setImageBitmap(bitmap); + qrImageView.setAlpha(1.0f); // Ensure fully visible + } + } + + @Override + public void onImageLoadFailed(String errorMessage) { + Log.e(TAG, "❌ Failed to load QR image: " + errorMessage); + if (qrImageView != null) { + qrImageView.setVisibility(View.GONE); + } + showError("Failed to load QR code: " + errorMessage); + } + }); + + // Update URL display + if (qrUrlTextView != null) { + qrUrlTextView.setText(qrImageUrl); + } + } else { + Log.w(TAG, "⚠️ QR image URL is not available"); + if (qrImageView != null) { + qrImageView.setVisibility(View.GONE); + } + showToast("QR code URL not available"); + } + } + + @Override + public void showAmount(String formattedAmount) { + Log.d(TAG, "💰 Showing amount: " + formattedAmount); + if (amountTextView != null) { + amountTextView.setText(formattedAmount); + } + } + + @Override + public void showTimer(String timeDisplay) { + if (timerTextView != null) { + timerTextView.setText(timeDisplay); + timerTextView.setTextColor(getResources().getColor(android.R.color.black)); + } + } + + @Override + public void showStatus(String status) { + Log.d(TAG, "📊 Showing status: " + status); + if (statusTextView != null) { + statusTextView.setText(status); + } + } + + @Override + public void showProviderName(String providerName) { + Log.d(TAG, "🏷️ Showing provider: " + providerName); + // Provider name bisa ditampilkan di UI jika ada komponen khusus + } + + @Override + public void updateQrImage(String newQrImageUrl) { + Log.d(TAG, "🔄 Updating QR image: " + newQrImageUrl); + + runOnUiThread(() -> { + // Reset QR image appearance first + if (qrImageView != null) { + qrImageView.setAlpha(1.0f); + qrImageView.setVisibility(View.VISIBLE); + } + + // Load new QR image + showQrImage(newQrImageUrl); + + // Update timer display + if (timerTextView != null) { + timerTextView.setTextColor(getResources().getColor(android.R.color.black)); + } + }); + } + + @Override + public void updateQrUrl(String newQrUrl) { + Log.d(TAG, "🔄 Updating QR URL: " + newQrUrl); + + runOnUiThread(() -> { + if (qrUrlTextView != null) { + qrUrlTextView.setText(newQrUrl); + qrUrlTextView.setVisibility(View.VISIBLE); + } + }); + } + + @Override + public void showQrExpired() { + Log.w(TAG, "⏰ Showing QR expired"); + + runOnUiThread(() -> { + // Make QR semi-transparent + if (qrImageView != null) { + qrImageView.setAlpha(0.5f); + } + + if (timerTextView != null) { + timerTextView.setText("EXPIRED"); + timerTextView.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + } + }); + } + + @Override + public void showQrRefreshing() { + Log.d(TAG, "🔄 Showing QR refreshing"); + + runOnUiThread(() -> { + if (timerTextView != null) { + timerTextView.setText("Refreshing..."); + timerTextView.setTextColor(getResources().getColor(android.R.color.holo_orange_dark)); + } + }); + } + + @Override + public void showQrRefreshFailed(String errorMessage) { + Log.e(TAG, "❌ QR refresh failed: " + errorMessage); + + runOnUiThread(() -> { + if (timerTextView != null) { + timerTextView.setText("Refresh Gagal"); + timerTextView.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + } + + // Tidak langsung navigate, biarkan presenter handle + showToast("Gagal refresh QR: " + errorMessage); + }); + } + + @Override + public void showQrRefreshSuccess() { + Log.d(TAG, "✅ QR refresh successful"); + + runOnUiThread(() -> { + // Reset QR image appearance + if (qrImageView != null) { + qrImageView.setAlpha(1.0f); + qrImageView.setVisibility(View.VISIBLE); + } + + // Reset timer color and show success message + if (timerTextView != null) { + timerTextView.setTextColor(getResources().getColor(android.R.color.black)); + } + + // Show success toast + showToast("QR Code berhasil diperbarui!"); + }); + } + + @Override + public void showPaymentSuccess(String providerName) { + Log.d(TAG, "🎉 Showing payment success for: " + providerName); + + runOnUiThread(() -> { + showFullScreenSuccess(providerName); + + // Navigate to receipt after 3 seconds, then to main activity + new Handler(Looper.getMainLooper()).postDelayed(() -> { + // Fixed: Remove the undefined 'view' variable and just check if activity is still valid + if (!isFinishing() && !isDestroyed()) { + navigateToReceipt(presenter.getTransaction()); + } + }, 3000); + }); + } + + @Override + public void showPaymentFailed(String reason) { + Log.w(TAG, "❌ Payment failed: " + reason); + showToast("Payment failed: " + reason); + } + + @Override + public void showPaymentPending() { + Log.d(TAG, "⏳ Payment pending"); + showStatus("Payment pending..."); + } + + @Override + public void showLoading() { + if (progressBar != null) { + progressBar.setVisibility(View.VISIBLE); + } + } + + @Override + public void hideLoading() { + if (progressBar != null) { + progressBar.setVisibility(View.GONE); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == 1001) { // Receipt activity result + Log.d(TAG, "📄 Receipt activity finished, navigating to main"); + navigateToMainWithTransactionComplete(); + } + } + + private void navigateToMainWithTransactionComplete() { + Intent intent = new Intent(this, com.example.bdkipoc.MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + // Add transaction completion data + intent.putExtra("transaction_completed", true); + if (presenter != null && presenter.getTransaction() != null) { + intent.putExtra("transaction_amount", String.valueOf(presenter.getTransaction().getOriginalAmount())); + intent.putExtra("payment_provider", presenter.getTransaction().getDisplayProviderName()); + } + + startActivity(intent); + finishAffinity(); // Clear all activities in the task + } + + @Override + public void navigateToReceipt(QrisTransaction transaction) { + Log.d(TAG, "📄 Navigating to receipt"); + + Intent intent = new Intent(this, ReceiptActivity.class); + + // Put transaction data + intent.putExtra("calling_activity", "QrisResultActivity"); + intent.putExtra("transaction_id", transaction.getTransactionId()); + intent.putExtra("reference_id", transaction.getReferenceId()); + intent.putExtra("order_id", transaction.getOrderId()); + intent.putExtra("transaction_amount", String.valueOf(transaction.getOriginalAmount())); + intent.putExtra("gross_amount", transaction.getGrossAmount() != null ? transaction.getGrossAmount() : String.valueOf(transaction.getOriginalAmount())); + intent.putExtra("created_at", getCurrentDateTime()); + intent.putExtra("transaction_date", getCurrentDateTime()); + intent.putExtra("payment_method", "QRIS"); + intent.putExtra("channel_code", "QRIS"); + intent.putExtra("channel_category", "RETAIL_OUTLET"); + intent.putExtra("card_type", transaction.getDisplayProviderName()); + intent.putExtra("merchant_name", "Marcel Panjaitan"); + intent.putExtra("merchant_location", "Jakarta, Indonesia"); + intent.putExtra("acquirer", transaction.getActualIssuer() != null ? transaction.getActualIssuer() : transaction.getAcquirer()); + intent.putExtra("mid", "71000026521"); + intent.putExtra("tid", "73001500"); + + // Enhanced data + intent.putExtra("detected_provider", transaction.getDetectedProvider()); + intent.putExtra("qr_expiration_minutes", transaction.getQrExpirationMinutes()); + intent.putExtra("was_qr_refresh_transaction", transaction.isQrRefreshTransaction()); + + // QR string + if (transaction.getQrString() != null && !transaction.getQrString().isEmpty()) { + intent.putExtra("qr_string", transaction.getQrString()); + } + + // Add flag to automatically return to main after receipt + intent.putExtra("auto_return_to_main", true); + + startActivityForResult(intent, 1001); + } + + @Override + public void navigateToMain() { + Intent intent = new Intent(this, com.example.bdkipoc.MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finishAffinity(); + } + + @Override + public void finishActivity() { + finish(); + } + + @Override + public void showToast(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + @Override + public void showError(String errorMessage) { + Log.e(TAG, "❌ Error: " + errorMessage); + Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show(); + } + + @Override + public void startSuccessAnimation() { + Log.d(TAG, "🎬 Starting success animation"); + // Animation akan di-handle oleh showFullScreenSuccess + } + + @Override + public void stopAllAnimations() { + Log.d(TAG, "🛑 Stopping all animations"); + if (animationHandler != null) { + animationHandler.removeCallbacksAndMessages(null); + } + } + + // ======================================================================================== + // PRIVATE HELPER METHODS + // ======================================================================================== + + /** + * Show full screen success dengan animations + */ + private void showFullScreenSuccess(String providerName) { + if (successScreen != null && !isFinishing()) { + // Hide main UI components + hideMainUIComponents(); + + // Set success message + if (successMessage != null) { + successMessage.setText("Pembayaran " + providerName + " Berhasil"); + } + + // Show success screen dengan fade in animation + successScreen.setVisibility(View.VISIBLE); + successScreen.setAlpha(0f); + + // Fade in background + ObjectAnimator backgroundFadeIn = ObjectAnimator.ofFloat(successScreen, "alpha", 0f, 1f); + backgroundFadeIn.setDuration(500); + backgroundFadeIn.start(); + + // Success icon animation + if (successIcon != null) { + successIcon.setScaleX(0f); + successIcon.setScaleY(0f); + successIcon.setAlpha(0f); + + 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); + + AnimatorSet iconAnimation = new AnimatorSet(); + iconAnimation.playTogether(scaleX, scaleY, iconFadeIn); + iconAnimation.setDuration(800); + iconAnimation.setStartDelay(300); + iconAnimation.setInterpolator(new android.view.animation.OvershootInterpolator(1.2f)); + iconAnimation.start(); + } + + // Success message animation + if (successMessage != null) { + successMessage.setAlpha(0f); + successMessage.setTranslationY(50f); + + ObjectAnimator messageSlideUp = ObjectAnimator.ofFloat(successMessage, "translationY", 50f, 0f); + ObjectAnimator messageFadeIn = ObjectAnimator.ofFloat(successMessage, "alpha", 0f, 1f); + + AnimatorSet messageAnimation = new AnimatorSet(); + messageAnimation.playTogether(messageSlideUp, messageFadeIn); + messageAnimation.setDuration(600); + messageAnimation.setStartDelay(600); + messageAnimation.setInterpolator(new android.view.animation.DecelerateInterpolator()); + messageAnimation.start(); + } + } + } + + + /** + * Hide main UI components untuk clean success screen + */ + private void hideMainUIComponents() { + if (mainCard != null) { + mainCard.setVisibility(View.GONE); + } + + if (headerBackground != null) { + headerBackground.setVisibility(View.GONE); + } + + if (backNavigation != null) { + backNavigation.setVisibility(View.GONE); + } + + if (cancelButton != null) { + cancelButton.setVisibility(View.GONE); + } + } + + /** + * Add click animation ke view + */ + private void addClickAnimation(View view) { + ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f, 1f); + ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f, 1f); + + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(scaleX, scaleY); + animatorSet.setDuration(150); + animatorSet.start(); + } + + /** + * Get current date time untuk receipt + */ + private String getCurrentDateTime() { + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("d/M/y H:m:s", new java.util.Locale("id", "ID")); + return sdf.format(new java.util.Date()); + } + + // ======================================================================================== + // LIFECYCLE METHODS + // ======================================================================================== + + @Override + protected void onDestroy() { + super.onDestroy(); + + // Cleanup presenter + if (presenter != null) { + presenter.onDestroy(); + } + + // Cleanup animation handler + if (animationHandler != null) { + animationHandler.removeCallbacksAndMessages(null); + } + + Log.d(TAG, "💀 QrisResultActivity destroyed"); + } + + @Override + protected void onPause() { + super.onPause(); + Log.d(TAG, "⏸️ QrisResultActivity paused"); + } + + @Override + protected void onResume() { + super.onResume(); + Log.d(TAG, "▶️ QrisResultActivity resumed"); + } + + @Override + public void onBackPressed() { + // Prevent back press during success screen animation + if (successScreen != null && successScreen.getVisibility() == View.VISIBLE) { + Log.d(TAG, "⬅️ Back press blocked during success screen"); + return; + } + + // Show confirmation dialog before leaving + androidx.appcompat.app.AlertDialog.Builder builder = new androidx.appcompat.app.AlertDialog.Builder(this); + builder.setTitle("Batalkan Transaksi"); + builder.setMessage("Apakah Anda yakin ingin membatalkan transaksi ini?"); + + builder.setPositiveButton("Ya, Batalkan", (dialog, which) -> { + if (presenter != null) { + presenter.onBackPressed(); + } else { + super.onBackPressed(); + } + }); + + builder.setNegativeButton("Tidak", (dialog, which) -> { + dialog.dismiss(); + }); + + builder.show(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/qris/view/QrisResultContract.java b/app/src/main/java/com/example/bdkipoc/qris/view/QrisResultContract.java new file mode 100644 index 0000000..8a65ef7 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/qris/view/QrisResultContract.java @@ -0,0 +1,86 @@ +package com.example.bdkipoc.qris.view; + +import com.example.bdkipoc.qris.model.QrisTransaction; + +/** + * Contract interface untuk QrisResult module + * Mendefinisikan komunikasi antara View dan Presenter + */ +public interface QrisResultContract { + + /** + * View interface - apa yang bisa dilakukan oleh View (Activity) + */ + interface View { + // UI Display methods + void showQrImage(String qrImageUrl); + void showAmount(String formattedAmount); + void showTimer(String timeDisplay); + void showStatus(String status); + void showProviderName(String providerName); + + // QR Management + void updateQrImage(String newQrImageUrl); + void updateQrUrl(String newQrUrl); + void showQrExpired(); + void showQrRefreshing(); + void showQrRefreshFailed(String errorMessage); + void showQrRefreshSuccess(); + + // Payment Status + void showPaymentSuccess(String providerName); + void showPaymentFailed(String reason); + void showPaymentPending(); + + // Loading states + void showLoading(); + void hideLoading(); + + // Navigation + void navigateToReceipt(QrisTransaction transaction); + void navigateToMain(); + void finishActivity(); + + // User feedback + void showToast(String message); + void showError(String errorMessage); + + // Animation + void startSuccessAnimation(); + void stopAllAnimations(); + } + + /** + * Presenter interface - apa yang bisa dilakukan oleh Presenter + */ + interface Presenter { + // Lifecycle + void attachView(View view); + void detachView(); + void onDestroy(); + + // Initialization + void initializeTransaction(String orderId, String transactionId, String amount, + String qrImageUrl, String qrString, String acquirer); + + // QR Management + void startQrManagement(); + void stopQrManagement(); + void refreshQrCode(); + void onQrExpired(); + + // Payment Monitoring + void startPaymentMonitoring(); + void stopPaymentMonitoring(); + void checkPaymentStatus(); + + // User Actions + void onCancelClicked(); + void onBackPressed(); + void onSimulatePayment(); // For testing + + // Timer + void startTimer(); + void stopTimer(); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_qris_result.xml b/app/src/main/res/layout/activity_qris_result.xml index e35e569..3aaa9b0 100644 --- a/app/src/main/res/layout/activity_qris_result.xml +++ b/app/src/main/res/layout/activity_qris_result.xml @@ -268,4 +268,4 @@ android:letterSpacing="0.02"/> - \ No newline at end of file +