midtrans solve

This commit is contained in:
riz081 2025-06-08 17:30:07 +07:00
parent edca7f92ec
commit a1f536b03e
4 changed files with 656 additions and 604 deletions

View File

@ -1,22 +1,13 @@
package com.example.bdkipoc; package com.example.bdkipoc;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
@ -28,6 +19,7 @@ import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -46,34 +38,24 @@ import java.util.UUID;
public class QrisActivity extends AppCompatActivity { public class QrisActivity extends AppCompatActivity {
// Views private ProgressBar progressBar;
private EditText editTextAmount;
private Button initiatePaymentButton; private Button initiatePaymentButton;
private TextView statusTextView;
private EditText editTextAmount;
private TextView referenceIdTextView;
private LinearLayout backNavigation; private LinearLayout backNavigation;
private TextView toolbarTitle;
private TextView descriptionText;
// Numpad buttons // Numpad buttons
private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000; private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000, btnDelete;
private TextView btnDelete; // Changed from ImageView to TextView private TextView descriptionText;
// Only needed views for loading state
private ProgressBar progressBar;
private TextView statusTextView;
private TextView referenceIdTextView;
private LinearLayout initialPaymentLayout;
// Data
private StringBuilder currentAmount = new StringBuilder();
private static final int MAX_AMOUNT_LENGTH = 12;
private String transactionId; private String transactionId;
private String transactionUuid; private String transactionUuid;
private String referenceId; private String referenceId;
private int amount; private int amount;
private JSONObject midtransResponse; private JSONObject midtransResponse;
// Animation private StringBuilder currentAmount = new StringBuilder();
private Handler animationHandler = new Handler(Looper.getMainLooper());
private static final String BACKEND_BASE = "https://be-edc.msvc.app"; 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"; private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
@ -83,49 +65,18 @@ public class QrisActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Set status bar color programmatically
setStatusBarColor();
setContentView(R.layout.activity_qris); setContentView(R.layout.activity_qris);
initializeViews(); // Initialize views
setupClickListeners(); progressBar = findViewById(R.id.progressBar);
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);
initiatePaymentButton = findViewById(R.id.initiatePaymentButton); 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); backNavigation = findViewById(R.id.back_navigation);
toolbarTitle = findViewById(R.id.toolbarTitle);
descriptionText = findViewById(R.id.descriptionText); descriptionText = findViewById(R.id.descriptionText);
// Numpad buttons // Initialize numpad buttons
btn1 = findViewById(R.id.btn1); btn1 = findViewById(R.id.btn1);
btn2 = findViewById(R.id.btn2); btn2 = findViewById(R.id.btn2);
btn3 = findViewById(R.id.btn3); btn3 = findViewById(R.id.btn3);
@ -137,232 +88,102 @@ public class QrisActivity extends AppCompatActivity {
btn9 = findViewById(R.id.btn9); btn9 = findViewById(R.id.btn9);
btn0 = findViewById(R.id.btn0); btn0 = findViewById(R.id.btn0);
btn000 = findViewById(R.id.btn000); btn000 = findViewById(R.id.btn000);
btnDelete = findViewById(R.id.btnDelete); // Now TextView btnDelete = findViewById(R.id.btnDelete);
// Only needed views for loading state // Generate reference ID
progressBar = findViewById(R.id.progressBar); referenceId = "ref-" + generateRandomString(8);
statusTextView = findViewById(R.id.statusTextView); referenceIdTextView.setText(referenceId);
referenceIdTextView = findViewById(R.id.referenceIdTextView);
// Set up click listeners
initiatePaymentButton.setOnClickListener(v -> createTransaction());
backNavigation.setOnClickListener(v -> finish());
// Main content layout (replaces initialPaymentLayout) // Set up numpad listeners
initialPaymentLayout = findViewById(R.id.mainContentLayout); setupNumpadListeners();
}
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 initial button state // Initially disable the button
updateButtonState(); 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) btnDelete.setOnClickListener(v -> deleteLastDigit());
editTextAmount.setFocusable(false);
editTextAmount.setClickable(false);
editTextAmount.setCursorVisible(false);
} }
private void navigateBack() { private void appendNumber(String number) {
finish(); currentAmount.append(number);
}
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);
}
updateAmountDisplay(); updateAmountDisplay();
updateButtonState();
addInputFeedback();
} }
private void deleteLastDigit() { private void deleteLastDigit() {
if (currentAmount.length() > 0) { if (currentAmount.length() > 0) {
String current = currentAmount.toString(); currentAmount.deleteCharAt(currentAmount.length() - 1);
// 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);
}
updateAmountDisplay(); updateAmountDisplay();
updateButtonState();
addDeleteFeedback();
} }
} }
private void updateAmountDisplay() { private void updateAmountDisplay() {
String amount = currentAmount.toString(); String amountStr = currentAmount.toString();
if (amount.isEmpty() || amount.equals("0")) { if (amountStr.isEmpty()) {
// Show description text, hide amount input
editTextAmount.setVisibility(View.GONE); editTextAmount.setVisibility(View.GONE);
descriptionText.setVisibility(View.VISIBLE); descriptionText.setText("Pastikan kembali nominal pembayaran pelanggan Anda");
initiatePaymentButton.setEnabled(false);
} else { } else {
// Show amount input, hide description text
String formattedAmount = formatCurrency(amount);
editTextAmount.setText(formattedAmount);
editTextAmount.setVisibility(View.VISIBLE); 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) { private String formatAmount(String amount) {
if (TextUtils.isEmpty(amount) || amount.equals("0")) { if (amount.isEmpty()) return "";
return "";
}
try { try {
long number = Long.parseLong(amount); long num = Long.parseLong(amount);
return String.format("%,d", number).replace(',', '.'); return String.format("%,d", num);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
return amount; 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() { private void createTransaction() {
String amountText = currentAmount.toString(); if (currentAmount.length() == 0) {
Toast.makeText(this, "Masukkan jumlah pembayaran", Toast.LENGTH_SHORT).show();
if (TextUtils.isEmpty(amountText) || amountText.equals("0")) {
showToast("Masukkan jumlah pembayaran");
return; return;
} }
try { progressBar.setVisibility(View.VISIBLE);
long amountValue = Long.parseLong(amountText); initiatePaymentButton.setEnabled(false);
statusTextView.setVisibility(View.VISIBLE);
// Validate minimum amount statusTextView.setText("Creating transaction...");
if (amountValue < 1000) {
showToast("Minimal pembayaran Rp 1.000"); new CreateTransactionTask().execute();
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
} }
private String generateRandomString(int length) { private String generateRandomString(int length) {
@ -377,11 +198,23 @@ public class QrisActivity extends AppCompatActivity {
} }
private String getServerKey() { private String getServerKey() {
// MIDTRANS_AUTH = 'Basic base64string' try {
String base64 = MIDTRANS_AUTH.replace("Basic ", ""); // MIDTRANS_AUTH = 'Basic base64string'
String decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT).toString(); String base64 = MIDTRANS_AUTH.replace("Basic ", "");
// Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present. byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT);
return decoded.replace(":\n", ""); 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) { private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
@ -397,52 +230,18 @@ public class QrisActivity extends AppCompatActivity {
} }
return hexString.toString(); return hexString.toString();
} catch (java.security.NoSuchAlgorithmException e) { } catch (java.security.NoSuchAlgorithmException e) {
Log.e("MidtransCharge", "Error generating signature: " + e.getMessage());
return ""; 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 @Override
public void onBackPressed() { public boolean onOptionsItemSelected(MenuItem item) {
// Simple back navigation if (item.getItemId() == android.R.id.home) {
navigateBack(); finish();
return true;
}
return super.onOptionsItemSelected(item);
} }
private class CreateTransactionTask extends AsyncTask<Void, Void, Boolean> { private class CreateTransactionTask extends AsyncTask<Void, Void, Boolean> {
@ -461,7 +260,32 @@ public class QrisActivity extends AppCompatActivity {
payload.put("channel_code", "QRIS"); payload.put("channel_code", "QRIS");
payload.put("reference_id", referenceId); 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("amount", amount);
payload.put("cashflow", "MONEY_IN"); payload.put("cashflow", "MONEY_IN");
@ -474,12 +298,15 @@ public class QrisActivity extends AppCompatActivity {
payload.put("mid", "71000026521"); payload.put("mid", "71000026521");
payload.put("tid", "73001500"); payload.put("tid", "73001500");
Log.d("MidtransCharge", "Backend transaction payload: " + payload.toString());
// Make the API call // Make the API call
URL url = new URI(BACKEND_BASE + "/transactions").toURL(); URL url = new URI(BACKEND_BASE + "/transactions").toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST"); conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept", "application/json"); conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
conn.setDoOutput(true); conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) { try (OutputStream os = conn.getOutputStream()) {
@ -488,6 +315,8 @@ public class QrisActivity extends AppCompatActivity {
} }
int responseCode = conn.getResponseCode(); int responseCode = conn.getResponseCode();
Log.d("MidtransCharge", "Backend response code: " + responseCode);
if (responseCode == 200 || responseCode == 201) { if (responseCode == 200 || responseCode == 201) {
// Read the response // Read the response
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
@ -497,11 +326,15 @@ public class QrisActivity extends AppCompatActivity {
response.append(responseLine.trim()); response.append(responseLine.trim());
} }
Log.d("MidtransCharge", "Backend response: " + response.toString());
// Parse the response to get transaction ID // Parse the response to get transaction ID
JSONObject jsonResponse = new JSONObject(response.toString()); JSONObject jsonResponse = new JSONObject(response.toString());
JSONObject data = jsonResponse.getJSONObject("data"); JSONObject data = jsonResponse.getJSONObject("data");
transactionId = String.valueOf(data.getInt("id")); transactionId = String.valueOf(data.getInt("id"));
Log.d("MidtransCharge", "Created transaction ID: " + transactionId);
// Now generate QRIS via Midtrans // Now generate QRIS via Midtrans
return generateQris(amount); return generateQris(amount);
} else { } else {
@ -512,18 +345,29 @@ public class QrisActivity extends AppCompatActivity {
while ((responseLine = br.readLine()) != null) { while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim()); 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; return false;
} }
} catch (Exception e) { } catch (Exception e) {
Log.e("MidtransCharge", "Exception: " + e.getMessage(), e); Log.e("MidtransCharge", "Backend transaction exception: " + e.getMessage(), e);
errorMessage = "Unexpected error: " + e.getMessage(); errorMessage = "Backend transaction error: " + e.getMessage();
return false; return false;
} }
} }
private boolean generateQris(int amount) { private boolean generateQris(int amount) {
try { 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 // Create QRIS charge JSON payload
JSONObject payload = new JSONObject(); JSONObject payload = new JSONObject();
payload.put("payment_type", "qris"); payload.put("payment_type", "qris");
@ -533,13 +377,31 @@ public class QrisActivity extends AppCompatActivity {
transactionDetails.put("gross_amount", amount); transactionDetails.put("gross_amount", amount);
payload.put("transaction_details", transactionDetails); 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 the request details
Log.d("MidtransCharge", "=== MIDTRANS QRIS REQUEST ===");
Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL); Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL);
Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH); 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", "X-Override-Notification: " + WEBHOOK_URL);
Log.d("MidtransCharge", "Payload: " + payload.toString()); Log.d("MidtransCharge", "Payload: " + payload.toString());
Log.d("MidtransCharge", "================================");
// Make the API call to Midtrans // Make the API call to Midtrans
URL url = new URI(MIDTRANS_CHARGE_URL).toURL(); 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("Content-Type", "application/json");
conn.setRequestProperty("Authorization", MIDTRANS_AUTH); conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
conn.setRequestProperty("X-Override-Notification", WEBHOOK_URL); conn.setRequestProperty("X-Override-Notification", WEBHOOK_URL);
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
conn.setDoOutput(true); conn.setDoOutput(true);
conn.setConnectTimeout(30000); // 30 seconds
conn.setReadTimeout(30000); // 30 seconds
try (OutputStream os = conn.getOutputStream()) { try (OutputStream os = conn.getOutputStream()) {
byte[] input = payload.toString().getBytes("utf-8"); byte[] input = payload.toString().getBytes("utf-8");
@ -557,6 +422,8 @@ public class QrisActivity extends AppCompatActivity {
} }
int responseCode = conn.getResponseCode(); int responseCode = conn.getResponseCode();
Log.d("MidtransCharge", "Midtrans HTTP Response Code: " + responseCode);
if (responseCode == 200 || responseCode == 201) { if (responseCode == 200 || responseCode == 201) {
InputStream inputStream = conn.getInputStream(); InputStream inputStream = conn.getInputStream();
if (inputStream != null) { if (inputStream != null) {
@ -567,8 +434,32 @@ public class QrisActivity extends AppCompatActivity {
response.append(responseLine.trim()); response.append(responseLine.trim());
} }
Log.d("MidtransCharge", "Midtrans Success Response: " + response.toString());
// Parse the response // Parse the response
midtransResponse = new JSONObject(response.toString()); 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; return true;
} else { } else {
Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available"); Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available");
@ -584,17 +475,38 @@ public class QrisActivity extends AppCompatActivity {
while ((responseLine = br.readLine()) != null) { while ((responseLine = br.readLine()) != null) {
errorResponse.append(responseLine.trim()); 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 { } else {
Log.e("MidtransCharge", "HTTP " + responseCode + ": No error stream available"); 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; return false;
} }
} catch (Exception e) { } catch (Exception e) {
Log.e("MidtransCharge", "Exception: " + e.getMessage(), e); Log.e("MidtransCharge", "Midtrans QRIS generation exception: " + e.getMessage(), e);
errorMessage = "Unexpected error: " + e.getMessage(); errorMessage = "Network error: " + e.getMessage();
return false; return false;
} }
} }
@ -604,7 +516,17 @@ public class QrisActivity extends AppCompatActivity {
if (success && midtransResponse != null) { if (success && midtransResponse != null) {
try { try {
// Extract needed values from midtransResponse // 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"); String qrImageUrl = actions.getString("url");
// Extract transaction_id // Extract transaction_id
@ -615,15 +537,19 @@ public class QrisActivity extends AppCompatActivity {
String exactGrossAmount = midtransResponse.getString("gross_amount"); String exactGrossAmount = midtransResponse.getString("gross_amount");
// Log everything before launching activity // 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", "qrImageUrl: " + qrImageUrl);
Log.d("MidtransCharge", "amount: " + amount); Log.d("MidtransCharge", "amount: " + amount);
Log.d("MidtransCharge", "referenceId: " + referenceId); Log.d("MidtransCharge", "referenceId: " + referenceId);
Log.d("MidtransCharge", "transactionUuid (orderId): " + transactionUuid); Log.d("MidtransCharge", "transactionUuid (orderId): " + transactionUuid);
Log.d("MidtransCharge", "transaction_id: " + transactionId); Log.d("MidtransCharge", "transaction_id: " + transactionId);
Log.d("MidtransCharge", "exactGrossAmount: " + exactGrossAmount); 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 intent = new Intent(QrisActivity.this, QrisResultActivity.class);
intent.putExtra("qrImageUrl", qrImageUrl); intent.putExtra("qrImageUrl", qrImageUrl);
intent.putExtra("amount", amount); intent.putExtra("amount", amount);
@ -637,54 +563,24 @@ public class QrisActivity extends AppCompatActivity {
try { try {
startActivity(intent); startActivity(intent);
// Finish this activity so user can't go back to input form finish(); // Close QrisActivity
finish();
} catch (Exception e) { } catch (Exception e) {
Log.e("MidtransCharge", "Failed to start QrisResultActivity: " + e.getMessage(), 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(); Toast.makeText(QrisActivity.this, "Error launching QR display: " + e.getMessage(), Toast.LENGTH_LONG).show();
resetToInitialState();
} }
return;
} catch (JSONException e) { } catch (JSONException e) {
Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e); Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e);
Toast.makeText(QrisActivity.this, "Error processing QRIS response", Toast.LENGTH_LONG).show(); Toast.makeText(QrisActivity.this, "Error processing QRIS response: " + e.getMessage(), Toast.LENGTH_LONG).show();
resetToInitialState();
} }
} else { } 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(); Toast.makeText(QrisActivity.this, message, Toast.LENGTH_LONG).show();
resetToInitialState(); initiatePaymentButton.setEnabled(true);
} }
progressBar.setVisibility(View.GONE); 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();
}
} }

View File

@ -3,26 +3,21 @@ package com.example.bdkipoc;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -35,28 +30,18 @@ import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.text.NumberFormat;
import java.util.Locale;
public class QrisResultActivity extends AppCompatActivity { public class QrisResultActivity extends AppCompatActivity {
private ImageView qrImageView; private ImageView qrImageView;
private TextView amountTextView; private TextView amountTextView;
private TextView referenceTextView; private TextView referenceTextView;
private TextView timerTextView;
private TextView qrStatusTextView;
private Button downloadQrisButton; private Button downloadQrisButton;
private Button checkStatusButton; private Button checkStatusButton;
private Button cancelButton;
private TextView statusTextView; private TextView statusTextView;
private Button returnMainButton; private Button returnMainButton;
private ProgressBar progressBar; 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 orderId;
private String grossAmount; private String grossAmount;
private String referenceId; private String referenceId;
@ -64,102 +49,42 @@ public class QrisResultActivity extends AppCompatActivity {
private String transactionTime; private String transactionTime;
private String acquirer; private String acquirer;
private String merchantId; private String merchantId;
private String originalQrImageUrl;
private int amount;
private String backendBase = "https://be-edc.msvc.app"; private String backendBase = "https://be-edc.msvc.app";
private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans"; private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans";
// Server key for signature generation
private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM=";
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Set status bar color programmatically
setStatusBarColor();
setContentView(R.layout.activity_qris_result); setContentView(R.layout.activity_qris_result);
initializeViews(); // Set up the toolbar
setupClickListeners(); Toolbar toolbar = findViewById(R.id.toolbar);
setupData(); if (toolbar != null) {
startTimer(); setSupportActionBar(toolbar);
} if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
private void setStatusBarColor() { getSupportActionBar().setDisplayShowHomeEnabled(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getSupportActionBar().setTitle("QRIS Payment");
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() { // Initialize views
qrImageView = findViewById(R.id.qrImageView); qrImageView = findViewById(R.id.qrImageView);
amountTextView = findViewById(R.id.amountTextView); amountTextView = findViewById(R.id.amountTextView);
referenceTextView = findViewById(R.id.referenceTextView); referenceTextView = findViewById(R.id.referenceTextView);
timerTextView = findViewById(R.id.timerTextView);
qrStatusTextView = findViewById(R.id.qrStatusTextView);
downloadQrisButton = findViewById(R.id.downloadQrisButton); downloadQrisButton = findViewById(R.id.downloadQrisButton);
checkStatusButton = findViewById(R.id.checkStatusButton); checkStatusButton = findViewById(R.id.checkStatusButton);
cancelButton = findViewById(R.id.cancelButton);
statusTextView = findViewById(R.id.statusTextView); statusTextView = findViewById(R.id.statusTextView);
returnMainButton = findViewById(R.id.returnMainButton); returnMainButton = findViewById(R.id.returnMainButton);
progressBar = findViewById(R.id.progressBar); progressBar = findViewById(R.id.progressBar);
backNavigation = findViewById(R.id.back_navigation);
}
private void setupClickListeners() { // Get intent data
// 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() {
Intent intent = getIntent(); Intent intent = getIntent();
String qrImageUrl = intent.getStringExtra("qrImageUrl"); String qrImageUrl = intent.getStringExtra("qrImageUrl");
originalQrImageUrl = qrImageUrl; // Store original URL for refresh int amount = intent.getIntExtra("amount", 0);
amount = intent.getIntExtra("amount", 0);
referenceId = intent.getStringExtra("referenceId"); referenceId = intent.getStringExtra("referenceId");
orderId = intent.getStringExtra("orderId"); orderId = intent.getStringExtra("orderId");
grossAmount = intent.getStringExtra("grossAmount"); grossAmount = intent.getStringExtra("grossAmount");
@ -168,152 +93,176 @@ public class QrisResultActivity extends AppCompatActivity {
acquirer = intent.getStringExtra("acquirer"); acquirer = intent.getStringExtra("acquirer");
merchantId = intent.getStringExtra("merchantId"); 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) { if (orderId == null || transactionId == null) {
Log.e("QrisResultFlow", "orderId or transactionId is null! Intent extras: " + intent.getExtras()); Log.e("QrisResultFlow", "Critical error: orderId or transactionId is null!");
Toast.makeText(this, "Missing transaction details!", Toast.LENGTH_LONG).show(); 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 (qrImageUrl == null || qrImageUrl.isEmpty()) {
if (grossAmount != null) { Log.e("QrisResultFlow", "Critical error: QR image URL is null or empty!");
String formattedAmount = "RP." + formatCurrency(grossAmount); Toast.makeText(this, "Missing QR code URL! Cannot display QR code.", Toast.LENGTH_LONG).show();
amountTextView.setText(formattedAmount);
} else if (amount > 0) {
String formattedAmount = "RP." + formatCurrency(String.valueOf(amount));
amountTextView.setText(formattedAmount);
} }
// Set reference ID (hidden) // Display formatted amount
if (referenceId != null) { String formattedAmount = formatCurrency(grossAmount != null ? grossAmount : String.valueOf(amount));
referenceTextView.setText("Reference ID: " + referenceId); amountTextView.setText("Amount: " + formattedAmount);
} referenceTextView.setText("Reference ID: " + referenceId);
// Load initial QR image // Load QR image if URL is available
if (qrImageUrl != null) { if (qrImageUrl != null && !qrImageUrl.isEmpty()) {
loadQrCode(qrImageUrl); 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 // 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) { private String formatCurrency(String amount) {
try { try {
long number = Long.parseLong(amount.replace(".00", "")); double amountDouble = Double.parseDouble(amount);
return String.format("%,d", number).replace(',', '.'); NumberFormat formatter = NumberFormat.getCurrencyInstance(new Locale("id", "ID"));
return formatter.format(amountDouble);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
return amount; Log.w("QrisResultFlow", "Error formatting currency: " + e.getMessage());
return "IDR " + amount;
} }
} }
private void startTimer() { private void downloadQrCode() {
countDownTimer = new CountDownTimer(timeLeftInMillis, 1000) { try {
@Override qrImageView.setDrawingCacheEnabled(true);
public void onTick(long millisUntilFinished) { qrImageView.buildDrawingCache();
timeLeftInMillis = millisUntilFinished; Bitmap bitmap = qrImageView.getDrawingCache();
int seconds = (int) (millisUntilFinished / 1000); if (bitmap != null) {
timerTextView.setText(String.valueOf(seconds)); saveImageToGallery(bitmap, "qris_code_" + System.currentTimeMillis());
} else {
// Update status text based on remaining time Toast.makeText(this, "Unable to capture QR code image", Toast.LENGTH_SHORT).show();
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"));
}
} }
} catch (Exception e) {
@Override Log.e("QrisResultFlow", "Error downloading QR code: " + e.getMessage(), e);
public void onFinish() { Toast.makeText(this, "Error downloading QR code: " + e.getMessage(), Toast.LENGTH_LONG).show();
timerTextView.setText("0"); } finally {
qrStatusTextView.setText("Refreshing QR Code..."); qrImageView.setDrawingCacheEnabled(false);
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;
} }
// 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<String, Void, Bitmap> { private static class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
ImageView bmImage; private ImageView bmImage;
private String errorMessage;
DownloadImageTask(ImageView bmImage) { DownloadImageTask(ImageView bmImage) {
this.bmImage = bmImage; this.bmImage = bmImage;
} }
@Override
protected Bitmap doInBackground(String... urls) { protected Bitmap doInBackground(String... urls) {
String urlDisplay = urls[0]; String urlDisplay = urls[0];
Bitmap bitmap = null; Bitmap bitmap = null;
try { try {
Log.d("QrisResultFlow", "Downloading image from: " + urlDisplay);
URL url = new URI(urlDisplay).toURL(); URL url = new URI(urlDisplay).toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true); connection.setDoInput(true);
connection.setConnectTimeout(10000); // 10 seconds
connection.setReadTimeout(10000); // 10 seconds
connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
connection.connect(); 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) { } catch (Exception e) {
e.printStackTrace(); Log.e("QrisResultFlow", "Exception downloading image: " + e.getMessage(), e);
errorMessage = "Error downloading QR code: " + e.getMessage();
} }
return bitmap; return bitmap;
} }
@Override
protected void onPostExecute(Bitmap result) { protected void onPostExecute(Bitmap result) {
if (result != null) { if (result != null) {
bmImage.setImageBitmap(result); 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"); getContentResolver(), bitmap, fileName, "QRIS Payment QR Code");
if (savedImageURL != null) { if (savedImageURL != null) {
Toast.makeText(this, "QRIS saved to gallery", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "QRIS saved to gallery", Toast.LENGTH_SHORT).show();
Log.d("QrisResultFlow", "QR code saved to gallery: " + savedImageURL);
} else { } else {
Toast.makeText(this, "Failed to save QRIS", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Failed to save QRIS", Toast.LENGTH_SHORT).show();
Log.e("QrisResultFlow", "Failed to save QR code to gallery");
} }
} catch (Exception e) { } 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(); Toast.makeText(this, "Error saving QRIS: " + e.getMessage(), Toast.LENGTH_LONG).show();
} }
} }
private void pollPendingPaymentLog(final String orderId) { 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); progressBar.setVisibility(View.VISIBLE);
statusTextView.setText("Checking payment status...");
new Thread(() -> { new Thread(() -> {
int maxAttempts = 10; int maxAttempts = 12; // Increased attempts
int intervalMs = 1500; int intervalMs = 2000; // 2 seconds interval
int attempt = 0; int attempt = 0;
boolean found = false; boolean found = false;
while (attempt < maxAttempts && !found) { while (attempt < maxAttempts && !found) {
try { try {
String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}"; 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); Log.d("QrisResultFlow", "Polling URL: " + urlStr);
URL url = new URL(urlStr); URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET"); conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json"); conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int responseCode = conn.getResponseCode(); int responseCode = conn.getResponseCode();
Log.d("QrisResultFlow", "Polling response code: " + responseCode);
if (responseCode == 200) { if (responseCode == 200) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder(); StringBuilder response = new StringBuilder();
@ -357,76 +320,160 @@ public class QrisResultActivity extends AppCompatActivity {
while ((line = br.readLine()) != null) { while ((line = br.readLine()) != null) {
response.append(line); response.append(line);
} }
Log.d("QrisResultFlow", "Polling response: " + response.toString());
JSONObject json = new JSONObject(response.toString()); JSONObject json = new JSONObject(response.toString());
JSONArray results = json.optJSONArray("results"); JSONArray results = json.optJSONArray("results");
if (results != null && results.length() > 0) { if (results != null && results.length() > 0) {
Log.d("QrisResultFlow", "Found " + results.length() + " log entries");
for (int i = 0; i < results.length(); i++) { for (int i = 0; i < results.length(); i++) {
JSONObject log = results.getJSONObject(i); JSONObject log = results.getJSONObject(i);
JSONObject reqBody = log.optJSONObject("request_body"); JSONObject reqBody = log.optJSONObject("request_body");
if (reqBody != null && "pending".equals(reqBody.optString("transaction_status"))) {
found = true; if (reqBody != null) {
break; 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) { } 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) { if (!found) {
attempt++; 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; final boolean logFound = found;
new Handler(Looper.getMainLooper()).post(() -> { new Handler(Looper.getMainLooper()).post(() -> {
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
if (logFound) { if (logFound) {
checkStatusButton.setEnabled(true); 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 { } 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(); }).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 // Simulate webhook callback
private void simulateWebhook() { private void simulateWebhook() {
Log.d("QrisResultFlow", "Starting webhook simulation");
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
statusTextView.setText("Simulating payment...");
checkStatusButton.setEnabled(false);
new Thread(() -> { new Thread(() -> {
try { try {
// Generate proper signature
String serverKey = getServerKey();
String signatureKey = generateSignature(orderId, "200", grossAmount, serverKey);
JSONObject payload = new JSONObject(); JSONObject payload = new JSONObject();
payload.put("transaction_type", "on-us"); payload.put("transaction_type", "on-us");
payload.put("transaction_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z"); payload.put("transaction_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
payload.put("transaction_status", "settlement"); 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_message", "midtrans payment notification");
payload.put("status_code", "200"); 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("settlement_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
payload.put("payment_type", "qris"); 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("merchant_id", merchantId != null ? merchantId : "DUMMY_MERCHANT_ID");
payload.put("issuer", acquirer != null ? acquirer : "gopay"); 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("fraud_status", "accept");
payload.put("currency", "IDR"); payload.put("currency", "IDR");
payload.put("acquirer", acquirer != null ? acquirer : "gopay"); payload.put("acquirer", acquirer != null ? acquirer : "gopay");
payload.put("shopeepay_reference_number", ""); payload.put("shopeepay_reference_number", "");
payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID"); 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", "Webhook payload: " + payload.toString());
Log.d("QrisResultFlow", "==========================");
URL url = new URL(webhookUrl); URL url = new URL(webhookUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST"); conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json"); conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
conn.setDoOutput(true); conn.setDoOutput(true);
conn.setConnectTimeout(15000);
conn.setReadTimeout(15000);
OutputStream os = conn.getOutputStream(); OutputStream os = conn.getOutputStream();
os.write(payload.toString().getBytes()); os.write(payload.toString().getBytes());
os.flush(); os.flush();
os.close(); os.close();
int responseCode = conn.getResponseCode(); int responseCode = conn.getResponseCode();
Log.d("QrisResultFlow", "Webhook response code: " + responseCode);
BufferedReader br = new BufferedReader(new InputStreamReader( BufferedReader br = new BufferedReader(new InputStreamReader(
responseCode < 400 ? conn.getInputStream() : conn.getErrorStream())); responseCode < 400 ? conn.getInputStream() : conn.getErrorStream()));
StringBuilder response = new StringBuilder(); StringBuilder response = new StringBuilder();
@ -434,58 +481,57 @@ public class QrisResultActivity extends AppCompatActivity {
while ((line = br.readLine()) != null) { while ((line = br.readLine()) != null) {
response.append(line); response.append(line);
} }
Log.d("QrisResultFlow", "Webhook response: " + response.toString()); Log.d("QrisResultFlow", "Webhook response: " + response.toString());
// Wait a bit for processing
Thread.sleep(2000);
} catch (Exception e) { } 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(() -> { new Handler(Looper.getMainLooper()).post(() -> {
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
// Show success state showPaymentSuccess();
showSuccessState();
}); });
}).start(); }).start();
} }
private void showSuccessState() { private void showPaymentSuccess() {
// Stop timer Log.d("QrisResultFlow", "Showing payment success screen");
if (countDownTimer != null) {
countDownTimer.cancel();
}
// Hide QR section and show success // Hide payment elements
qrImageView.setVisibility(View.GONE); qrImageView.setVisibility(View.GONE);
amountTextView.setVisibility(View.GONE); amountTextView.setVisibility(View.GONE);
timerTextView.setVisibility(View.GONE); referenceTextView.setVisibility(View.GONE);
qrStatusTextView.setVisibility(View.GONE);
downloadQrisButton.setVisibility(View.GONE); downloadQrisButton.setVisibility(View.GONE);
checkStatusButton.setVisibility(View.GONE); checkStatusButton.setVisibility(View.GONE);
cancelButton.setVisibility(View.GONE);
// Show success message // Show success elements
statusTextView.setText("Payment Successful!");
statusTextView.setTextColor(Color.parseColor("#4CAF50"));
statusTextView.setTextSize(24);
statusTextView.setVisibility(View.VISIBLE); statusTextView.setVisibility(View.VISIBLE);
statusTextView.setText("✅ Payment Successful!\n\nTransaction ID: " + transactionId +
"\nReference: " + referenceId +
"\nAmount: " + formatCurrency(grossAmount));
returnMainButton.setVisibility(View.VISIBLE); 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 @Override
protected void onDestroy() { public boolean onOptionsItemSelected(android.view.MenuItem item) {
super.onDestroy(); if (item.getItemId() == android.R.id.home) {
if (countDownTimer != null) { onBackPressed();
countDownTimer.cancel(); return true;
} }
return super.onOptionsItemSelected(item);
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (countDownTimer != null) { Intent intent = new Intent(this, com.example.bdkipoc.MainActivity.class);
countDownTimer.cancel(); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
} startActivity(intent);
super.onBackPressed(); finishAffinity();
} }
} }

View File

@ -8,6 +8,7 @@ import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.core.content.ContextCompat;
import java.util.List; import java.util.List;
import java.text.NumberFormat; import java.text.NumberFormat;
@ -52,6 +53,14 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
holder.amount.setText("Rp. " + t.amount); holder.amount.setText("Rp. " + t.amount);
} }
// Set status with appropriate color
holder.status.setText(t.status.toUpperCase());
setStatusColor(holder.status, t.status);
// Set payment method
String paymentMethod = getPaymentMethodName(t.channelCode, t.channelCategory);
holder.paymentMethod.setText(paymentMethod);
holder.itemView.setOnClickListener(v -> { holder.itemView.setOnClickListener(v -> {
if (printClickListener != null) { if (printClickListener != null) {
printClickListener.onPrintClick(t); printClickListener.onPrintClick(t);
@ -66,19 +75,77 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
}); });
} }
private void setStatusColor(TextView statusTextView, String status) {
String statusLower = status.toLowerCase();
int color;
if (statusLower.equals("failed") || statusLower.equals("failure") ||
statusLower.equals("error") || statusLower.equals("declined")) {
// Red for failed statuses
color = ContextCompat.getColor(statusTextView.getContext(), android.R.color.holo_red_dark);
} else if (statusLower.equals("success") || statusLower.equals("paid") ||
statusLower.equals("settlement") || statusLower.equals("completed")) {
// Green for successful statuses
color = ContextCompat.getColor(statusTextView.getContext(), android.R.color.holo_green_dark);
} else if (statusLower.equals("pending") || statusLower.equals("processing") ||
statusLower.equals("waiting") || statusLower.equals("init")) {
// Orange/Yellow for pending statuses
color = ContextCompat.getColor(statusTextView.getContext(), android.R.color.holo_orange_dark);
} else {
// Default gray for unknown statuses
color = ContextCompat.getColor(statusTextView.getContext(), android.R.color.darker_gray);
}
statusTextView.setTextColor(color);
}
private String getPaymentMethodName(String channelCode, String channelCategory) {
// Convert channel code to readable payment method name
if (channelCode == null) return "Unknown";
switch (channelCode.toUpperCase()) {
case "QRIS":
return "QRIS";
case "DEBIT":
return "Kartu Debit";
case "CREDIT":
return "Kartu Kredit";
case "BCA":
return "BCA";
case "MANDIRI":
return "Mandiri";
case "BNI":
return "BNI";
case "BRI":
return "BRI";
case "CASH":
return "Tunai";
case "EDC":
return "EDC";
default:
// If channel category is available, use it as fallback
if (channelCategory != null && !channelCategory.isEmpty()) {
return channelCategory.toUpperCase();
}
return channelCode.toUpperCase();
}
}
@Override @Override
public int getItemCount() { public int getItemCount() {
return transactionList.size(); return transactionList.size();
} }
static class TransactionViewHolder extends RecyclerView.ViewHolder { static class TransactionViewHolder extends RecyclerView.ViewHolder {
TextView amount, referenceId; TextView amount, referenceId, status, paymentMethod;
LinearLayout printSection; LinearLayout printSection;
public TransactionViewHolder(@NonNull View itemView) { public TransactionViewHolder(@NonNull View itemView) {
super(itemView); super(itemView);
amount = itemView.findViewById(R.id.textAmount); amount = itemView.findViewById(R.id.textAmount);
referenceId = itemView.findViewById(R.id.textReferenceId); referenceId = itemView.findViewById(R.id.textReferenceId);
status = itemView.findViewById(R.id.textStatus);
paymentMethod = itemView.findViewById(R.id.textPaymentMethod);
printSection = itemView.findViewById(R.id.printSection); printSection = itemView.findViewById(R.id.printSection);
} }
} }

View File

@ -13,16 +13,59 @@
android:padding="16dp" android:padding="16dp"
android:gravity="center_vertical"> android:gravity="center_vertical">
<!-- Kolom 1: Reference ID --> <!-- Kolom 1: Transaction Info -->
<TextView <LinearLayout
android:id="@+id/textReferenceId"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="ref-eowu3pin" android:orientation="vertical">
android:textColor="#333333"
android:textSize="16sp" <!-- Reference ID -->
android:textStyle="bold" /> <TextView
android:id="@+id/textReferenceId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ref-eowu3pin"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold" />
<!-- Status and Payment Method -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/textStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SUCCESS"
android:textSize="12sp"
android:textStyle="bold"
android:textColor="#4CAF50" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" • "
android:textSize="12sp"
android:textColor="#999999"
android:layout_marginHorizontal="4dp" />
<TextView
android:id="@+id/textPaymentMethod"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="QRIS"
android:textSize="12sp"
android:textColor="#333333" />
</LinearLayout>
</LinearLayout>
<!-- Kolom 2: Amount --> <!-- Kolom 2: Amount -->
<TextView <TextView