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
+