diff --git a/app/src/main/java/com/example/bdkipoc/MainActivity.java b/app/src/main/java/com/example/bdkipoc/MainActivity.java index 24c457d..1a3db64 100644 --- a/app/src/main/java/com/example/bdkipoc/MainActivity.java +++ b/app/src/main/java/com/example/bdkipoc/MainActivity.java @@ -158,7 +158,7 @@ public class MainActivity extends AppCompatActivity { if (cardView != null) { cardView.setOnClickListener(v -> { if (cardId == R.id.card_kartu_kredit) { - startActivity(new Intent(MainActivity.this, CreditCardActivity.class)); + startActivity(new Intent(MainActivity.this, PaymentActivity.class)); } else if (cardId == R.id.card_kartu_debit) { startActivity(new Intent(MainActivity.this, PaymentActivity.class)); } else if (cardId == R.id.card_qris) { diff --git a/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java b/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java index 8d7e014..8b21143 100644 --- a/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java +++ b/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java @@ -1,5 +1,7 @@ package com.example.bdkipoc; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -17,7 +19,7 @@ import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; +import androidx.cardview.widget.CardView; import org.json.JSONArray; import org.json.JSONException; @@ -36,23 +38,36 @@ 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 TextView statusTextView; private Button returnMainButton; - private ProgressBar progressBar; // QR Refresh Components - private TextView timerTextView; - private TextView qrStatusTextView; private Handler qrRefreshHandler; private Runnable qrRefreshRunnable; private int countdownSeconds = 60; private boolean isQrRefreshActive = true; + // Success screen views + private View successScreen; + private ImageView successIcon; + private TextView successMessage; + private String orderId; private String grossAmount; private String referenceId; @@ -63,6 +78,10 @@ public class QrisResultActivity extends AppCompatActivity { private String currentQrImageUrl; private int originalAmount; + // ✅ ADD: QR String untuk validasi QRIS + private String currentQrString = ""; + private String qrStringFromMidtrans = ""; + // ✅ FIXED: Store actual issuer/acquirer from Midtrans response private String actualIssuerFromMidtrans = ""; private String actualAcquirerFromMidtrans = ""; @@ -100,6 +119,9 @@ public class QrisResultActivity extends AppCompatActivity { put("qris", "QRIS"); }}; + // Animation handler + private Handler animationHandler = new Handler(Looper.getMainLooper()); + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -120,6 +142,9 @@ public class QrisResultActivity extends AppCompatActivity { // Setup UI setupUI(); + // Setup success screen + setupSuccessScreen(); + // Start QR refresh timer startQrRefreshTimer(); @@ -128,24 +153,47 @@ public class QrisResultActivity extends AppCompatActivity { // Set up click listeners setupClickListeners(); + + // Start continuous payment monitoring + startContinuousPaymentMonitoring(); } 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); - statusTextView = findViewById(R.id.statusTextView); returnMainButton = findViewById(R.id.returnMainButton); - progressBar = findViewById(R.id.progressBar); - timerTextView = findViewById(R.id.timerTextView); - qrStatusTextView = findViewById(R.id.qrStatusTextView); + + // Success screen views + successScreen = findViewById(R.id.success_screen); + successIcon = findViewById(R.id.success_icon); + successMessage = findViewById(R.id.success_message); // Initialize handler for QR refresh qrRefreshHandler = new Handler(Looper.getMainLooper()); } + private void setupSuccessScreen() { + // Initially hide success screen + if (successScreen != null) { + successScreen.setVisibility(View.GONE); + } + } + private void getIntentData() { Intent intent = getIntent(); currentQrImageUrl = intent.getStringExtra("qrImageUrl"); @@ -157,10 +205,17 @@ public class QrisResultActivity extends AppCompatActivity { transactionTime = intent.getStringExtra("transactionTime"); acquirer = intent.getStringExtra("acquirer"); merchantId = intent.getStringExtra("merchantId"); + + // ✅ GET QR STRING from intent if available + qrStringFromMidtrans = intent.getStringExtra("qrString"); + if (qrStringFromMidtrans != null) { + currentQrString = qrStringFromMidtrans; + } // Enhanced logging for debugging Log.d("QrisResultFlow", "=== QRIS RESULT ACTIVITY STARTED ==="); Log.d("QrisResultFlow", "QR Image URL: " + currentQrImageUrl); + Log.d("QrisResultFlow", "QR String: " + (currentQrString.length() > 50 ? currentQrString.substring(0, 50) + "..." : currentQrString)); Log.d("QrisResultFlow", "Amount (int): " + originalAmount); Log.d("QrisResultFlow", "Gross Amount (string): " + grossAmount); Log.d("QrisResultFlow", "Reference ID: " + referenceId); @@ -178,24 +233,80 @@ public class QrisResultActivity extends AppCompatActivity { return; } - // Display formatted amount - String formattedAmount = formatCurrency(grossAmount != null ? grossAmount : String.valueOf(originalAmount)); + // Display formatted amount in rupiah format + String formattedAmount = formatRupiahAmount(grossAmount != null ? grossAmount : String.valueOf(originalAmount)); amountTextView.setText(formattedAmount); - referenceTextView.setText("Reference ID: " + referenceId); + + // Set reference data to hidden field + if (referenceTextView != null) { + referenceTextView.setText("Reference ID: " + referenceId); + } // Load initial QR image loadQrImage(currentQrImageUrl); - // Initialize UI state - checkStatusButton.setEnabled(false); - statusTextView.setText("Waiting for payment..."); + // Initialize timer display + timerTextView.setText("60"); - // Setup QR refresh status text - qrStatusTextView.setText("QR Code akan refresh dalam"); - qrStatusTextView.setVisibility(View.VISIBLE); - timerTextView.setVisibility(View.VISIBLE); + // Set initial status in hidden field + if (statusTextView != null) { + statusTextView.setText("Waiting for payment..."); + } + + // Enable simulate payment functionality + if (checkStatusButton != null) { + checkStatusButton.setEnabled(false); + } + + // ✅ VALIDATE QR STRING + validateQrString(currentQrString); + } - startContinuousPaymentMonitoring(); + private String formatRupiahAmount(String amount) { + try { + // Remove any existing currency symbols and formatting + String cleanAmount = amount.replaceAll("[^0-9]", ""); + long amountLong = Long.parseLong(cleanAmount); + + // Format with dots as thousand separators + return "RP." + String.format("%,d", amountLong).replace(',', '.'); + } catch (NumberFormatException e) { + Log.w("QrisResultFlow", "Error formatting rupiah amount: " + e.getMessage()); + return "RP." + amount; + } + } + + // ✅ NEW: Validate QR String format + 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()); + Log.d("QrisResultFlow", "QR String preview: " + (qrString.length() > 100 ? qrString.substring(0, 100) + "..." : qrString)); + + // ✅ BASIC QRIS FORMAT VALIDATION + if (qrString.startsWith("00020101") || qrString.startsWith("00020102")) { + Log.d("QrisResultFlow", "✅ QR String has valid QRIS header"); + } else { + Log.w("QrisResultFlow", "⚠️ QR String might not be valid QRIS format - missing standard header"); + } + + // ✅ CHECK FOR REQUIRED FIELDS + if (qrString.contains("ID.CO.QRIS.WWW")) { + Log.d("QrisResultFlow", "✅ QR String contains QRIS Indonesia identifier"); + } else { + Log.w("QrisResultFlow", "⚠️ QR String missing Indonesia QRIS identifier"); + } + + // ✅ CHECK FOR AMOUNT + if (qrString.contains("54")) { // Field 54 is transaction amount + Log.d("QrisResultFlow", "✅ QR String contains amount field"); + } else { + Log.w("QrisResultFlow", "⚠️ QR String missing amount field"); + } } private void startQrRefreshTimer() { @@ -225,7 +336,7 @@ public class QrisResultActivity extends AppCompatActivity { }; qrRefreshHandler.post(qrRefreshRunnable); - Log.d("QrisResultFlow", "🕒 QR refresh timer started - 60 seconds countdown (dynamic monitoring)"); + Log.d("QrisResultFlow", "🕒 QR refresh timer started - 60 seconds countdown"); } private void refreshQrCode() { @@ -233,40 +344,38 @@ public class QrisResultActivity extends AppCompatActivity { return; } - Log.d("QrisResultFlow", "🔄 Starting QR code refresh for parent transaction..."); + Log.d("QrisResultFlow", "🔄 Starting QR code refresh..."); // Show loading state timerTextView.setText("..."); - qrStatusTextView.setText("Refreshing QR Code (will create new QR transaction)..."); // Generate new QR code in background new Thread(() -> { try { - String newQrUrl = generateNewQrCode(); + QrRefreshResult result = generateNewQrCode(); runOnUiThread(() -> { - if (newQrUrl != null && !newQrUrl.isEmpty()) { - // ✅ Successfully refreshed QR - NOW MONITORING NEW QR TRANSACTION - currentQrImageUrl = newQrUrl; - loadQrImage(newQrUrl); + if (result != null && result.qrUrl != null && !result.qrUrl.isEmpty()) { + // ✅ Successfully refreshed QR + currentQrImageUrl = result.qrUrl; + currentQrString = result.qrString; // ✅ UPDATE QR STRING + loadQrImage(result.qrUrl); + + // ✅ VALIDATE NEW QR STRING + validateQrString(currentQrString); // ✅ IMPORTANT: Now monitoring QR refresh transaction for payment Log.d("QrisResultFlow", "🔄 QR refreshed - now monitoring new QR transaction"); Log.d("QrisResultFlow", "🔄 Parent transaction ID: " + transactionId + " (reference only)"); Log.d("QrisResultFlow", "🔄 Monitoring QR transaction ID: " + currentQrTransactionId); - // Update UI with refresh info - updateUIAfterRefresh(); - // Restart timer countdownSeconds = 60; - qrStatusTextView.setText("QR Code akan refresh dalam"); - Log.d("QrisResultFlow", "✅ QR code refreshed successfully - monitoring new QR transaction"); - Toast.makeText(QrisResultActivity.this, "QR refreshed - scan new QR for payment", Toast.LENGTH_SHORT).show(); + Log.d("QrisResultFlow", "✅ QR code refreshed successfully"); + Toast.makeText(QrisResultActivity.this, "QR Code refreshed with valid format", Toast.LENGTH_SHORT).show(); } else { // Failed to generate new QR Log.e("QrisResultFlow", "❌ Failed to refresh QR code"); - qrStatusTextView.setText("Failed to refresh QR - trying again in 30s"); countdownSeconds = 30; Toast.makeText(QrisResultActivity.this, "QR refresh failed, retrying...", Toast.LENGTH_SHORT).show(); } @@ -279,7 +388,6 @@ public class QrisResultActivity extends AppCompatActivity { Log.e("QrisResultFlow", "❌ QR refresh error: " + e.getMessage(), e); runOnUiThread(() -> { - qrStatusTextView.setText("QR refresh error - retrying in 30s"); countdownSeconds = 30; qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000); Toast.makeText(QrisResultActivity.this, "QR refresh error", Toast.LENGTH_SHORT).show(); @@ -288,21 +396,27 @@ public class QrisResultActivity extends AppCompatActivity { }).start(); } - private void updateUIAfterRefresh() { - String refreshTime = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date()); - String transactionType = isMonitoringQrRefreshTransaction ? "new QR" : "same payment"; - referenceTextView.setText("Reference ID: " + referenceId + " (QR refreshed at " + refreshTime + " - " + transactionType + ")"); - Log.d("QrisResultFlow", "🔄 UI updated after QR refresh - monitoring: " + currentQrTransactionId); + // ✅ NEW: 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; + } } - private String generateNewQrCode() { + // ✅ FIXED: Return both QR URL and QR String + private QrRefreshResult generateNewQrCode() { try { Log.d("QrisResultFlow", "🔧 Refreshing QR code for existing transaction"); Log.d("QrisResultFlow", "🔄 Parent Transaction ID: " + transactionId); Log.d("QrisResultFlow", "🔄 Parent Order ID: " + orderId); // ✅ GENERATE SHORT ORDER ID to avoid 50 character limit - // Get last part of timestamp (6 digits) to ensure uniqueness String shortTimestamp = String.valueOf(System.currentTimeMillis()).substring(7); String newOrderId = orderId.substring(0, Math.min(orderId.length(), 43)) + "-q" + shortTimestamp; @@ -310,7 +424,6 @@ public class QrisResultActivity extends AppCompatActivity { // ✅ VALIDATE ORDER ID LENGTH if (newOrderId.length() > 50) { - // Fallback: use first 36 chars of original + short suffix newOrderId = orderId.substring(0, 36) + "-q" + shortTimestamp.substring(0, Math.min(shortTimestamp.length(), 7)); Log.w("QrisResultFlow", "⚠️ Order ID too long, using fallback: " + newOrderId + " (Length: " + newOrderId.length() + ")"); } @@ -328,13 +441,11 @@ public class QrisResultActivity extends AppCompatActivity { JSONObject payload = new JSONObject(); payload.put("payment_type", "qris"); - // ✅ USE SHORT ORDER ID to avoid 50 character limit JSONObject transactionDetails = new JSONObject(); - transactionDetails.put("order_id", newOrderId); // ✅ SHORT ORDER ID + transactionDetails.put("order_id", newOrderId); transactionDetails.put("gross_amount", originalAmount); payload.put("transaction_details", transactionDetails); - // Add customer details (sama seperti sebelumnya) JSONObject customerDetails = new JSONObject(); customerDetails.put("first_name", "Test"); customerDetails.put("last_name", "Customer"); @@ -342,7 +453,6 @@ public class QrisResultActivity extends AppCompatActivity { customerDetails.put("phone", "081234567890"); payload.put("customer_details", customerDetails); - // ✅ UPDATE ITEM NAME UNTUK MENANDAKAN QR REFRESH JSONArray itemDetails = new JSONArray(); JSONObject item = new JSONObject(); item.put("id", "item1_qr_refresh_" + System.currentTimeMillis()); @@ -353,10 +463,8 @@ public class QrisResultActivity extends AppCompatActivity { itemDetails.put(item); payload.put("item_details", itemDetails); - // ✅ ADD CUSTOM FIELD UNTUK TRACKING PARENT TRANSACTION payload.put("custom_field1", customField1.toString()); - // ✅ ADD QR REFRESH INDICATOR JSONObject qrisDetails = new JSONObject(); qrisDetails.put("acquirer", "gopay"); qrisDetails.put("qr_refresh", true); @@ -376,7 +484,7 @@ public class QrisResultActivity extends AppCompatActivity { conn.setRequestProperty("X-Override-Notification", webhookUrl); conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0 QR-Refresh"); conn.setRequestProperty("X-QR-Refresh", "true"); - conn.setRequestProperty("X-Parent-Transaction", transactionId); // ✅ Header indicator + conn.setRequestProperty("X-Parent-Transaction", transactionId); conn.setDoOutput(true); conn.setConnectTimeout(30000); conn.setReadTimeout(30000); @@ -397,57 +505,54 @@ public class QrisResultActivity extends AppCompatActivity { response.append(responseLine.trim()); } + // ✅ LOG FULL RESPONSE FOR DEBUGGING + Log.d("QrisResultFlow", "📋 Full QR Refresh Response: " + response.toString()); + JSONObject jsonResponse = new JSONObject(response.toString()); - // ✅ CHECK FOR ERRORS IN RESPONSE if (jsonResponse.has("status_code")) { String statusCode = jsonResponse.getString("status_code"); if (!statusCode.equals("201")) { String statusMessage = jsonResponse.optString("status_message", "Unknown error"); Log.e("QrisResultFlow", "❌ QR refresh failed with status: " + statusCode + " - " + statusMessage); - - // ✅ DETAILED ERROR LOGGING - Log.e("QrisResultFlow", "❌ Failed payload was: " + payload.toString()); - Log.e("QrisResultFlow", "❌ Full response: " + response.toString()); return null; } } - // ✅ EXTRACT NEW QR URL + // ✅ 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); - String newQrUrl = actions.getString("url"); - - // ✅ GET NEW TRANSACTION INFO & STORE QR REFRESH TRANSACTION ID - String newTransactionId = jsonResponse.optString("transaction_id", ""); - String newOrderIdFromResponse = jsonResponse.optString("order_id", ""); - - // ✅ CRITICAL: Store QR refresh transaction ID for payment monitoring - if (!newTransactionId.isEmpty()) { - currentQrTransactionId = newTransactionId; - isMonitoringQrRefreshTransaction = true; - Log.d("QrisResultFlow", "🔄 Now monitoring QR refresh transaction: " + newTransactionId); - } - - Log.d("QrisResultFlow", "✅ QR refresh successful!"); - Log.d("QrisResultFlow", "🆕 New QR URL: " + newQrUrl); - Log.d("QrisResultFlow", "🆕 New QR Transaction ID: " + newTransactionId); - Log.d("QrisResultFlow", "🆕 New QR Order ID: " + newOrderIdFromResponse + " (Length: " + newOrderIdFromResponse.length() + ")"); - Log.d("QrisResultFlow", "🔗 Linked to Parent Transaction: " + transactionId); - Log.d("QrisResultFlow", "🔗 Linked to Parent Order: " + orderId); - - // ✅ IMPORTANT: Now monitor QR refresh transaction for payment - Log.d("QrisResultFlow", "📝 Payment monitoring switched to QR refresh transaction: " + newTransactionId); - - return newQrUrl; + newQrUrl = actions.getString("url"); } } - Log.e("QrisResultFlow", "❌ No actions found in refresh response"); - Log.e("QrisResultFlow", "❌ Full response: " + response.toString()); - return null; + // ✅ GET QR STRING (CRITICAL FOR QRIS VALIDATION) + if (jsonResponse.has("qr_string")) { + newQrString = jsonResponse.getString("qr_string"); + Log.d("QrisResultFlow", "✅ Found QR String in response: " + (newQrString.length() > 50 ? newQrString.substring(0, 50) + "..." : newQrString)); + } else { + Log.w("QrisResultFlow", "⚠️ No QR String found in response - QR might be unparsable"); + } + + if (!newTransactionId.isEmpty()) { + currentQrTransactionId = newTransactionId; + isMonitoringQrRefreshTransaction = true; + Log.d("QrisResultFlow", "🔄 Now monitoring QR refresh transaction: " + newTransactionId); + } + + Log.d("QrisResultFlow", "✅ QR refresh successful!"); + Log.d("QrisResultFlow", "🆕 New QR URL: " + newQrUrl); + Log.d("QrisResultFlow", "🆕 New QR String Length: " + (newQrString != null ? newQrString.length() : "null")); + Log.d("QrisResultFlow", "🆕 New QR Transaction ID: " + newTransactionId); + + return new QrRefreshResult(newQrUrl, newQrString, newTransactionId); } else { InputStream errorStream = conn.getErrorStream(); @@ -464,7 +569,6 @@ public class QrisResultActivity extends AppCompatActivity { } Log.e("QrisResultFlow", "❌ QR refresh HTTP error " + responseCode + ": " + errorResponse); - Log.e("QrisResultFlow", "❌ Request payload was: " + payload.toString()); return null; } @@ -474,14 +578,24 @@ public class QrisResultActivity extends AppCompatActivity { } } + // ✅ ENHANCED: Load QR image with better error handling private void loadQrImage(String qrImageUrl) { if (qrImageUrl != null && !qrImageUrl.isEmpty()) { Log.d("QrisResultFlow", "🖼️ Loading QR image from: " + qrImageUrl); - new DownloadImageTask(qrImageView).execute(qrImageUrl); + + // ✅ VALIDATE URL FORMAT + 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); - downloadQrisButton.setEnabled(false); + Toast.makeText(this, "QR code URL not available", Toast.LENGTH_SHORT).show(); } } @@ -491,64 +605,85 @@ public class QrisResultActivity extends AppCompatActivity { qrRefreshHandler.removeCallbacks(qrRefreshRunnable); } - timerTextView.setVisibility(View.GONE); - qrStatusTextView.setVisibility(View.GONE); - Log.d("QrisResultFlow", "🛑 QR refresh timer stopped"); } private void setupClickListeners() { - downloadQrisButton.setOnClickListener(v -> downloadQrCode()); - - checkStatusButton.setOnClickListener(v -> { - Log.d("QrisResultFlow", "Check status button clicked"); + // Cancel button + cancelButton.setOnClickListener(v -> { + addClickAnimation(v); stopQrRefresh(); - simulateWebhook(); + finish(); }); - returnMainButton.setOnClickListener(v -> { - stopQrRefresh(); - 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(); - }); - } - - private String formatCurrency(String amount) { - try { - double amountDouble = Double.parseDouble(amount); - NumberFormat formatter = NumberFormat.getCurrencyInstance(new Locale("id", "ID")); - return formatter.format(amountDouble); - } catch (NumberFormatException e) { - Log.w("QrisResultFlow", "Error formatting currency: " + e.getMessage()); - return "IDR " + amount; + // Back navigation + if (backNavigation != null) { + backNavigation.setOnClickListener(v -> { + addClickAnimation(v); + stopQrRefresh(); + finish(); + }); } - } - private void downloadQrCode() { - try { - qrImageView.setDrawingCacheEnabled(true); - qrImageView.buildDrawingCache(); - Bitmap bitmap = qrImageView.getDrawingCache(); - if (bitmap != null) { - saveImageToGallery(bitmap, "qris_code_" + System.currentTimeMillis()); - } else { - Toast.makeText(this, "Unable to capture QR code image", Toast.LENGTH_SHORT).show(); + // Hidden check status button for testing + if (checkStatusButton != null) { + checkStatusButton.setOnClickListener(v -> { + Log.d("QrisResultFlow", "Check status button clicked"); + stopQrRefresh(); + simulateWebhook(); + }); + } + + // Hidden return main button + if (returnMainButton != null) { + returnMainButton.setOnClickListener(v -> { + stopQrRefresh(); + 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"); + stopQrRefresh(); + simulateWebhook(); + } } - } catch (Exception e) { - Log.e("QrisResultFlow", "Error downloading QR code: " + e.getMessage(), e); - Toast.makeText(this, "Error downloading QR code: " + e.getMessage(), Toast.LENGTH_LONG).show(); - } finally { - qrImageView.setDrawingCacheEnabled(false); - } + }); } - private static class DownloadImageTask extends AsyncTask { + // 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; - DownloadImageTask(ImageView bmImage) { + EnhancedDownloadImageTask(ImageView bmImage) { this.bmImage = bmImage; } @@ -556,37 +691,75 @@ public class QrisResultActivity extends AppCompatActivity { protected Bitmap doInBackground(String... urls) { String urlDisplay = urls[0]; Bitmap bitmap = null; + try { - Log.d("QrisResultFlow", "Downloading image from: " + urlDisplay); + // ✅ VALIDATE URL + 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(10000); - connection.setReadTimeout(10000); + connection.setConnectTimeout(15000); // Increase timeout + connection.setReadTimeout(15000); connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + connection.setRequestProperty("Accept", "image/*"); + + // ✅ ADD AUTHORIZATION if needed for Midtrans QR + if (urlDisplay.contains("midtrans.com")) { + connection.setRequestProperty("Authorization", MIDTRANS_AUTH); + } + connection.connect(); int responseCode = connection.getResponseCode(); - Log.d("QrisResultFlow", "Image download response code: " + responseCode); + 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: " + + Log.d("QrisResultFlow", "✅ Image downloaded successfully. Size: " + bitmap.getWidth() + "x" + bitmap.getHeight()); } else { - Log.e("QrisResultFlow", "Failed to decode bitmap from stream"); + 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); + Log.e("QrisResultFlow", "❌ Failed to download image. HTTP code: " + responseCode); + + // ✅ READ ERROR RESPONSE + InputStream errorStream = connection.getErrorStream(); + if (errorStream != null) { + BufferedReader br = new BufferedReader(new InputStreamReader(errorStream)); + StringBuilder errorResponse = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + errorResponse.append(line); + } + Log.e("QrisResultFlow", "❌ Error response: " + errorResponse.toString()); + } + errorMessage = "Failed to download QR code (HTTP " + responseCode + ")"; } + } catch (Exception e) { - Log.e("QrisResultFlow", "Exception downloading image: " + e.getMessage(), e); + Log.e("QrisResultFlow", "❌ Exception downloading image: " + e.getMessage(), e); errorMessage = "Error downloading QR code: " + e.getMessage(); } + return bitmap; } @@ -594,34 +767,17 @@ public class QrisResultActivity extends AppCompatActivity { protected void onPostExecute(Bitmap result) { if (result != null) { bmImage.setImageBitmap(result); - Log.d("QrisResultFlow", "QR code image displayed successfully"); + Log.d("QrisResultFlow", "✅ QR code image displayed successfully"); } else { - Log.e("QrisResultFlow", "Failed to display QR code image"); + 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(), errorMessage, Toast.LENGTH_LONG).show(); + Toast.makeText(bmImage.getContext(), "QR Error: " + errorMessage, Toast.LENGTH_LONG).show(); } } } } - private void saveImageToGallery(Bitmap bitmap, String fileName) { - try { - String savedImageURL = android.provider.MediaStore.Images.Media.insertImage( - getContentResolver(), bitmap, fileName, "QRIS Payment QR Code"); - if (savedImageURL != null) { - Toast.makeText(this, "QRIS saved to gallery", Toast.LENGTH_SHORT).show(); - Log.d("QrisResultFlow", "QR code saved to gallery: " + savedImageURL); - } else { - Toast.makeText(this, "Failed to save QRIS", Toast.LENGTH_SHORT).show(); - Log.e("QrisResultFlow", "Failed to save QR code to gallery"); - } - } catch (Exception e) { - Log.e("QrisResultFlow", "Error saving QR code: " + e.getMessage(), e); - Toast.makeText(this, "Error saving QRIS: " + e.getMessage(), Toast.LENGTH_LONG).show(); - } - } - // ✅ HELPER: Convert technical issuer name ke display name private String getDisplayName(String technicalName) { if (technicalName == null || technicalName.isEmpty()) { @@ -683,8 +839,8 @@ public class QrisResultActivity extends AppCompatActivity { JSONObject payload = new JSONObject(); payload.put("status_code", "200"); payload.put("status_message", "Success, transaction is found"); - payload.put("transaction_id", webhookTransactionId); // ✅ Use monitoring transaction - payload.put("order_id", orderId); // ✅ Keep parent order ID for reference + payload.put("transaction_id", webhookTransactionId); + payload.put("order_id", orderId); payload.put("merchant_id", merchantId != null ? merchantId : "G900255786"); payload.put("gross_amount", grossAmount != null ? grossAmount : String.valueOf(originalAmount)); payload.put("currency", "IDR"); @@ -693,11 +849,17 @@ public class QrisResultActivity extends AppCompatActivity { payload.put("transaction_status", finalStatus.equals("PAID") ? "settlement" : finalStatus.toLowerCase()); payload.put("fraud_status", "accept"); payload.put("acquirer", finalAcquirer); - payload.put("issuer", finalIssuer); // ✅ Use actual issuer + payload.put("issuer", finalIssuer); payload.put("settlement_time", getCurrentISOTime()); payload.put("reference_id", referenceId); payload.put("shopeepay_reference_number", ""); + // ✅ ADD QR STRING if available + if (!currentQrString.isEmpty()) { + payload.put("qr_string", currentQrString); + Log.d("QrisResultFlow", "📋 Added QR String to webhook payload"); + } + // ✅ SIGNATURE untuk validasi String serverKey = getServerKey(); String signature = generateSignature(orderId, "200", @@ -758,11 +920,7 @@ public class QrisResultActivity extends AppCompatActivity { .format(new java.util.Date()); } - // ✅ SIMPLIFIED: Use the main webhook method - private void syncStatusToBackend(String referenceId, String finalStatus, String paidOrderId) { - syncTransactionStatusToBackend(finalStatus); - } - + // ✅ MODIFIED: Show full screen success instead of simple text display private void showPaymentSuccess() { Log.d("QrisResultFlow", "Showing payment success screen"); @@ -771,49 +929,119 @@ public class QrisResultActivity extends AppCompatActivity { // ✅ SYNC STATUS KE BACKEND syncTransactionStatusToBackend("PAID"); - // Hide payment elements - qrImageView.setVisibility(View.GONE); - amountTextView.setVisibility(View.GONE); - referenceTextView.setVisibility(View.GONE); - downloadQrisButton.setVisibility(View.GONE); - checkStatusButton.setVisibility(View.GONE); + // Show full screen success + showSuccessScreen(); - // Show success elements - statusTextView.setVisibility(View.VISIBLE); - statusTextView.setText("✅ Payment Successful!\n\nTransaction ID: " + transactionId + - "\nReference: " + referenceId + - "\nAmount: " + formatCurrency(grossAmount)); - - returnMainButton.setVisibility(View.VISIBLE); - - // Automatically launch receipt after a short delay - new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> { + // Navigate to receipt after delay + animationHandler.postDelayed(() -> { launchReceiptActivity(); - }, 2000); + }, 2500); Toast.makeText(this, "Payment simulation completed successfully!", Toast.LENGTH_LONG).show(); } + // ✅ NEW: 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) { + successMessage.setText("Pembayaran 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) { + // Start with invisible icon + successIcon.setScaleX(0f); + successIcon.setScaleY(0f); + successIcon.setAlpha(0f); + + // Scale animation with bounce effect + ObjectAnimator scaleX = ObjectAnimator.ofFloat(successIcon, "scaleX", 0f, 1.2f, 1f); + ObjectAnimator scaleY = ObjectAnimator.ofFloat(successIcon, "scaleY", 0f, 1.2f, 1f); + ObjectAnimator iconFadeIn = ObjectAnimator.ofFloat(successIcon, "alpha", 0f, 1f); + + 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(); + } + } + } + + // ✅ NEW: Hide main UI components for clean success screen + private void hideMainUIComponents() { + // Hide main content + if (mainCard != null) { + mainCard.setVisibility(View.GONE); + } + + // Hide header elements + if (headerBackground != null) { + headerBackground.setVisibility(View.GONE); + } + if (backNavigation != null) { + backNavigation.setVisibility(View.GONE); + } + + // Hide cancel button + if (cancelButton != null) { + cancelButton.setVisibility(View.GONE); + } + } + @Override protected void onDestroy() { super.onDestroy(); stopQrRefresh(); + 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; + } + stopQrRefresh(); - 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(); + finish(); super.onBackPressed(); } private void pollPendingPaymentLog(final String orderId) { Log.d("QrisResultFlow", "Starting polling for orderId: " + orderId); - progressBar.setVisibility(View.VISIBLE); - statusTextView.setText("Checking payment status..."); new Thread(() -> { int maxAttempts = 12; @@ -876,7 +1104,6 @@ public class QrisResultActivity extends AppCompatActivity { Log.d("QrisResultFlow", "🎉 Payment already completed with status: " + transactionStatus); new Handler(Looper.getMainLooper()).post(() -> { - progressBar.setVisibility(View.GONE); stopQrRefresh(); showPaymentSuccess(); Toast.makeText(QrisResultActivity.this, "Payment completed!", Toast.LENGTH_LONG).show(); @@ -911,13 +1138,10 @@ public class QrisResultActivity extends AppCompatActivity { final boolean logFound = found; new Handler(Looper.getMainLooper()).post(() -> { - progressBar.setVisibility(View.GONE); - if (logFound) { + if (logFound && checkStatusButton != null) { checkStatusButton.setEnabled(true); - statusTextView.setText("Ready to simulate payment"); Toast.makeText(QrisResultActivity.this, "Payment log found!", Toast.LENGTH_SHORT).show(); - } else { - statusTextView.setText("Payment log not found"); + } else if (checkStatusButton != null) { Toast.makeText(QrisResultActivity.this, "Payment log not found. Manual simulation available.", Toast.LENGTH_LONG).show(); checkStatusButton.setEnabled(true); } @@ -1012,12 +1236,20 @@ public class QrisResultActivity extends AppCompatActivity { String actualIssuer = statusResponse.optString("issuer", ""); String actualAcquirer = statusResponse.optString("acquirer", ""); + // ✅ UPDATE QR STRING if available in status response + String qrStringFromStatus = statusResponse.optString("qr_string", ""); + if (!qrStringFromStatus.isEmpty()) { + currentQrString = qrStringFromStatus; + Log.d("QrisResultFlow", "🔄 Updated QR String from status check"); + } + Log.d("QrisResultFlow", "💳 Payment status check result (" + transactionType + "):"); Log.d("QrisResultFlow", " Status: " + transactionStatus); Log.d("QrisResultFlow", " Payment Type: " + paymentType); Log.d("QrisResultFlow", " Amount: " + grossAmount); Log.d("QrisResultFlow", " Actual Issuer: " + actualIssuer); Log.d("QrisResultFlow", " Actual Acquirer: " + actualAcquirer); + Log.d("QrisResultFlow", " QR String Available: " + !qrStringFromStatus.isEmpty()); // ✅ CRITICAL FIX: Store the actual values from Midtrans if (!actualIssuer.isEmpty() && !actualIssuer.equalsIgnoreCase("qris")) { @@ -1069,9 +1301,6 @@ public class QrisResultActivity extends AppCompatActivity { private void simulateWebhook() { Log.d("QrisResultFlow", "🚀 Starting webhook simulation"); - progressBar.setVisibility(View.VISIBLE); - statusTextView.setText("Simulating payment..."); - checkStatusButton.setEnabled(false); stopQrRefresh(); @@ -1099,7 +1328,7 @@ public class QrisResultActivity extends AppCompatActivity { 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); // ✅ Use monitoring transaction + payload.put("transaction_id", currentTransactionId); payload.put("status_message", "midtrans payment notification"); payload.put("status_code", "200"); payload.put("signature_key", signatureKey); @@ -1107,13 +1336,19 @@ public class QrisResultActivity extends AppCompatActivity { payload.put("payment_type", "qris"); payload.put("order_id", currentOrderId); payload.put("merchant_id", merchantId != null ? merchantId : "G900255786"); - payload.put("issuer", finalIssuer); // ✅ Use actual issuer + payload.put("issuer", finalIssuer); payload.put("gross_amount", currentGrossAmount); payload.put("fraud_status", "accept"); payload.put("currency", "IDR"); - payload.put("acquirer", finalAcquirer); // ✅ Use actual acquirer + payload.put("acquirer", finalAcquirer); payload.put("shopeepay_reference_number", ""); payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID"); + + // ✅ ADD QR STRING to webhook if available + if (!currentQrString.isEmpty()) { + payload.put("qr_string", currentQrString); + Log.d("QrisResultFlow", "📋 Added QR String to webhook simulation"); + } URL url = new URL(webhookUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); @@ -1150,7 +1385,6 @@ public class QrisResultActivity extends AppCompatActivity { } new Handler(Looper.getMainLooper()).post(() -> { - progressBar.setVisibility(View.GONE); showPaymentSuccess(); }); }).start(); @@ -1173,6 +1407,7 @@ public class QrisResultActivity extends AppCompatActivity { Log.d("QrisResultFlow", " Amount: " + originalAmount); Log.d("QrisResultFlow", " Actual Issuer: " + finalIssuer); Log.d("QrisResultFlow", " Display Card Type: " + displayCardType); + Log.d("QrisResultFlow", " QR String Available: " + !currentQrString.isEmpty()); intent.putExtra("transaction_id", transactionId); intent.putExtra("reference_id", referenceId); @@ -1184,13 +1419,18 @@ public class QrisResultActivity extends AppCompatActivity { intent.putExtra("payment_method", "QRIS"); intent.putExtra("channel_code", "QRIS"); intent.putExtra("channel_category", "RETAIL_OUTLET"); - intent.putExtra("card_type", displayCardType); // ✅ Use display name + intent.putExtra("card_type", displayCardType); intent.putExtra("merchant_name", "Marcel Panjaitan"); intent.putExtra("merchant_location", "Jakarta, Indonesia"); - intent.putExtra("acquirer", finalIssuer); // ✅ Use actual issuer + intent.putExtra("acquirer", finalIssuer); intent.putExtra("mid", "71000026521"); intent.putExtra("tid", "73001500"); + // ✅ ADD QR STRING to receipt if available + if (!currentQrString.isEmpty()) { + intent.putExtra("qr_string", currentQrString); + } + startActivity(intent); } @@ -1203,13 +1443,4 @@ public class QrisResultActivity extends AppCompatActivity { 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()); } - - @Override - public boolean onOptionsItemSelected(android.view.MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_status.xml b/app/src/main/res/drawable/bg_status.xml new file mode 100644 index 0000000..0d7aeab --- /dev/null +++ b/app/src/main/res/drawable/bg_status.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_cancel_background.xml b/app/src/main/res/drawable/button_cancel_background.xml new file mode 100644 index 0000000..db18a5d --- /dev/null +++ b/app/src/main/res/drawable/button_cancel_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/timer_circle_background.xml b/app/src/main/res/drawable/timer_circle_background.xml new file mode 100644 index 0000000..0a111f2 --- /dev/null +++ b/app/src/main/res/drawable/timer_circle_background.xml @@ -0,0 +1,5 @@ + + + + + \ 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 91569ec..055ba66 100644 --- a/app/src/main/res/layout/activity_qris_result.xml +++ b/app/src/main/res/layout/activity_qris_result.xml @@ -1,223 +1,241 @@ - + android:background="#F5F5F5" + tools:context=".QrisResultActivity"> - + - - - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> - + + + + + + android:text="Generate QR" + android:textColor="@android:color/white" + android:textSize="18sp" + android:textStyle="bold" + android:fontFamily="@font/inter" /> - - - - - + + app:layout_constraintTop_toBottomOf="@id/header_background" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toTopOf="@id/cancel_button" + app:layout_constraintVertical_bias="0.3"> + android:padding="32dp" + android:gravity="center"> - - - - + + android:textColor="#000000" + android:letterSpacing="0.1" + android:layout_marginBottom="24dp" + android:fontFamily="@font/inter" /> + android:layout_width="240dp" + android:layout_height="240dp" + android:layout_marginBottom="24dp" + android:scaleType="centerInside" + android:background="#FFFFFF" + android:padding="8dp" /> - + + android:textColor="#333333" + android:layout_marginBottom="16dp" + android:fontFamily="@font/inter" /> - + - - - + android:gravity="center" + android:background="@drawable/timer_circle_background" + android:fontFamily="@font/inter" /> - - - - -