refactor code + generate qr (bug pending)
This commit is contained in:
parent
e0aec6e840
commit
8cef8fdb22
@ -59,8 +59,9 @@
|
|||||||
android:name=".QrisActivity"
|
android:name=".QrisActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<!-- FIXED: Updated to correct package path -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".QrisResultActivity"
|
android:name=".qris.view.QrisResultActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
@ -106,4 +107,5 @@
|
|||||||
<activity android:name="com.sunmi.emv.l2.view.AppSelectActivity"/>
|
<activity android:name="com.sunmi.emv.l2.view.AppSelectActivity"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
package com.example.bdkipoc;
|
package com.example.bdkipoc;
|
||||||
|
import com.example.bdkipoc.qris.view.QrisResultActivity;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
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"/>
|
android:letterSpacing="0.02"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user