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