diff --git a/app/src/main/java/com/example/bdkipoc/QrisActivity.java b/app/src/main/java/com/example/bdkipoc/QrisActivity.java index 674b77b..d5b60d9 100644 --- a/app/src/main/java/com/example/bdkipoc/QrisActivity.java +++ b/app/src/main/java/com/example/bdkipoc/QrisActivity.java @@ -1,22 +1,13 @@ package com.example.bdkipoc; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.graphics.Color; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; import android.util.Log; +import android.view.MenuItem; import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.view.animation.AccelerateDecelerateInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; @@ -28,6 +19,7 @@ import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import org.json.JSONException; import org.json.JSONObject; @@ -46,34 +38,24 @@ import java.util.UUID; public class QrisActivity extends AppCompatActivity { - // Views - private EditText editTextAmount; + private ProgressBar progressBar; private Button initiatePaymentButton; + private TextView statusTextView; + private EditText editTextAmount; + private TextView referenceIdTextView; private LinearLayout backNavigation; - private TextView toolbarTitle; - private TextView descriptionText; // Numpad buttons - private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000; - private TextView btnDelete; // Changed from ImageView to TextView - - // Only needed views for loading state - private ProgressBar progressBar; - private TextView statusTextView; - private TextView referenceIdTextView; - private LinearLayout initialPaymentLayout; + private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000, btnDelete; + private TextView descriptionText; - // Data - private StringBuilder currentAmount = new StringBuilder(); - private static final int MAX_AMOUNT_LENGTH = 12; private String transactionId; private String transactionUuid; private String referenceId; private int amount; private JSONObject midtransResponse; - // Animation - private Handler animationHandler = new Handler(Looper.getMainLooper()); + private StringBuilder currentAmount = new StringBuilder(); private static final String BACKEND_BASE = "https://be-edc.msvc.app"; private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge"; @@ -83,49 +65,18 @@ public class QrisActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - // Set status bar color programmatically - setStatusBarColor(); - setContentView(R.layout.activity_qris); - initializeViews(); - setupClickListeners(); - setupInitialStates(); - - // Generate reference ID - referenceId = "ref-" + generateRandomString(8); - referenceIdTextView.setText(referenceId); - - // Initially hide the progress and status views - progressBar.setVisibility(View.GONE); - statusTextView.setVisibility(View.GONE); - initialPaymentLayout.setVisibility(View.VISIBLE); - } - - private void setStatusBarColor() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - Window window = getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - window.setStatusBarColor(Color.parseColor("#E31937")); // Red color - - // Make status bar icons white (for dark red background) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - View decorView = window.getDecorView(); - decorView.setSystemUiVisibility(0); // Clear light status bar flag - } - } - } - - private void initializeViews() { - // New UI components (similar to PaymentActivity) - editTextAmount = findViewById(R.id.editTextAmount); + // Initialize views + progressBar = findViewById(R.id.progressBar); initiatePaymentButton = findViewById(R.id.initiatePaymentButton); + statusTextView = findViewById(R.id.statusTextView); + editTextAmount = findViewById(R.id.editTextAmount); + referenceIdTextView = findViewById(R.id.referenceIdTextView); backNavigation = findViewById(R.id.back_navigation); - toolbarTitle = findViewById(R.id.toolbarTitle); descriptionText = findViewById(R.id.descriptionText); - // Numpad buttons + // Initialize numpad buttons btn1 = findViewById(R.id.btn1); btn2 = findViewById(R.id.btn2); btn3 = findViewById(R.id.btn3); @@ -137,232 +88,102 @@ public class QrisActivity extends AppCompatActivity { btn9 = findViewById(R.id.btn9); btn0 = findViewById(R.id.btn0); btn000 = findViewById(R.id.btn000); - btnDelete = findViewById(R.id.btnDelete); // Now TextView + btnDelete = findViewById(R.id.btnDelete); - // Only needed views for loading state - progressBar = findViewById(R.id.progressBar); - statusTextView = findViewById(R.id.statusTextView); - referenceIdTextView = findViewById(R.id.referenceIdTextView); + // Generate reference ID + referenceId = "ref-" + generateRandomString(8); + referenceIdTextView.setText(referenceId); + + // Set up click listeners + initiatePaymentButton.setOnClickListener(v -> createTransaction()); + backNavigation.setOnClickListener(v -> finish()); - // Main content layout (replaces initialPaymentLayout) - initialPaymentLayout = findViewById(R.id.mainContentLayout); - } - - private void setupClickListeners() { - // Back navigation - backNavigation.setOnClickListener(v -> { - addClickAnimation(v); - navigateBack(); - }); - - toolbarTitle.setOnClickListener(v -> { - addClickAnimation(v); - navigateBack(); - }); - - // Numpad listeners - btn1.setOnClickListener(v -> handleNumpadClick(v, "1")); - btn2.setOnClickListener(v -> handleNumpadClick(v, "2")); - btn3.setOnClickListener(v -> handleNumpadClick(v, "3")); - btn4.setOnClickListener(v -> handleNumpadClick(v, "4")); - btn5.setOnClickListener(v -> handleNumpadClick(v, "5")); - btn6.setOnClickListener(v -> handleNumpadClick(v, "6")); - btn7.setOnClickListener(v -> handleNumpadClick(v, "7")); - btn8.setOnClickListener(v -> handleNumpadClick(v, "8")); - btn9.setOnClickListener(v -> handleNumpadClick(v, "9")); - btn0.setOnClickListener(v -> handleNumpadClick(v, "0")); - btn000.setOnClickListener(v -> handleNumpadClick(v, "000")); - - // Delete button - btnDelete.setOnClickListener(v -> { - addClickAnimation(v); - deleteLastDigit(); - }); - - // Original QRIS buttons - only initiate payment needed - initiatePaymentButton.setOnClickListener(v -> { - if (initiatePaymentButton.isEnabled()) { - addButtonClickAnimation(v); - createTransaction(); - } - }); - } - - private void setupInitialStates() { - // Initially hide amount input and show description - editTextAmount.setVisibility(View.GONE); - descriptionText.setVisibility(View.VISIBLE); + // Set up numpad listeners + setupNumpadListeners(); - // Set initial button state - updateButtonState(); + // Initially disable the button + initiatePaymentButton.setEnabled(false); + } + + private void setupNumpadListeners() { + View.OnClickListener numberClickListener = v -> { + TextView button = (TextView) v; + String number = button.getText().toString(); + appendNumber(number); + }; + + btn1.setOnClickListener(numberClickListener); + btn2.setOnClickListener(numberClickListener); + btn3.setOnClickListener(numberClickListener); + btn4.setOnClickListener(numberClickListener); + btn5.setOnClickListener(numberClickListener); + btn6.setOnClickListener(numberClickListener); + btn7.setOnClickListener(numberClickListener); + btn8.setOnClickListener(numberClickListener); + btn9.setOnClickListener(numberClickListener); + btn0.setOnClickListener(numberClickListener); + btn000.setOnClickListener(numberClickListener); - // Disable EditText input (only numpad input allowed) - editTextAmount.setFocusable(false); - editTextAmount.setClickable(false); - editTextAmount.setCursorVisible(false); + btnDelete.setOnClickListener(v -> deleteLastDigit()); } - - private void navigateBack() { - finish(); - } - - private void handleNumpadClick(View view, String digit) { - addClickAnimation(view); - addDigit(digit); - } - - private void addDigit(String digit) { - // Validate input length - if (currentAmount.length() >= MAX_AMOUNT_LENGTH) { - showToast("Maksimal " + MAX_AMOUNT_LENGTH + " digit"); - return; - } - - // Handle leading zeros - if (currentAmount.length() == 0) { - if (digit.equals("000")) { - // Don't allow 000 as first input - return; - } - currentAmount.append(digit); - } else if (currentAmount.length() == 1 && currentAmount.toString().equals("0")) { - if (!digit.equals("000")) { - // Replace single 0 with new digit - currentAmount = new StringBuilder(digit); - } else { - return; - } - } else { - currentAmount.append(digit); - } - + + private void appendNumber(String number) { + currentAmount.append(number); updateAmountDisplay(); - updateButtonState(); - addInputFeedback(); } - + private void deleteLastDigit() { if (currentAmount.length() > 0) { - String current = currentAmount.toString(); - - // If current ends with 000, remove all three digits - if (current.endsWith("000") && current.length() >= 3) { - currentAmount.delete(currentAmount.length() - 3, currentAmount.length()); - } else { - currentAmount.deleteCharAt(currentAmount.length() - 1); - } - + currentAmount.deleteCharAt(currentAmount.length() - 1); updateAmountDisplay(); - updateButtonState(); - addDeleteFeedback(); } } - + private void updateAmountDisplay() { - String amount = currentAmount.toString(); + String amountStr = currentAmount.toString(); - if (amount.isEmpty() || amount.equals("0")) { - // Show description text, hide amount input + if (amountStr.isEmpty()) { editTextAmount.setVisibility(View.GONE); - descriptionText.setVisibility(View.VISIBLE); + descriptionText.setText("Pastikan kembali nominal pembayaran pelanggan Anda"); + initiatePaymentButton.setEnabled(false); } else { - // Show amount input, hide description text - String formattedAmount = formatCurrency(amount); - editTextAmount.setText(formattedAmount); editTextAmount.setVisibility(View.VISIBLE); - descriptionText.setVisibility(View.GONE); + editTextAmount.setText(formatAmount(amountStr)); + descriptionText.setText("Tekan Konfirmasi untuk melanjutkan"); + + // Enable button if amount is valid + try { + int amt = Integer.parseInt(amountStr); + initiatePaymentButton.setEnabled(amt >= 1000); + } catch (NumberFormatException e) { + initiatePaymentButton.setEnabled(false); + } } } - - private String formatCurrency(String amount) { - if (TextUtils.isEmpty(amount) || amount.equals("0")) { - return ""; - } + + private String formatAmount(String amount) { + if (amount.isEmpty()) return ""; try { - long number = Long.parseLong(amount); - return String.format("%,d", number).replace(',', '.'); + long num = Long.parseLong(amount); + return String.format("%,d", num); } catch (NumberFormatException e) { return amount; } } - private void updateButtonState() { - boolean hasValidAmount = currentAmount.length() > 0 && - !currentAmount.toString().equals("0") && - !currentAmount.toString().isEmpty(); - - initiatePaymentButton.setEnabled(hasValidAmount); - - // Use MaterialButton's backgroundTint property - com.google.android.material.button.MaterialButton materialButton = - (com.google.android.material.button.MaterialButton) initiatePaymentButton; - - if (hasValidAmount) { - // Active state - red background like in the XML - materialButton.setBackgroundTintList(android.content.res.ColorStateList.valueOf(Color.parseColor("#DE0701"))); - materialButton.setTextColor(Color.WHITE); - materialButton.setAlpha(1.0f); - } else { - // Inactive state - gray background - materialButton.setBackgroundTintList(android.content.res.ColorStateList.valueOf(Color.parseColor("#E8E8E8"))); - materialButton.setTextColor(Color.parseColor("#999999")); - materialButton.setAlpha(0.6f); - } - } - private void createTransaction() { - String amountText = currentAmount.toString(); - - if (TextUtils.isEmpty(amountText) || amountText.equals("0")) { - showToast("Masukkan jumlah pembayaran"); + if (currentAmount.length() == 0) { + Toast.makeText(this, "Masukkan jumlah pembayaran", Toast.LENGTH_SHORT).show(); return; } - - try { - long amountValue = Long.parseLong(amountText); - - // Validate minimum amount - if (amountValue < 1000) { - showToast("Minimal pembayaran Rp 1.000"); - return; - } - - // Validate maximum amount - if (amountValue > 999999999L) { - showToast("Maksimal pembayaran Rp 999.999.999"); - return; - } - - // Set amount for transaction - amount = (int) amountValue; - - // Show loading state - progressBar.setVisibility(View.VISIBLE); - statusTextView.setText("Creating transaction..."); - statusTextView.setVisibility(View.VISIBLE); - initiatePaymentButton.setEnabled(false); - - new CreateTransactionTask().execute(); - - } catch (NumberFormatException e) { - showToast("Format jumlah tidak valid"); - } - } - - private void displayQrCode(String qrImageUrl) { - // This method is no longer needed since we navigate to QrisResultActivity - // Keeping it for compatibility but it won't be called - } - - private void simulateWebhook() { - // This method is no longer needed since we navigate to QrisResultActivity - // QrisResultActivity handles webhook simulation - } - - private void showSuccessScreen() { - // This method is no longer needed since we navigate to QrisResultActivity - // QrisResultActivity handles success display + + progressBar.setVisibility(View.VISIBLE); + initiatePaymentButton.setEnabled(false); + statusTextView.setVisibility(View.VISIBLE); + statusTextView.setText("Creating transaction..."); + + new CreateTransactionTask().execute(); } private String generateRandomString(int length) { @@ -377,11 +198,23 @@ public class QrisActivity extends AppCompatActivity { } private String getServerKey() { - // MIDTRANS_AUTH = 'Basic base64string' - String base64 = MIDTRANS_AUTH.replace("Basic ", ""); - String decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT).toString(); - // Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present. - return decoded.replace(":\n", ""); + try { + // MIDTRANS_AUTH = 'Basic base64string' + String base64 = MIDTRANS_AUTH.replace("Basic ", ""); + byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT); + String decodedString = new String(decoded); + // Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present. + return decodedString.replace(":", ""); + } catch (Exception e) { + Log.e("MidtransCharge", "Error decoding server key: " + e.getMessage()); + return ""; + } + } + + private boolean isValidServerKey(String serverKey) { + return serverKey != null && + serverKey.startsWith("SB-Mid-server-") && + serverKey.length() > 20; } private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) { @@ -397,52 +230,18 @@ public class QrisActivity extends AppCompatActivity { } return hexString.toString(); } catch (java.security.NoSuchAlgorithmException e) { + Log.e("MidtransCharge", "Error generating signature: " + e.getMessage()); return ""; } } - // 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(); - } - - private void addButtonClickAnimation(View view) { - ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.98f, 1f); - ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.98f, 1f); - - AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(scaleX, scaleY); - animatorSet.setDuration(200); - animatorSet.start(); - } - - private void addInputFeedback() { - ObjectAnimator fadeIn = ObjectAnimator.ofFloat(editTextAmount, "alpha", 0.7f, 1f); - fadeIn.setDuration(200); - fadeIn.start(); - } - - private void addDeleteFeedback() { - ObjectAnimator shake = ObjectAnimator.ofFloat(editTextAmount, "translationX", 0f, -10f, 10f, 0f); - shake.setDuration(300); - shake.start(); - } - - // Utility methods - private void showToast(String message) { - Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); - } - @Override - public void onBackPressed() { - // Simple back navigation - navigateBack(); + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); } private class CreateTransactionTask extends AsyncTask { @@ -461,7 +260,32 @@ public class QrisActivity extends AppCompatActivity { payload.put("channel_code", "QRIS"); payload.put("reference_id", referenceId); - Log.d("MidtransCharge", "Amount for transaction: " + amount); + // Get amount from current input + String amountText = currentAmount.toString(); + Log.d("MidtransCharge", "Raw amount text: " + amountText); + + try { + // Parse amount - expecting integer in lowest denomination (Indonesian Rupiah) + amount = Integer.parseInt(amountText); + + // Validate minimum amount + if (amount < 1000) { + errorMessage = "Minimum amount is IDR 1,000"; + return false; + } + + // Validate maximum amount for testing + if (amount > 10000000) { + errorMessage = "Maximum amount is IDR 10,000,000"; + return false; + } + + Log.d("MidtransCharge", "Parsed amount: " + amount); + } catch (NumberFormatException e) { + Log.e("MidtransCharge", "Amount parsing error: " + e.getMessage()); + errorMessage = "Invalid amount format. Please enter numbers only."; + return false; + } payload.put("amount", amount); payload.put("cashflow", "MONEY_IN"); @@ -474,12 +298,15 @@ public class QrisActivity extends AppCompatActivity { payload.put("mid", "71000026521"); payload.put("tid", "73001500"); + Log.d("MidtransCharge", "Backend transaction payload: " + payload.toString()); + // Make the API call URL url = new URI(BACKEND_BASE + "/transactions").toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); conn.setDoOutput(true); try (OutputStream os = conn.getOutputStream()) { @@ -488,6 +315,8 @@ public class QrisActivity extends AppCompatActivity { } int responseCode = conn.getResponseCode(); + Log.d("MidtransCharge", "Backend response code: " + responseCode); + if (responseCode == 200 || responseCode == 201) { // Read the response BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); @@ -497,11 +326,15 @@ public class QrisActivity extends AppCompatActivity { response.append(responseLine.trim()); } + Log.d("MidtransCharge", "Backend response: " + response.toString()); + // Parse the response to get transaction ID JSONObject jsonResponse = new JSONObject(response.toString()); JSONObject data = jsonResponse.getJSONObject("data"); transactionId = String.valueOf(data.getInt("id")); + Log.d("MidtransCharge", "Created transaction ID: " + transactionId); + // Now generate QRIS via Midtrans return generateQris(amount); } else { @@ -512,18 +345,29 @@ public class QrisActivity extends AppCompatActivity { while ((responseLine = br.readLine()) != null) { response.append(responseLine.trim()); } - errorMessage = "Error creating transaction: " + response.toString(); + Log.e("MidtransCharge", "Backend error response: " + response.toString()); + errorMessage = "Error creating backend transaction: " + response.toString(); return false; } } catch (Exception e) { - Log.e("MidtransCharge", "Exception: " + e.getMessage(), e); - errorMessage = "Unexpected error: " + e.getMessage(); + Log.e("MidtransCharge", "Backend transaction exception: " + e.getMessage(), e); + errorMessage = "Backend transaction error: " + e.getMessage(); return false; } } private boolean generateQris(int amount) { try { + // Validate server key first + String serverKey = getServerKey(); + if (!isValidServerKey(serverKey)) { + Log.e("MidtransCharge", "Invalid server key format"); + errorMessage = "Invalid server key configuration"; + return false; + } + + Log.d("MidtransCharge", "Using server key: " + serverKey.substring(0, Math.min(20, serverKey.length())) + "..."); + // Create QRIS charge JSON payload JSONObject payload = new JSONObject(); payload.put("payment_type", "qris"); @@ -533,13 +377,31 @@ public class QrisActivity extends AppCompatActivity { transactionDetails.put("gross_amount", amount); payload.put("transaction_details", transactionDetails); + // Add customer details (recommended for better success rate) + 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); + + // Add item details (optional but recommended) + org.json.JSONArray itemDetails = new org.json.JSONArray(); + JSONObject item = new JSONObject(); + item.put("id", "item1"); + item.put("price", amount); + item.put("quantity", 1); + item.put("name", "QRIS Payment"); + itemDetails.put(item); + payload.put("item_details", itemDetails); + // Log the request details + Log.d("MidtransCharge", "=== MIDTRANS QRIS REQUEST ==="); Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL); Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH); - Log.d("MidtransCharge", "Accept: application/json"); - Log.d("MidtransCharge", "Content-Type: application/json"); Log.d("MidtransCharge", "X-Override-Notification: " + WEBHOOK_URL); Log.d("MidtransCharge", "Payload: " + payload.toString()); + Log.d("MidtransCharge", "================================"); // Make the API call to Midtrans URL url = new URI(MIDTRANS_CHARGE_URL).toURL(); @@ -549,7 +411,10 @@ public class QrisActivity extends AppCompatActivity { conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Authorization", MIDTRANS_AUTH); conn.setRequestProperty("X-Override-Notification", WEBHOOK_URL); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); conn.setDoOutput(true); + conn.setConnectTimeout(30000); // 30 seconds + conn.setReadTimeout(30000); // 30 seconds try (OutputStream os = conn.getOutputStream()) { byte[] input = payload.toString().getBytes("utf-8"); @@ -557,6 +422,8 @@ public class QrisActivity extends AppCompatActivity { } int responseCode = conn.getResponseCode(); + Log.d("MidtransCharge", "Midtrans HTTP Response Code: " + responseCode); + if (responseCode == 200 || responseCode == 201) { InputStream inputStream = conn.getInputStream(); if (inputStream != null) { @@ -567,8 +434,32 @@ public class QrisActivity extends AppCompatActivity { response.append(responseLine.trim()); } + Log.d("MidtransCharge", "Midtrans Success Response: " + response.toString()); + // Parse the response midtransResponse = new JSONObject(response.toString()); + + // Check if response contains error within success response + if (midtransResponse.has("status_code")) { + String statusCode = midtransResponse.getString("status_code"); + if (!statusCode.equals("201")) { + String statusMessage = midtransResponse.optString("status_message", "Unknown error"); + Log.e("MidtransCharge", "Midtrans Error in response: " + statusCode + " - " + statusMessage); + errorMessage = "Midtrans Error: " + statusMessage + " (Code: " + statusCode + ")"; + return false; + } + } + + // Validate response has required fields + if (!midtransResponse.has("actions") || + !midtransResponse.has("transaction_id") || + !midtransResponse.has("gross_amount")) { + Log.e("MidtransCharge", "Missing required fields in Midtrans response"); + errorMessage = "Invalid response from Midtrans - missing required fields"; + return false; + } + + Log.d("MidtransCharge", "QRIS generation successful!"); return true; } else { Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available"); @@ -584,17 +475,38 @@ public class QrisActivity extends AppCompatActivity { while ((responseLine = br.readLine()) != null) { errorResponse.append(responseLine.trim()); } - Log.e("MidtransCharge", "HTTP " + responseCode + ": " + errorResponse.toString()); - errorMessage = "Error generating QRIS: HTTP " + responseCode + ": " + errorResponse.toString(); + + Log.e("MidtransCharge", "Midtrans HTTP " + responseCode + ": " + errorResponse.toString()); + + // Try to parse error JSON for better error message + try { + JSONObject errorJson = new JSONObject(errorResponse.toString()); + + // Handle different error response formats + String errorMessage = ""; + if (errorJson.has("error_messages")) { + errorMessage = errorJson.optString("error_messages", "Unknown error"); + } else if (errorJson.has("status_message")) { + errorMessage = errorJson.optString("status_message", "Unknown error"); + } else if (errorJson.has("message")) { + errorMessage = errorJson.optString("message", "Unknown error"); + } else { + errorMessage = errorResponse.toString(); + } + + this.errorMessage = "Midtrans Error: " + errorMessage; + } catch (JSONException e) { + this.errorMessage = "HTTP " + responseCode + ": " + errorResponse.toString(); + } } else { Log.e("MidtransCharge", "HTTP " + responseCode + ": No error stream available"); - errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No error stream available"; + this.errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No error stream available"; } return false; } } catch (Exception e) { - Log.e("MidtransCharge", "Exception: " + e.getMessage(), e); - errorMessage = "Unexpected error: " + e.getMessage(); + Log.e("MidtransCharge", "Midtrans QRIS generation exception: " + e.getMessage(), e); + errorMessage = "Network error: " + e.getMessage(); return false; } } @@ -604,7 +516,17 @@ public class QrisActivity extends AppCompatActivity { if (success && midtransResponse != null) { try { // Extract needed values from midtransResponse - JSONObject actions = midtransResponse.getJSONArray("actions").getJSONObject(0); + org.json.JSONArray actionsArray = midtransResponse.getJSONArray("actions"); + if (actionsArray.length() == 0) { + Log.e("MidtransCharge", "No actions found in Midtrans response"); + Toast.makeText(QrisActivity.this, "Error: No QR code URL found in response", Toast.LENGTH_LONG).show(); + initiatePaymentButton.setEnabled(true); + progressBar.setVisibility(View.GONE); + statusTextView.setVisibility(View.GONE); + return; + } + + JSONObject actions = actionsArray.getJSONObject(0); String qrImageUrl = actions.getString("url"); // Extract transaction_id @@ -615,15 +537,19 @@ public class QrisActivity extends AppCompatActivity { String exactGrossAmount = midtransResponse.getString("gross_amount"); // Log everything before launching activity - Log.d("MidtransCharge", "Creating QrisResultActivity intent with:"); + Log.d("MidtransCharge", "=== LAUNCHING QRIS RESULT ACTIVITY ==="); Log.d("MidtransCharge", "qrImageUrl: " + qrImageUrl); Log.d("MidtransCharge", "amount: " + amount); Log.d("MidtransCharge", "referenceId: " + referenceId); Log.d("MidtransCharge", "transactionUuid (orderId): " + transactionUuid); Log.d("MidtransCharge", "transaction_id: " + transactionId); Log.d("MidtransCharge", "exactGrossAmount: " + exactGrossAmount); + Log.d("MidtransCharge", "transactionTime: " + transactionTime); + Log.d("MidtransCharge", "acquirer: " + acquirer); + Log.d("MidtransCharge", "merchantId: " + merchantId); + Log.d("MidtransCharge", "========================================"); - // Launch QrisResultActivity instead of showing QR inline + // Launch QrisResultActivity Intent intent = new Intent(QrisActivity.this, QrisResultActivity.class); intent.putExtra("qrImageUrl", qrImageUrl); intent.putExtra("amount", amount); @@ -637,54 +563,24 @@ public class QrisActivity extends AppCompatActivity { try { startActivity(intent); - // Finish this activity so user can't go back to input form - finish(); + finish(); // Close QrisActivity } catch (Exception e) { Log.e("MidtransCharge", "Failed to start QrisResultActivity: " + e.getMessage(), e); Toast.makeText(QrisActivity.this, "Error launching QR display: " + e.getMessage(), Toast.LENGTH_LONG).show(); - resetToInitialState(); } - + return; } catch (JSONException e) { Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e); - Toast.makeText(QrisActivity.this, "Error processing QRIS response", Toast.LENGTH_LONG).show(); - resetToInitialState(); + Toast.makeText(QrisActivity.this, "Error processing QRIS response: " + e.getMessage(), Toast.LENGTH_LONG).show(); } } else { - String message = (errorMessage != null && !errorMessage.isEmpty()) ? errorMessage : "Unknown error occurred. Please check Logcat for details."; + String message = (errorMessage != null && !errorMessage.isEmpty()) ? + errorMessage : "Unknown error occurred. Please check Logcat for details."; Toast.makeText(QrisActivity.this, message, Toast.LENGTH_LONG).show(); - resetToInitialState(); + initiatePaymentButton.setEnabled(true); } progressBar.setVisibility(View.GONE); + statusTextView.setVisibility(View.GONE); } } - - private void resetToInitialState() { - // Reset to show the input form again - progressBar.setVisibility(View.GONE); - statusTextView.setVisibility(View.GONE); - initiatePaymentButton.setEnabled(true); - updateButtonState(); - } - - // Remove DownloadImageTask - no longer needed - - // Remove SimulateWebhookTask - no longer needed - - @Override - protected void onDestroy() { - super.onDestroy(); - if (animationHandler != null) { - animationHandler.removeCallbacksAndMessages(null); - } - } - - // Public methods for testing - public String getCurrentAmount() { - return currentAmount.toString(); - } - - public boolean isInitiateButtonEnabled() { - return initiatePaymentButton.isEnabled(); - } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java b/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java index c4d45c8..eee0786 100644 --- a/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java +++ b/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java @@ -3,26 +3,21 @@ package com.example.bdkipoc; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.Color; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; -import android.os.CountDownTimer; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.view.View; -import android.view.Window; -import android.view.WindowManager; import android.widget.Button; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import org.json.JSONArray; import org.json.JSONException; @@ -35,28 +30,18 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; +import java.text.NumberFormat; +import java.util.Locale; public class QrisResultActivity extends AppCompatActivity { private ImageView qrImageView; private TextView amountTextView; private TextView referenceTextView; - private TextView timerTextView; - private TextView qrStatusTextView; private Button downloadQrisButton; private Button checkStatusButton; - private Button cancelButton; private TextView statusTextView; private Button returnMainButton; private ProgressBar progressBar; - private LinearLayout backNavigation; - - // Timer and QR refresh - private CountDownTimer countDownTimer; - private long timeLeftInMillis = 60000; // 60 seconds - private int qrRefreshCount = 0; - private final int MAX_QR_REFRESH = 5; // Maximum 5 refreshes - - // Data variables private String orderId; private String grossAmount; private String referenceId; @@ -64,102 +49,42 @@ public class QrisResultActivity extends AppCompatActivity { private String transactionTime; private String acquirer; private String merchantId; - private String originalQrImageUrl; - private int amount; private String backendBase = "https://be-edc.msvc.app"; private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans"; + + // Server key for signature generation + private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM="; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - // Set status bar color programmatically - setStatusBarColor(); - setContentView(R.layout.activity_qris_result); - initializeViews(); - setupClickListeners(); - setupData(); - startTimer(); - } - - private void setStatusBarColor() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - Window window = getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - window.setStatusBarColor(Color.parseColor("#E31937")); // Red color - - // Make status bar icons white (for dark red background) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - View decorView = window.getDecorView(); - decorView.setSystemUiVisibility(0); // Clear light status bar flag + // Set up the toolbar + Toolbar toolbar = findViewById(R.id.toolbar); + if (toolbar != null) { + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setDisplayShowHomeEnabled(true); + getSupportActionBar().setTitle("QRIS Payment"); } } - } - private void initializeViews() { + // Initialize views qrImageView = findViewById(R.id.qrImageView); amountTextView = findViewById(R.id.amountTextView); referenceTextView = findViewById(R.id.referenceTextView); - timerTextView = findViewById(R.id.timerTextView); - qrStatusTextView = findViewById(R.id.qrStatusTextView); downloadQrisButton = findViewById(R.id.downloadQrisButton); checkStatusButton = findViewById(R.id.checkStatusButton); - cancelButton = findViewById(R.id.cancelButton); statusTextView = findViewById(R.id.statusTextView); returnMainButton = findViewById(R.id.returnMainButton); progressBar = findViewById(R.id.progressBar); - backNavigation = findViewById(R.id.back_navigation); - } - private void setupClickListeners() { - // Back navigation - if (backNavigation != null) { - backNavigation.setOnClickListener(v -> finish()); - } - - // Cancel button - cancelButton.setOnClickListener(v -> { - if (countDownTimer != null) { - countDownTimer.cancel(); - } - finish(); - }); - - // Download QRIS button (now visible and functional) - downloadQrisButton.setOnClickListener(v -> { - qrImageView.setDrawingCacheEnabled(true); - Bitmap bitmap = qrImageView.getDrawingCache(); - if (bitmap != null) { - saveImageToGallery(bitmap, "qris_code_" + System.currentTimeMillis()); - } - qrImageView.setDrawingCacheEnabled(false); - }); - - // Check Payment Status button (now visible and functional) - checkStatusButton.setOnClickListener(v -> { - Toast.makeText(this, "Checking payment status...", Toast.LENGTH_SHORT).show(); - simulateWebhook(); - }); - - // Return to Main Screen button (now visible and functional) - returnMainButton.setOnClickListener(v -> { - if (countDownTimer != null) { - countDownTimer.cancel(); - } - Intent intent = new Intent(QrisResultActivity.this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - finishAffinity(); - }); - } - - private void setupData() { + // Get intent data Intent intent = getIntent(); String qrImageUrl = intent.getStringExtra("qrImageUrl"); - originalQrImageUrl = qrImageUrl; // Store original URL for refresh - amount = intent.getIntExtra("amount", 0); + int amount = intent.getIntExtra("amount", 0); referenceId = intent.getStringExtra("referenceId"); orderId = intent.getStringExtra("orderId"); grossAmount = intent.getStringExtra("grossAmount"); @@ -168,152 +93,176 @@ public class QrisResultActivity extends AppCompatActivity { acquirer = intent.getStringExtra("acquirer"); merchantId = intent.getStringExtra("merchantId"); + // Enhanced logging for debugging + Log.d("QrisResultFlow", "=== QRIS RESULT ACTIVITY STARTED ==="); + Log.d("QrisResultFlow", "QR Image URL: " + qrImageUrl); + Log.d("QrisResultFlow", "Amount (int): " + amount); + Log.d("QrisResultFlow", "Gross Amount (string): " + grossAmount); + Log.d("QrisResultFlow", "Reference ID: " + referenceId); + Log.d("QrisResultFlow", "Order ID: " + orderId); + Log.d("QrisResultFlow", "Transaction ID: " + transactionId); + Log.d("QrisResultFlow", "Transaction Time: " + transactionTime); + Log.d("QrisResultFlow", "Acquirer: " + acquirer); + Log.d("QrisResultFlow", "Merchant ID: " + merchantId); + Log.d("QrisResultFlow", "======================================"); + + // Validate required data if (orderId == null || transactionId == null) { - Log.e("QrisResultFlow", "orderId or transactionId is null! Intent extras: " + intent.getExtras()); - Toast.makeText(this, "Missing transaction details!", Toast.LENGTH_LONG).show(); + Log.e("QrisResultFlow", "Critical error: orderId or transactionId is null!"); + Log.e("QrisResultFlow", "Intent extras: " + intent.getExtras()); + Toast.makeText(this, "Missing transaction details! Cannot proceed.", Toast.LENGTH_LONG).show(); + finish(); + return; } - // Format and display amount - if (grossAmount != null) { - String formattedAmount = "RP." + formatCurrency(grossAmount); - amountTextView.setText(formattedAmount); - } else if (amount > 0) { - String formattedAmount = "RP." + formatCurrency(String.valueOf(amount)); - amountTextView.setText(formattedAmount); + if (qrImageUrl == null || qrImageUrl.isEmpty()) { + Log.e("QrisResultFlow", "Critical error: QR image URL is null or empty!"); + Toast.makeText(this, "Missing QR code URL! Cannot display QR code.", Toast.LENGTH_LONG).show(); } - // Set reference ID (hidden) - if (referenceId != null) { - referenceTextView.setText("Reference ID: " + referenceId); - } - - // Load initial QR image - if (qrImageUrl != null) { - loadQrCode(qrImageUrl); + // Display formatted amount + String formattedAmount = formatCurrency(grossAmount != null ? grossAmount : String.valueOf(amount)); + amountTextView.setText("Amount: " + formattedAmount); + referenceTextView.setText("Reference ID: " + referenceId); + + // Load QR image if URL is available + if (qrImageUrl != null && !qrImageUrl.isEmpty()) { + Log.d("QrisResultFlow", "Loading QR image from: " + qrImageUrl); + new DownloadImageTask(qrImageView).execute(qrImageUrl); + } else { + Log.w("QrisResultFlow", "QR image URL is not available"); + qrImageView.setVisibility(View.GONE); + downloadQrisButton.setEnabled(false); } + // Initialize UI state + checkStatusButton.setEnabled(false); + statusTextView.setText("Waiting for payment..."); + // Start polling for pending payment log - if (orderId != null) { - pollPendingPaymentLog(orderId); - } + pollPendingPaymentLog(orderId); + + // Set up click listeners + setupClickListeners(); + } + + private void setupClickListeners() { + // Download QRIS button + downloadQrisButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + downloadQrCode(); + } + }); + + // Check Payment Status button + checkStatusButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Log.d("QrisResultFlow", "Check status button clicked"); + simulateWebhook(); + } + }); + + // Return to Main Screen button + returnMainButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + 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 { - long number = Long.parseLong(amount.replace(".00", "")); - return String.format("%,d", number).replace(',', '.'); + double amountDouble = Double.parseDouble(amount); + NumberFormat formatter = NumberFormat.getCurrencyInstance(new Locale("id", "ID")); + return formatter.format(amountDouble); } catch (NumberFormatException e) { - return amount; + Log.w("QrisResultFlow", "Error formatting currency: " + e.getMessage()); + return "IDR " + amount; } } - private void startTimer() { - countDownTimer = new CountDownTimer(timeLeftInMillis, 1000) { - @Override - public void onTick(long millisUntilFinished) { - timeLeftInMillis = millisUntilFinished; - int seconds = (int) (millisUntilFinished / 1000); - timerTextView.setText(String.valueOf(seconds)); - - // Update status text based on remaining time - if (seconds > 10) { - qrStatusTextView.setText("QR Code akan refresh dalam " + seconds + " detik"); - qrStatusTextView.setTextColor(Color.parseColor("#666666")); - } else { - qrStatusTextView.setText("QR Code akan refresh dalam " + seconds + " detik"); - qrStatusTextView.setTextColor(Color.parseColor("#E31937")); - } + 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(); } - - @Override - public void onFinish() { - timerTextView.setText("0"); - qrStatusTextView.setText("Refreshing QR Code..."); - qrStatusTextView.setTextColor(Color.parseColor("#E31937")); - - // Auto refresh QR code - refreshQrCode(); - } - }.start(); - } - - private void refreshQrCode() { - qrRefreshCount++; - - if (qrRefreshCount >= MAX_QR_REFRESH) { - // Max refresh reached, show expired message - timerTextView.setText("Expired"); - qrStatusTextView.setText("QR Code telah expired. Silahkan generate ulang."); - qrStatusTextView.setTextColor(Color.parseColor("#E31937")); - Toast.makeText(this, "QR Code expired. Please generate new payment.", Toast.LENGTH_LONG).show(); - return; + } 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); } - - // Show loading state - progressBar.setVisibility(View.VISIBLE); - qrStatusTextView.setText("Generating new QR Code... (" + qrRefreshCount + "/" + MAX_QR_REFRESH + ")"); - - // Simulate generating new QR code - new Thread(() -> { - try { - // Simulate API call delay - Thread.sleep(2000); - - // Generate new QR code (in real app, call Midtrans API again) - generateNewQrCode(); - - } catch (InterruptedException e) { - e.printStackTrace(); - } - }).start(); - } - - private void generateNewQrCode() { - new Handler(Looper.getMainLooper()).post(() -> { - progressBar.setVisibility(View.GONE); - - // In real implementation, you would call Midtrans API again - // For demo, we'll reload the same QR with a timestamp to show refresh - String refreshedUrl = originalQrImageUrl + "?refresh=" + System.currentTimeMillis(); - loadQrCode(refreshedUrl); - - // Reset timer for new 60 seconds - timeLeftInMillis = 60000; - startTimer(); - - Toast.makeText(this, "QR Code refreshed! (" + qrRefreshCount + "/" + MAX_QR_REFRESH + ")", Toast.LENGTH_SHORT).show(); - }); - } - - private void loadQrCode(String qrImageUrl) { - new DownloadImageTask(qrImageView).execute(qrImageUrl); } private static class DownloadImageTask extends AsyncTask { - ImageView bmImage; + private ImageView bmImage; + private String errorMessage; + DownloadImageTask(ImageView bmImage) { this.bmImage = bmImage; } + @Override protected Bitmap doInBackground(String... urls) { String urlDisplay = urls[0]; Bitmap bitmap = null; try { + Log.d("QrisResultFlow", "Downloading image from: " + urlDisplay); URL url = new URI(urlDisplay).toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setDoInput(true); + connection.setConnectTimeout(10000); // 10 seconds + connection.setReadTimeout(10000); // 10 seconds + connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); connection.connect(); - InputStream input = connection.getInputStream(); - bitmap = BitmapFactory.decodeStream(input); + + 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) { - e.printStackTrace(); + 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); + // Show error message to user if available + if (errorMessage != null && bmImage.getContext() != null) { + Toast.makeText(bmImage.getContext(), errorMessage, Toast.LENGTH_LONG).show(); + } } } } @@ -325,31 +274,45 @@ public class QrisResultActivity extends AppCompatActivity { 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(); } } private void pollPendingPaymentLog(final String orderId) { - Log.d("QrisResultFlow", "Polling for orderId (transaction_uuid): " + orderId); + Log.d("QrisResultFlow", "Starting polling for orderId: " + orderId); progressBar.setVisibility(View.VISIBLE); + statusTextView.setText("Checking payment status..."); + new Thread(() -> { - int maxAttempts = 10; - int intervalMs = 1500; + int maxAttempts = 12; // Increased attempts + int intervalMs = 2000; // 2 seconds interval int attempt = 0; boolean found = false; + while (attempt < maxAttempts && !found) { try { String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}"; + Log.d("QrisResultFlow", "Polling attempt " + (attempt + 1) + "/" + maxAttempts); Log.d("QrisResultFlow", "Polling URL: " + urlStr); + URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + int responseCode = conn.getResponseCode(); + Log.d("QrisResultFlow", "Polling response code: " + responseCode); + if (responseCode == 200) { BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); StringBuilder response = new StringBuilder(); @@ -357,76 +320,160 @@ public class QrisResultActivity extends AppCompatActivity { while ((line = br.readLine()) != null) { response.append(line); } + + Log.d("QrisResultFlow", "Polling response: " + response.toString()); + 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 && "pending".equals(reqBody.optString("transaction_status"))) { - found = true; - break; + + 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 ("pending".equals(transactionStatus) && orderId.equals(logOrderId)) { + found = true; + Log.d("QrisResultFlow", "Found matching pending payment log!"); + break; + } } } + } else { + Log.d("QrisResultFlow", "No log entries found in response"); } + } else { + Log.w("QrisResultFlow", "Polling failed with HTTP code: " + responseCode); } } catch (Exception e) { - Log.e("QrisResultFlow", "Polling error: " + e.getMessage(), e); + Log.e("QrisResultFlow", "Polling error on attempt " + (attempt + 1) + ": " + e.getMessage(), e); } + if (!found) { attempt++; - try { Thread.sleep(intervalMs); } catch (InterruptedException ignored) {} + if (attempt < maxAttempts) { + try { + Thread.sleep(intervalMs); + } catch (InterruptedException ignored) { + Log.d("QrisResultFlow", "Polling interrupted"); + break; + } + } } } + final boolean logFound = found; new Handler(Looper.getMainLooper()).post(() -> { progressBar.setVisibility(View.GONE); if (logFound) { checkStatusButton.setEnabled(true); - Toast.makeText(QrisResultActivity.this, "Pending payment log found!", Toast.LENGTH_SHORT).show(); + statusTextView.setText("Ready to simulate payment"); + Toast.makeText(QrisResultActivity.this, "Pending payment log found! You can now simulate payment.", Toast.LENGTH_SHORT).show(); + Log.d("QrisResultFlow", "Polling completed successfully - payment log found"); } else { - Toast.makeText(QrisResultActivity.this, "Pending payment log NOT found.", Toast.LENGTH_LONG).show(); + statusTextView.setText("Payment log not found"); + Toast.makeText(QrisResultActivity.this, "Pending payment log NOT found. You may still try to simulate payment.", Toast.LENGTH_LONG).show(); + Log.w("QrisResultFlow", "Polling completed - payment log NOT found after " + maxAttempts + " attempts"); + // Enable button anyway to allow manual testing + 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"; + } + } + // Simulate webhook callback private void simulateWebhook() { + Log.d("QrisResultFlow", "Starting webhook simulation"); progressBar.setVisibility(View.VISIBLE); + statusTextView.setText("Simulating payment..."); + checkStatusButton.setEnabled(false); + new Thread(() -> { try { + // Generate proper signature + String serverKey = getServerKey(); + String signatureKey = generateSignature(orderId, "200", grossAmount, serverKey); + 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", transactionId); // Use the actual transaction_id + payload.put("transaction_id", transactionId); payload.put("status_message", "midtrans payment notification"); payload.put("status_code", "200"); - payload.put("signature_key", "dummy_signature"); + 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", orderId); // Use order_id + payload.put("order_id", orderId); payload.put("merchant_id", merchantId != null ? merchantId : "DUMMY_MERCHANT_ID"); payload.put("issuer", acquirer != null ? acquirer : "gopay"); - payload.put("gross_amount", grossAmount); // Use exact gross amount + payload.put("gross_amount", grossAmount); payload.put("fraud_status", "accept"); payload.put("currency", "IDR"); payload.put("acquirer", acquirer != null ? acquirer : "gopay"); payload.put("shopeepay_reference_number", ""); payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID"); + + Log.d("QrisResultFlow", "=== WEBHOOK SIMULATION ==="); + Log.d("QrisResultFlow", "Webhook URL: " + webhookUrl); Log.d("QrisResultFlow", "Webhook payload: " + payload.toString()); + Log.d("QrisResultFlow", "=========================="); URL url = new URL(webhookUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0"); conn.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", "Webhook response code: " + responseCode); + BufferedReader br = new BufferedReader(new InputStreamReader( responseCode < 400 ? conn.getInputStream() : conn.getErrorStream())); StringBuilder response = new StringBuilder(); @@ -434,58 +481,57 @@ public class QrisResultActivity extends AppCompatActivity { while ((line = br.readLine()) != null) { response.append(line); } + Log.d("QrisResultFlow", "Webhook response: " + response.toString()); + + // Wait a bit for processing + Thread.sleep(2000); + } catch (Exception e) { - Log.e("QrisResultFlow", "Webhook error: " + e.getMessage(), e); + Log.e("QrisResultFlow", "Webhook simulation error: " + e.getMessage(), e); } + new Handler(Looper.getMainLooper()).post(() -> { progressBar.setVisibility(View.GONE); - // Show success state - showSuccessState(); + showPaymentSuccess(); }); }).start(); } - private void showSuccessState() { - // Stop timer - if (countDownTimer != null) { - countDownTimer.cancel(); - } + private void showPaymentSuccess() { + Log.d("QrisResultFlow", "Showing payment success screen"); - // Hide QR section and show success + // Hide payment elements qrImageView.setVisibility(View.GONE); amountTextView.setVisibility(View.GONE); - timerTextView.setVisibility(View.GONE); - qrStatusTextView.setVisibility(View.GONE); + referenceTextView.setVisibility(View.GONE); downloadQrisButton.setVisibility(View.GONE); checkStatusButton.setVisibility(View.GONE); - cancelButton.setVisibility(View.GONE); - // Show success message - statusTextView.setText("Payment Successful!"); - statusTextView.setTextColor(Color.parseColor("#4CAF50")); - statusTextView.setTextSize(24); + // Show success elements statusTextView.setVisibility(View.VISIBLE); + statusTextView.setText("✅ Payment Successful!\n\nTransaction ID: " + transactionId + + "\nReference: " + referenceId + + "\nAmount: " + formatCurrency(grossAmount)); returnMainButton.setVisibility(View.VISIBLE); - returnMainButton.setText("Back to Main"); - returnMainButton.setBackgroundColor(Color.parseColor("#4CAF50")); - Toast.makeText(this, "Payment completed successfully!", Toast.LENGTH_LONG).show(); + Toast.makeText(this, "Payment simulation completed successfully!", Toast.LENGTH_LONG).show(); } @Override - protected void onDestroy() { - super.onDestroy(); - if (countDownTimer != null) { - countDownTimer.cancel(); + public boolean onOptionsItemSelected(android.view.MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; } + return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { - if (countDownTimer != null) { - countDownTimer.cancel(); - } - super.onBackPressed(); + 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(); } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java b/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java index 54e2c98..717cf08 100644 --- a/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java +++ b/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java @@ -8,6 +8,7 @@ import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import androidx.core.content.ContextCompat; import java.util.List; import java.text.NumberFormat; @@ -52,6 +53,14 @@ public class TransactionAdapter extends RecyclerView.Adapter { if (printClickListener != null) { printClickListener.onPrintClick(t); @@ -66,19 +75,77 @@ public class TransactionAdapter extends RecyclerView.Adapter - - + + android:orientation="vertical"> + + + + + + + + + + + + + + + +