diff --git a/app/build.gradle b/app/build.gradle
index a01b099..34c8584 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,6 +5,13 @@ plugins {
android {
namespace 'com.example.bdkipoc'
compileSdk 35
+
+ // Tambahkan lint options
+ lint {
+ abortOnError false
+ disable 'GoogleAppIndexingWarning'
+ disable 'NonConstantResourceId'
+ }
defaultConfig {
applicationId "com.example.bdkipoc"
@@ -22,20 +29,32 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+
+ // Keep Java 11 - lebih modern dari referensi
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
+
+ // Tambahkan sourceSets untuk native libs jika diperlukan
+ sourceSets {
+ main {
+ jniLibs.srcDirs = ['libs']
+ }
+ }
}
dependencies {
-
+ implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
implementation libs.cardview
implementation 'androidx.recyclerview:recyclerview:1.3.0'
+ implementation 'com.sunmi:printerlibrary:1.0.15'
+
+ // Test dependencies
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
diff --git a/app/libs/PayLib-release-2.0.17-sources.jar b/app/libs/PayLib-release-2.0.17-sources.jar
new file mode 100644
index 0000000..b27cfeb
Binary files /dev/null and b/app/libs/PayLib-release-2.0.17-sources.jar differ
diff --git a/app/libs/PayLib-release-2.0.17.aar b/app/libs/PayLib-release-2.0.17.aar
new file mode 100644
index 0000000..2f01592
Binary files /dev/null and b/app/libs/PayLib-release-2.0.17.aar differ
diff --git a/app/libs/armeabi-v7a/libAE_100.so b/app/libs/armeabi-v7a/libAE_100.so
new file mode 100644
index 0000000..4a3f975
Binary files /dev/null and b/app/libs/armeabi-v7a/libAE_100.so differ
diff --git a/app/libs/armeabi-v7a/libCPACE_100.so b/app/libs/armeabi-v7a/libCPACE_100.so
new file mode 100644
index 0000000..6ebb7da
Binary files /dev/null and b/app/libs/armeabi-v7a/libCPACE_100.so differ
diff --git a/app/libs/armeabi-v7a/libDPAS_100.so b/app/libs/armeabi-v7a/libDPAS_100.so
new file mode 100644
index 0000000..fe02e6c
Binary files /dev/null and b/app/libs/armeabi-v7a/libDPAS_100.so differ
diff --git a/app/libs/armeabi-v7a/libEFTPOS_001.so b/app/libs/armeabi-v7a/libEFTPOS_001.so
new file mode 100644
index 0000000..10510af
Binary files /dev/null and b/app/libs/armeabi-v7a/libEFTPOS_001.so differ
diff --git a/app/libs/armeabi-v7a/libEMVL2Base.so b/app/libs/armeabi-v7a/libEMVL2Base.so
new file mode 100644
index 0000000..9c896d6
Binary files /dev/null and b/app/libs/armeabi-v7a/libEMVL2Base.so differ
diff --git a/app/libs/armeabi-v7a/libEMVL2Dirct.so b/app/libs/armeabi-v7a/libEMVL2Dirct.so
new file mode 100644
index 0000000..f537ed5
Binary files /dev/null and b/app/libs/armeabi-v7a/libEMVL2Dirct.so differ
diff --git a/app/libs/armeabi-v7a/libEMV_100.so b/app/libs/armeabi-v7a/libEMV_100.so
new file mode 100644
index 0000000..874789b
Binary files /dev/null and b/app/libs/armeabi-v7a/libEMV_100.so differ
diff --git a/app/libs/armeabi-v7a/libEntry.so b/app/libs/armeabi-v7a/libEntry.so
new file mode 100644
index 0000000..11a39c0
Binary files /dev/null and b/app/libs/armeabi-v7a/libEntry.so differ
diff --git a/app/libs/armeabi-v7a/libFLASH_001.so b/app/libs/armeabi-v7a/libFLASH_001.so
new file mode 100644
index 0000000..ca7a642
Binary files /dev/null and b/app/libs/armeabi-v7a/libFLASH_001.so differ
diff --git a/app/libs/armeabi-v7a/libJCB_100.so b/app/libs/armeabi-v7a/libJCB_100.so
new file mode 100644
index 0000000..028213f
Binary files /dev/null and b/app/libs/armeabi-v7a/libJCB_100.so differ
diff --git a/app/libs/armeabi-v7a/libMIR_001.so b/app/libs/armeabi-v7a/libMIR_001.so
new file mode 100644
index 0000000..af4ea5f
Binary files /dev/null and b/app/libs/armeabi-v7a/libMIR_001.so differ
diff --git a/app/libs/armeabi-v7a/libPAGO_001.so b/app/libs/armeabi-v7a/libPAGO_001.so
new file mode 100644
index 0000000..93d17a5
Binary files /dev/null and b/app/libs/armeabi-v7a/libPAGO_001.so differ
diff --git a/app/libs/armeabi-v7a/libPURE_001.so b/app/libs/armeabi-v7a/libPURE_001.so
new file mode 100644
index 0000000..ad79644
Binary files /dev/null and b/app/libs/armeabi-v7a/libPURE_001.so differ
diff --git a/app/libs/armeabi-v7a/libPaypass_100.so b/app/libs/armeabi-v7a/libPaypass_100.so
new file mode 100644
index 0000000..0f67b8f
Binary files /dev/null and b/app/libs/armeabi-v7a/libPaypass_100.so differ
diff --git a/app/libs/armeabi-v7a/libPaywave_100.so b/app/libs/armeabi-v7a/libPaywave_100.so
new file mode 100644
index 0000000..19f77f4
Binary files /dev/null and b/app/libs/armeabi-v7a/libPaywave_100.so differ
diff --git a/app/libs/armeabi-v7a/libQPBOC_100.so b/app/libs/armeabi-v7a/libQPBOC_100.so
new file mode 100644
index 0000000..b1786f7
Binary files /dev/null and b/app/libs/armeabi-v7a/libQPBOC_100.so differ
diff --git a/app/libs/armeabi-v7a/libRupay_001.so b/app/libs/armeabi-v7a/libRupay_001.so
new file mode 100644
index 0000000..5f9a764
Binary files /dev/null and b/app/libs/armeabi-v7a/libRupay_001.so differ
diff --git a/app/libs/armeabi-v7a/libSamsungPay_001.so b/app/libs/armeabi-v7a/libSamsungPay_001.so
new file mode 100644
index 0000000..3038744
Binary files /dev/null and b/app/libs/armeabi-v7a/libSamsungPay_001.so differ
diff --git a/app/libs/armeabi-v7a/libsunmiemvl2.so b/app/libs/armeabi-v7a/libsunmiemvl2.so
new file mode 100644
index 0000000..2531f9c
Binary files /dev/null and b/app/libs/armeabi-v7a/libsunmiemvl2.so differ
diff --git a/app/libs/sunmiemvl2split-1.0.1.jar b/app/libs/sunmiemvl2split-1.0.1.jar
new file mode 100644
index 0000000..f03145c
Binary files /dev/null and b/app/libs/sunmiemvl2split-1.0.1.jar differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c24742c..8836e28 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,7 +8,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/CacheHelper.java b/app/src/main/java/com/example/bdkipoc/CacheHelper.java
new file mode 100644
index 0000000..07073ce
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/CacheHelper.java
@@ -0,0 +1,27 @@
+package com.example.bdkipoc;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public class CacheHelper {
+
+ private static final String PREFERENCE_FILE_NAME = "sm_pay_demo_obj";
+
+ private static final String KEY_LANGUAGE = "key_language";
+
+ public static void saveCurrentLanguage(int language) {
+ SharedPreferences sharedPreferences = MyApplication.app.getSharedPreferences(PREFERENCE_FILE_NAME, Context.MODE_PRIVATE);
+ int value = sharedPreferences.getInt(KEY_LANGUAGE, Constant.LANGUAGE_AUTO);
+ if (value == language) return;
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putInt(KEY_LANGUAGE, language);
+ editor.apply();
+ }
+
+ public static int getCurrentLanguage() {
+ SharedPreferences sharedPreferences = MyApplication.app.getSharedPreferences(PREFERENCE_FILE_NAME, Context.MODE_PRIVATE);
+ return sharedPreferences.getInt(KEY_LANGUAGE, Constant.LANGUAGE_AUTO);
+ }
+
+
+}
diff --git a/app/src/main/java/com/example/bdkipoc/Constant.java b/app/src/main/java/com/example/bdkipoc/Constant.java
new file mode 100644
index 0000000..9d11786
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/Constant.java
@@ -0,0 +1,17 @@
+package com.example.bdkipoc;
+
+public class Constant {
+
+ public static final String TAG = "SDKTestDemo";
+
+ public static final int LANGUAGE_AUTO = 0;
+ public static final int LANGUAGE_ZH_CN = 1;
+ public static final int LANGUAGE_EN_US = 2;
+ public static final int LANGUAGE_JA_JP = 3;
+
+ public static final int SCAN_MODEL_NONE = 100;
+ public static final int SCAN_MODEL_P2Lite = 101;
+
+ public static final String SCAN_MODEL_NONE_VALUE = "NONE";
+ public static final String SCAN_MODEL_P2Lite_VALUE = "P2Lite";
+}
diff --git a/app/src/main/java/com/example/bdkipoc/MainActivity.java b/app/src/main/java/com/example/bdkipoc/MainActivity.java
index 1a3db64..af9cda2 100644
--- a/app/src/main/java/com/example/bdkipoc/MainActivity.java
+++ b/app/src/main/java/com/example/bdkipoc/MainActivity.java
@@ -19,6 +19,13 @@ import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.button.MaterialButton;
+import com.example.bdkipoc.cetakulang.ReprintActivity;
+import com.example.bdkipoc.cetakulang.ReprintAdapterActivity;
+
+import com.example.bdkipoc.R;
+import com.example.bdkipoc.transaction.CreateTransactionActivity;
+import com.example.bdkipoc.transaction.ResultTransactionActivity;
+
public class MainActivity extends AppCompatActivity {
private boolean isExpanded = false; // False = showing only 9 main menus, True = showing all 15 menus
@@ -83,12 +90,9 @@ public class MainActivity extends AppCompatActivity {
// 6 dummy menus should be hidden initially
CardView[] dummyCards = {
- findViewById(R.id.card_dummy_menu_1),
- findViewById(R.id.card_dummy_menu_2),
- findViewById(R.id.card_dummy_menu_3),
- findViewById(R.id.card_dummy_menu_4),
- findViewById(R.id.card_dummy_menu_5),
- findViewById(R.id.card_dummy_menu_6)
+ findViewById(R.id.card_bantuan),
+ findViewById(R.id.card_info_toko),
+ findViewById(R.id.card_pengaturan),
};
for (CardView card : dummyCards) {
@@ -135,21 +139,17 @@ public class MainActivity extends AppCompatActivity {
R.id.card_kartu_debit,
R.id.card_qris,
// Row 2 (Always visible - 3 items)
+ R.id.card_transfer,
R.id.card_uang_elektronik,
R.id.card_cetak_ulang,
- R.id.card_settlement,
// Row 3 (Always visible - 3 items)
+ R.id.card_refund,
+ R.id.card_settlement,
R.id.card_histori,
- R.id.card_bantuan,
- R.id.card_info_toko,
// Row 4 (Hidden initially - 3 items)
- R.id.card_dummy_menu_1,
- R.id.card_dummy_menu_2,
- R.id.card_dummy_menu_3,
- // Row 5 (Hidden initially - 3 items)
- R.id.card_dummy_menu_4,
- R.id.card_dummy_menu_5,
- R.id.card_dummy_menu_6
+ R.id.card_bantuan,
+ R.id.card_info_toko,
+ R.id.card_pengaturan,
};
// Set up click listeners for all cards
@@ -157,39 +157,37 @@ public class MainActivity extends AppCompatActivity {
CardView cardView = findViewById(cardId);
if (cardView != null) {
cardView.setOnClickListener(v -> {
+ // ✅ ENHANCED: Navigate with payment type information
if (cardId == R.id.card_kartu_kredit) {
- startActivity(new Intent(MainActivity.this, PaymentActivity.class));
+ navigateToCreateTransaction("credit_card", cardId, "Kartu Kredit");
} else if (cardId == R.id.card_kartu_debit) {
- startActivity(new Intent(MainActivity.this, PaymentActivity.class));
+ navigateToCreateTransaction("debit_card", cardId, "Kartu Debit");
} else if (cardId == R.id.card_qris) {
startActivity(new Intent(MainActivity.this, QrisActivity.class));
+ // Col-2
+ } else if (cardId == R.id.card_transfer) {
+ navigateToCreateTransaction("transfer", cardId, "Transfer");
} else if (cardId == R.id.card_uang_elektronik) {
- startActivity(new Intent(MainActivity.this, PaymentActivity.class));
+ navigateToCreateTransaction("e_money", cardId, "Uang Elektronik");
} else if (cardId == R.id.card_cetak_ulang) {
- startActivity(new Intent(MainActivity.this, TransactionActivity.class));
+ startActivity(new Intent(MainActivity.this, ReprintActivity.class));
+ // Col-3
+ } else if (cardId == R.id.card_refund) {
+ navigateToCreateTransaction("refund", cardId, "Refund");
} else if (cardId == R.id.card_settlement) {
Toast.makeText(this, "Settlement - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_histori) {
- Toast.makeText(this, "Histori - Coming Soon", Toast.LENGTH_SHORT).show();
+ startActivity(new Intent(MainActivity.this, HistoryActivity.class));
+ // Col-4
} else if (cardId == R.id.card_bantuan) {
Toast.makeText(this, "Bantuan - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_info_toko) {
Toast.makeText(this, "Info Toko - Coming Soon", Toast.LENGTH_SHORT).show();
- } else if (cardId == R.id.card_dummy_menu_1) {
- Toast.makeText(this, "Dummy Menu 1 - Coming Soon", Toast.LENGTH_SHORT).show();
- } else if (cardId == R.id.card_dummy_menu_2) {
- Toast.makeText(this, "Dummy Menu 2 - Coming Soon", Toast.LENGTH_SHORT).show();
- } else if (cardId == R.id.card_dummy_menu_3) {
- Toast.makeText(this, "Dummy Menu 3 - Coming Soon", Toast.LENGTH_SHORT).show();
- } else if (cardId == R.id.card_dummy_menu_4) {
- Toast.makeText(this, "Dummy Menu 4 - Coming Soon", Toast.LENGTH_SHORT).show();
- } else if (cardId == R.id.card_dummy_menu_5) {
- Toast.makeText(this, "Dummy Menu 5 - Coming Soon", Toast.LENGTH_SHORT).show();
- } else if (cardId == R.id.card_dummy_menu_6) {
- Toast.makeText(this, "Dummy Menu 6 - Coming Soon", Toast.LENGTH_SHORT).show();
+ } else if (cardId == R.id.card_pengaturan) {
+ Toast.makeText(this, "Pengaturan - Coming Soon", Toast.LENGTH_SHORT).show();
} else {
// Fallback for any other cards
- Toast.makeText(this, "Menu Diklik: " + getResources().getResourceEntryName(cardId), Toast.LENGTH_SHORT).show();
+ navigateToCreateTransaction("credit_card", cardId, "Unknown");
}
});
}
@@ -197,12 +195,9 @@ public class MainActivity extends AppCompatActivity {
// Get references to ONLY the dummy cards that need to be toggled
CardView[] toggleableCards = {
- findViewById(R.id.card_dummy_menu_1),
- findViewById(R.id.card_dummy_menu_2),
- findViewById(R.id.card_dummy_menu_3),
- findViewById(R.id.card_dummy_menu_4),
- findViewById(R.id.card_dummy_menu_5),
- findViewById(R.id.card_dummy_menu_6)
+ findViewById(R.id.card_bantuan),
+ findViewById(R.id.card_info_toko),
+ findViewById(R.id.card_pengaturan),
};
// Set up "Lainnya" button click listener
@@ -249,6 +244,125 @@ public class MainActivity extends AppCompatActivity {
}
}
+ // ✅ NEW: Enhanced navigation method with payment type information
+ private void navigateToCreateTransaction(String paymentType, int cardMenuId, String cardName) {
+ try {
+ Intent intent = new Intent(MainActivity.this, CreateTransactionActivity.class);
+
+ // ✅ ENHANCED: Pass comprehensive payment information
+ intent.putExtra("PAYMENT_TYPE", paymentType);
+ intent.putExtra("CARD_MENU_ID", cardMenuId);
+ intent.putExtra("CARD_NAME", cardName);
+ intent.putExtra("CALLING_ACTIVITY", "MainActivity");
+
+ // ✅ DEBUG: Log navigation details
+ android.util.Log.d("MainActivity", "=== NAVIGATING TO CREATE TRANSACTION ===");
+ android.util.Log.d("MainActivity", "Payment Type: " + paymentType);
+ android.util.Log.d("MainActivity", "Card Menu ID: " + cardMenuId);
+ android.util.Log.d("MainActivity", "Card Name: " + cardName);
+ android.util.Log.d("MainActivity", "========================================");
+
+ startActivity(intent);
+
+ } catch (Exception e) {
+ android.util.Log.e("MainActivity", "Error navigating to CreateTransaction: " + e.getMessage(), e);
+ Toast.makeText(this, "Error opening transaction: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ // ✅ NEW: Helper method to get payment type from card ID (for backward compatibility)
+ private String getPaymentTypeFromCardId(int cardId) {
+ if (cardId == R.id.card_kartu_kredit) {
+ return "credit_card";
+ } else if (cardId == R.id.card_kartu_debit) {
+ return "debit_card";
+ } else if (cardId == R.id.card_qris) {
+ return "qris";
+ } else if (cardId == R.id.card_transfer) {
+ return "transfer";
+ } else if (cardId == R.id.card_uang_elektronik) {
+ return "e_money";
+ } else if (cardId == R.id.card_refund) {
+ return "refund";
+ } else {
+ android.util.Log.w("MainActivity", "Unknown card ID: " + cardId + ", defaulting to credit_card");
+ return "credit_card";
+ }
+ }
+
+ // ✅ NEW: Helper method to get card name from card ID
+ private String getCardNameFromCardId(int cardId) {
+ if (cardId == R.id.card_kartu_kredit) {
+ return "Kartu Kredit";
+ } else if (cardId == R.id.card_kartu_debit) {
+ return "Kartu Debit";
+ } else if (cardId == R.id.card_qris) {
+ return "QRIS";
+ } else if (cardId == R.id.card_transfer) {
+ return "Transfer";
+ } else if (cardId == R.id.card_uang_elektronik) {
+ return "Uang Elektronik";
+ } else if (cardId == R.id.card_refund) {
+ return "Refund";
+ } else if (cardId == R.id.card_settlement) {
+ return "Settlement";
+ } else if (cardId == R.id.card_histori) {
+ return "Histori";
+ } else if (cardId == R.id.card_cetak_ulang) {
+ return "Cetak Ulang";
+ } else if (cardId == R.id.card_bantuan) {
+ return "Bantuan";
+ } else if (cardId == R.id.card_info_toko) {
+ return "Info Toko";
+ } else if (cardId == R.id.card_pengaturan) {
+ return "Pengaturan";
+ } else {
+ return "Unknown";
+ }
+ }
+
+ // ✅ NEW: Method to validate payment type compatibility
+ private boolean isPaymentTypeSupported(String paymentType) {
+ String[] supportedTypes = {
+ "credit_card", "debit_card", "e_money", "qris",
+ "transfer", "refund"
+ };
+
+ for (String supportedType : supportedTypes) {
+ if (supportedType.equals(paymentType)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // ✅ NEW: Method to show payment type selection dialog (for future use)
+ private void showPaymentTypeDialog() {
+ androidx.appcompat.app.AlertDialog.Builder builder = new androidx.appcompat.app.AlertDialog.Builder(this);
+ builder.setTitle("Pilih Jenis Pembayaran");
+
+ String[] paymentTypes = {
+ "Kartu Kredit", "Kartu Debit", "Uang Elektronik",
+ "QRIS", "Transfer", "Refund"
+ };
+ String[] paymentTypeCodes = {
+ "credit_card", "debit_card", "e_money",
+ "qris", "transfer", "refund"
+ };
+
+ builder.setItems(paymentTypes, (dialog, which) -> {
+ String selectedType = paymentTypeCodes[which];
+ String selectedName = paymentTypes[which];
+
+ // Use a generic card ID for dialog selection
+ navigateToCreateTransaction(selectedType, -1, selectedName);
+ });
+
+ builder.setNegativeButton("Batal", null);
+ builder.show();
+ }
+
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
@@ -263,5 +377,74 @@ public class MainActivity extends AppCompatActivity {
// Clear any transaction completion flags to avoid repeated messages
getIntent().removeExtra("transaction_completed");
getIntent().removeExtra("transaction_amount");
+
+ // ✅ NEW: Log resume for debugging
+ android.util.Log.d("MainActivity", "MainActivity resumed");
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ android.util.Log.d("MainActivity", "MainActivity paused");
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ android.util.Log.d("MainActivity", "MainActivity destroyed");
+ }
+
+ // ✅ NEW: Method to handle direct payment type launch (for external calls)
+ public static Intent createTransactionIntent(android.content.Context context, String paymentType, String cardName) {
+ Intent intent = new Intent(context, CreateTransactionActivity.class);
+ intent.putExtra("PAYMENT_TYPE", paymentType);
+ intent.putExtra("CARD_NAME", cardName);
+ intent.putExtra("CALLING_ACTIVITY", "External");
+ return intent;
+ }
+
+ // ✅ NEW: Public method to simulate card click (for testing)
+ public void simulateCardClick(int cardId) {
+ CardView cardView = findViewById(cardId);
+ if (cardView != null) {
+ cardView.performClick();
+ } else {
+ android.util.Log.w("MainActivity", "Card not found for ID: " + cardId);
+ }
+ }
+
+ // ✅ NEW: Method to get all available payment types
+ public String[] getAvailablePaymentTypes() {
+ return new String[]{
+ "credit_card", "debit_card", "e_money",
+ "qris", "transfer", "refund"
+ };
+ }
+
+ // ✅ NEW: Method to get payment type display names
+ public String[] getPaymentTypeDisplayNames() {
+ return new String[]{
+ "Kartu Kredit", "Kartu Debit", "Uang Elektronik",
+ "QRIS", "Transfer", "Refund"
+ };
+ }
+
+ // ✅ NEW: Debug method to log all card IDs and their payment types
+ private void debugCardMappings() {
+ android.util.Log.d("MainActivity", "=== CARD PAYMENT TYPE MAPPINGS ===");
+
+ int[] cardIds = {
+ R.id.card_kartu_kredit, R.id.card_kartu_debit, R.id.card_qris,
+ R.id.card_transfer, R.id.card_uang_elektronik, R.id.card_refund
+ };
+
+ for (int cardId : cardIds) {
+ String paymentType = getPaymentTypeFromCardId(cardId);
+ String cardName = getCardNameFromCardId(cardId);
+ android.util.Log.d("MainActivity",
+ "Card ID: " + cardId + " -> Payment Type: " + paymentType + " -> Name: " + cardName);
+ }
+
+ android.util.Log.d("MainActivity", "==================================");
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/MyApplication.java b/app/src/main/java/com/example/bdkipoc/MyApplication.java
new file mode 100644
index 0000000..6104fb8
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/MyApplication.java
@@ -0,0 +1,197 @@
+package com.example.bdkipoc;
+
+import android.app.Application;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.IBinder;
+import android.util.DisplayMetrics;
+
+import com.example.bdkipoc.emv.EmvTTS;
+import com.example.bdkipoc.utils.LogUtil;
+import com.example.bdkipoc.utils.Utility;
+import com.sunmi.pay.hardware.aidlv2.emv.EMVOptV2;
+import com.sunmi.pay.hardware.aidlv2.etc.ETCOptV2;
+import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2;
+import com.sunmi.pay.hardware.aidlv2.print.PrinterOptV2;
+import com.sunmi.pay.hardware.aidlv2.readcard.ReadCardOptV2;
+import com.sunmi.pay.hardware.aidlv2.rfid.RFIDOptV2;
+import com.sunmi.pay.hardware.aidlv2.security.BiometricManagerV2;
+import com.sunmi.pay.hardware.aidlv2.security.DevCertManagerV2;
+import com.sunmi.pay.hardware.aidlv2.security.NoLostKeyManagerV2;
+import com.sunmi.pay.hardware.aidlv2.security.SecurityOptV2;
+import com.sunmi.pay.hardware.aidlv2.system.BasicOptV2;
+import com.sunmi.pay.hardware.aidlv2.tax.TaxOptV2;
+import com.sunmi.pay.hardware.aidlv2.test.TestOptV2;
+import com.sunmi.pay.hardware.wrapper.HCEManagerV2Wrapper;
+import com.sunmi.peripheral.printer.InnerPrinterCallback;
+import com.sunmi.peripheral.printer.InnerPrinterException;
+import com.sunmi.peripheral.printer.InnerPrinterManager;
+import com.sunmi.peripheral.printer.SunmiPrinterService;
+
+import java.util.Locale;
+
+import sunmi.paylib.SunmiPayKernel;
+
+public class MyApplication extends Application {
+ public static MyApplication app;
+
+ public BasicOptV2 basicOptV2; // 获取基础操作模块
+ public ReadCardOptV2 readCardOptV2; // 获取读卡模块
+ public PinPadOptV2 pinPadOptV2; // 获取PinPad操作模块
+ public SecurityOptV2 securityOptV2; // 获取安全操作模块
+ public EMVOptV2 emvOptV2; // 获取EMV操作模块
+ public TaxOptV2 taxOptV2; // 获取税控操作模块
+ public ETCOptV2 etcOptV2; // 获取ETC操作模块
+ public PrinterOptV2 printerOptV2; // 获取打印操作模块
+ public TestOptV2 testOptV2; // 获取测试操作模块
+ public DevCertManagerV2 devCertManagerV2; // 设备证书操作模块
+ public NoLostKeyManagerV2 noLostKeyManagerV2; // NoLostKey操作模块
+ public HCEManagerV2Wrapper hceV2Wrapper; // HCE操作模块
+ public RFIDOptV2 rfidOptV2; // RFID操作模块
+ public SunmiPrinterService sunmiPrinterService; // 打印模块
+ //public IScanInterface scanInterface; // 扫码模块 (commented out)
+ public BiometricManagerV2 mBiometricManagerV2; // 生物特征模块
+
+ private boolean connectPaySDK;//是否已连接PaySDK
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ app = this;
+ initLocaleLanguage();
+ initEmvTTS();
+ bindPrintService();
+ bindPaySDKService();
+ //bindScannerService(); // Commented out scanner service binding
+ }
+
+ public static void initLocaleLanguage() {
+ Resources resources = app.getResources();
+ DisplayMetrics dm = resources.getDisplayMetrics();
+ Configuration config = resources.getConfiguration();
+ int showLanguage = CacheHelper.getCurrentLanguage();
+ if (showLanguage == Constant.LANGUAGE_AUTO) {
+ LogUtil.e(Constant.TAG, config.locale.getCountry() + "---这是系统语言");
+ config.locale = Resources.getSystem().getConfiguration().locale;
+ } else if (showLanguage == Constant.LANGUAGE_ZH_CN) {
+ LogUtil.e(Constant.TAG, "这是中文");
+ config.locale = Locale.SIMPLIFIED_CHINESE;
+ } else if (showLanguage == Constant.LANGUAGE_EN_US) {
+ LogUtil.e(Constant.TAG, "这是英文");
+ config.locale = Locale.ENGLISH;
+ } else if (showLanguage == Constant.LANGUAGE_JA_JP) {
+ LogUtil.e(Constant.TAG, "这是日文");
+ config.locale = Locale.JAPAN;
+ }
+ resources.updateConfiguration(config, dm);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ LogUtil.e(Constant.TAG, "onConfigurationChanged");
+ }
+
+ public boolean isConnectPaySDK() {
+ return connectPaySDK;
+ }
+
+ /**
+ * bind PaySDK service
+ */
+ public void bindPaySDKService() {
+ final SunmiPayKernel payKernel = SunmiPayKernel.getInstance();
+ payKernel.setEmvL2Split(true);
+ payKernel.initPaySDK(this, new SunmiPayKernel.ConnectCallback() {
+ @Override
+ public void onConnectPaySDK() {
+ LogUtil.e(Constant.TAG, "onConnectPaySDK...");
+ emvOptV2 = payKernel.mEMVOptV2;
+ basicOptV2 = payKernel.mBasicOptV2;
+ pinPadOptV2 = payKernel.mPinPadOptV2;
+ readCardOptV2 = payKernel.mReadCardOptV2;
+ securityOptV2 = payKernel.mSecurityOptV2;
+ taxOptV2 = payKernel.mTaxOptV2;
+ etcOptV2 = payKernel.mETCOptV2;
+ printerOptV2 = payKernel.mPrinterOptV2;
+ testOptV2 = payKernel.mTestOptV2;
+ devCertManagerV2 = payKernel.mDevCertManagerV2;
+ noLostKeyManagerV2 = payKernel.mNoLostKeyManagerV2;
+ mBiometricManagerV2 = payKernel.mBiometricManagerV2;
+ hceV2Wrapper = payKernel.mHCEManagerV2Wrapper;
+ rfidOptV2 = payKernel.mRFIDOptV2;
+ connectPaySDK = true;
+ }
+
+ @Override
+ public void onDisconnectPaySDK() {
+ LogUtil.e(Constant.TAG, "onDisconnectPaySDK...");
+ connectPaySDK = false;
+ emvOptV2 = null;
+ basicOptV2 = null;
+ pinPadOptV2 = null;
+ readCardOptV2 = null;
+ securityOptV2 = null;
+ taxOptV2 = null;
+ etcOptV2 = null;
+ printerOptV2 = null;
+ devCertManagerV2 = null;
+ noLostKeyManagerV2 = null;
+ mBiometricManagerV2 = null;
+ rfidOptV2 = null;
+ Utility.showToast(R.string.connect_fail);
+ }
+ });
+ }
+
+ /**
+ * bind printer service
+ */
+ private void bindPrintService() {
+ try {
+ InnerPrinterManager.getInstance().bindService(this, new InnerPrinterCallback() {
+ @Override
+ protected void onConnected(SunmiPrinterService service) {
+ sunmiPrinterService = service;
+ }
+
+ @Override
+ protected void onDisconnected() {
+ sunmiPrinterService = null;
+ }
+ });
+ } catch (InnerPrinterException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * bind scanner service (commented out)
+ */
+ /*
+ public void bindScannerService() {
+ Intent intent = new Intent();
+ intent.setPackage("com.sunmi.scanner");
+ intent.setAction("com.sunmi.scanner.IScanInterface");
+ bindService(intent, new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ scanInterface = IScanInterface.Stub.asInterface(service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ scanInterface = null;
+ }
+ }, Service.BIND_AUTO_CREATE);
+ }
+ */
+
+ private void initEmvTTS() {
+ EmvTTS.getInstance().init();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/PaymentActivity.java b/app/src/main/java/com/example/bdkipoc/PaymentActivity.java
deleted file mode 100644
index 0e88fe3..0000000
--- a/app/src/main/java/com/example/bdkipoc/PaymentActivity.java
+++ /dev/null
@@ -1,530 +0,0 @@
-package com.example.bdkipoc;
-
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.app.Dialog;
-import android.content.Intent;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.text.TextUtils;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.animation.AccelerateDecelerateInterpolator;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.RadioGroup;
-import android.widget.TextView;
-import android.widget.Toast;
-import androidx.appcompat.app.AppCompatActivity;
-
-public class PaymentActivity extends AppCompatActivity {
-
- // Views
- private EditText editTextAmount;
- private Button confirmButton;
- private LinearLayout backNavigation;
- private ImageView backArrow;
- private TextView toolbarTitle;
-
- // Numpad buttons
- private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000;
- private ImageView btnDelete;
-
- // Modal components
- private Dialog paymentModal;
-
- // Data
- private StringBuilder currentAmount = new StringBuilder();
- private static final int MAX_AMOUNT_LENGTH = 12;
-
- // Animation
- private Handler animationHandler = new Handler(Looper.getMainLooper());
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // Set status bar color programmatically
- setStatusBarColor();
-
- setContentView(R.layout.activity_payment);
-
- initializeViews();
- setupClickListeners();
- setupInitialStates();
- setupModal();
- }
-
- private void setStatusBarColor() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- Window window = getWindow();
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- window.setStatusBarColor(Color.parseColor("#E31937")); // Red color
-
- // Make status bar icons white (for dark red background)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- View decorView = window.getDecorView();
- decorView.setSystemUiVisibility(0); // Clear light status bar flag
- }
- }
- }
-
- private void initializeViews() {
- // Main views
- editTextAmount = findViewById(R.id.editTextAmount);
- confirmButton = findViewById(R.id.confirmButton);
- backNavigation = findViewById(R.id.back_navigation);
- backArrow = findViewById(R.id.backArrow);
- toolbarTitle = findViewById(R.id.toolbarTitle);
-
- // Numpad buttons
- btn1 = findViewById(R.id.btn1);
- btn2 = findViewById(R.id.btn2);
- btn3 = findViewById(R.id.btn3);
- btn4 = findViewById(R.id.btn4);
- btn5 = findViewById(R.id.btn5);
- btn6 = findViewById(R.id.btn6);
- btn7 = findViewById(R.id.btn7);
- btn8 = findViewById(R.id.btn8);
- btn9 = findViewById(R.id.btn9);
- btn0 = findViewById(R.id.btn0);
- btn000 = findViewById(R.id.btn000);
- btnDelete = findViewById(R.id.btnDelete);
- }
-
- private void setupModal() {
- // Create modal dialog
- paymentModal = new Dialog(this);
- paymentModal.setContentView(R.layout.modal_layout);
-
- // Remove background dimming - make it fully transparent
- if (paymentModal.getWindow() != null) {
- paymentModal.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
- }
-
- // Setup modal listeners
- setupModalListeners();
- }
-
- private void setupModalListeners() {
- // Make modal non-cancelable by touching outside
- paymentModal.setCanceledOnTouchOutside(false);
-
- // Auto dismiss after 3 seconds (simulate card processing)
- Handler modalHandler = new Handler(Looper.getMainLooper());
- paymentModal.setOnShowListener(dialog -> {
- modalHandler.postDelayed(() -> {
- if (paymentModal != null && paymentModal.isShowing()) {
- // First dismiss modal, then navigate
- paymentModal.dismiss();
-
- // Add small delay to ensure modal is fully dismissed
- animationHandler.postDelayed(() -> {
- navigateToPinActivity();
- }, 100);
- }
- }, 3000);
- });
- }
-
- private void setupClickListeners() {
- // Back navigation - entire LinearLayout is clickable
- backNavigation.setOnClickListener(v -> {
- addClickAnimation(v);
- navigateBack();
- });
-
- // Individual back arrow (for additional touch area)
- backArrow.setOnClickListener(v -> {
- addClickAnimation(v);
- navigateBack();
- });
-
- // Toolbar title (also clickable for back navigation)
- toolbarTitle.setOnClickListener(v -> {
- addClickAnimation(v);
- navigateBack();
- });
-
- // Numpad listeners
- btn1.setOnClickListener(v -> handleNumpadClick(v, "1"));
- btn2.setOnClickListener(v -> handleNumpadClick(v, "2"));
- btn3.setOnClickListener(v -> handleNumpadClick(v, "3"));
- btn4.setOnClickListener(v -> handleNumpadClick(v, "4"));
- btn5.setOnClickListener(v -> handleNumpadClick(v, "5"));
- btn6.setOnClickListener(v -> handleNumpadClick(v, "6"));
- btn7.setOnClickListener(v -> handleNumpadClick(v, "7"));
- btn8.setOnClickListener(v -> handleNumpadClick(v, "8"));
- btn9.setOnClickListener(v -> handleNumpadClick(v, "9"));
- btn0.setOnClickListener(v -> handleNumpadClick(v, "0"));
- btn000.setOnClickListener(v -> handleNumpadClick(v, "000"));
-
- // Delete button
- btnDelete.setOnClickListener(v -> {
- addClickAnimation(v);
- deleteLastDigit();
- });
-
- // Confirm button - NOW SHOWS MODAL INSTEAD OF DIRECT PAYMENT
- confirmButton.setOnClickListener(v -> {
- if (confirmButton.isEnabled()) {
- addButtonClickAnimation(v);
- showPaymentModal();
- }
- });
- }
-
- private void navigateBack() {
- // Simple back navigation without card animation
- finish();
- }
-
- private void handleNumpadClick(View view, String digit) {
- addClickAnimation(view);
- addDigit(digit);
- }
-
- private void setupInitialStates() {
- // Set initial amount display
- editTextAmount.setText("");
-
- // Set initial button state
- updateButtonState();
-
- // Disable EditText input (only numpad input allowed)
- editTextAmount.setFocusable(false);
- editTextAmount.setClickable(false);
- editTextAmount.setCursorVisible(false);
- }
-
- private void addDigit(String digit) {
- // Validate input length
- if (currentAmount.length() >= MAX_AMOUNT_LENGTH) {
- showToast("Maksimal " + MAX_AMOUNT_LENGTH + " digit");
- return;
- }
-
- // Handle leading zeros
- if (currentAmount.length() == 0) {
- if (digit.equals("000")) {
- // Don't allow 000 as first input
- return;
- }
- currentAmount.append(digit);
- } else if (currentAmount.length() == 1 && currentAmount.toString().equals("0")) {
- if (!digit.equals("000")) {
- // Replace single 0 with new digit
- currentAmount = new StringBuilder(digit);
- } else {
- return;
- }
- } else {
- currentAmount.append(digit);
- }
-
- updateAmountDisplay();
- updateButtonState();
- addInputFeedback();
- }
-
- private void deleteLastDigit() {
- if (currentAmount.length() > 0) {
- String current = currentAmount.toString();
-
- // If current ends with 000, remove all three digits
- if (current.endsWith("000") && current.length() >= 3) {
- currentAmount.delete(currentAmount.length() - 3, currentAmount.length());
- } else {
- currentAmount.deleteCharAt(currentAmount.length() - 1);
- }
-
- updateAmountDisplay();
- updateButtonState();
- addDeleteFeedback();
- }
- }
-
- private void updateAmountDisplay() {
- String amount = currentAmount.toString();
-
- if (amount.isEmpty() || amount.equals("0")) {
- editTextAmount.setText("");
- } else {
- String formattedAmount = formatCurrency(amount);
- editTextAmount.setText(formattedAmount);
- }
- }
-
- private String formatCurrency(String amount) {
- if (TextUtils.isEmpty(amount) || amount.equals("0")) {
- return "";
- }
-
- try {
- long number = Long.parseLong(amount);
- return String.format("%,d", number).replace(',', '.');
- } catch (NumberFormatException e) {
- return amount;
- }
- }
-
- private void updateButtonState() {
- boolean hasValidAmount = currentAmount.length() > 0 &&
- !currentAmount.toString().equals("0") &&
- !currentAmount.toString().isEmpty();
-
- confirmButton.setEnabled(hasValidAmount);
-
- if (hasValidAmount) {
- // Active state
- confirmButton.setBackgroundResource(R.drawable.button_active_background);
- confirmButton.setTextColor(Color.WHITE);
- confirmButton.setAlpha(1.0f);
- } else {
- // Inactive state
- confirmButton.setBackgroundResource(R.drawable.button_inactive_background);
- confirmButton.setTextColor(Color.parseColor("#999999"));
- confirmButton.setAlpha(0.6f);
- }
- }
-
- // NEW METHOD: Show payment modal instead of direct payment processing
- private void showPaymentModal() {
- String amount = currentAmount.toString();
-
- if (TextUtils.isEmpty(amount) || amount.equals("0")) {
- showToast("Masukkan jumlah pembayaran");
- return;
- }
-
- try {
- long amountValue = Long.parseLong(amount);
-
- // Validate minimum amount
- if (amountValue < 1000) {
- showToast("Minimal pembayaran Rp 1.000");
- return;
- }
-
- // Validate maximum amount
- if (amountValue > 999999999L) {
- showToast("Maksimal pembayaran Rp 999.999.999");
- return;
- }
-
- // Show modal with animation
- showModalWithAnimation();
-
- } catch (NumberFormatException e) {
- showToast("Format jumlah tidak valid");
- }
- }
-
- private void showModalWithAnimation() {
- // Add debug log
- showToast("Showing card modal...");
-
- paymentModal.show();
-
- // Add slide-up animation
- View modalView = paymentModal.findViewById(android.R.id.content);
- if (modalView != null) {
- ObjectAnimator slideUp = ObjectAnimator.ofFloat(modalView, "translationY", 300f, 0f);
- ObjectAnimator fadeIn = ObjectAnimator.ofFloat(modalView, "alpha", 0f, 1f);
-
- AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(slideUp, fadeIn);
- animatorSet.setDuration(300);
- animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
- animatorSet.start();
- }
- }
-
- private void dismissModal() {
- if (paymentModal != null && paymentModal.isShowing()) {
- // Add slide-down animation before dismissing
- View modalView = paymentModal.findViewById(android.R.id.content);
- if (modalView != null) {
- ObjectAnimator slideDown = ObjectAnimator.ofFloat(modalView, "translationY", 0f, 300f);
- ObjectAnimator fadeOut = ObjectAnimator.ofFloat(modalView, "alpha", 1f, 0f);
-
- AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(slideDown, fadeOut);
- animatorSet.setDuration(200);
- animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
-
- animatorSet.addListener(new android.animation.AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(android.animation.Animator animation) {
- paymentModal.dismiss();
- }
- });
-
- animatorSet.start();
- } else {
- paymentModal.dismiss();
- }
- }
- }
-
- private void processModalConfirmation() {
- // This method is no longer needed since modal auto-dismisses
- }
-
- private void navigateToPinActivity() {
- String amount = currentAmount.toString();
-
- // Add debug log
- showToast("Navigating to PIN Activity...");
-
- try {
- long amountValue = Long.parseLong(amount);
-
- // Launch PIN Activity with amount data
- Intent intent = new Intent(this, PinActivity.class);
- intent.putExtra(PinActivity.EXTRA_SOURCE_ACTIVITY, "PaymentActivity");
- intent.putExtra(PinActivity.EXTRA_AMOUNT, String.valueOf(amountValue));
- startActivityForResult(intent, 100);
-
- } catch (NumberFormatException e) {
- showToast("Format jumlah tidak valid: " + e.getMessage());
- } catch (Exception e) {
- showToast("Error navigating to PIN: " + e.getMessage());
- }
- }
-
- private void processCardPayment() {
- // This method is called after PIN verification is successful
- // Now process the actual payment
- String amount = currentAmount.toString();
-
- try {
- long amountValue = Long.parseLong(amount);
-
- // Show processing message
- showToast("PIN berhasil diverifikasi! Memproses pembayaran...");
-
- // Process the final payment
- processPayment(amountValue);
-
- } catch (NumberFormatException e) {
- showToast("Format jumlah tidak valid");
- }
- }
-
- private void processPayment(long amount) {
- // Show loading state
- confirmButton.setText("Memproses...");
- confirmButton.setEnabled(false);
-
- // Simulate payment processing
- animationHandler.postDelayed(() -> {
- // Show success message
- showToast("Pembayaran berhasil! Jumlah: Rp " + formatCurrency(String.valueOf(amount)));
-
- // Reset state and go back (this is final step after PIN verification)
- resetPaymentState();
- navigateBack();
- }, 2000);
- }
-
- private void resetPaymentState() {
- currentAmount = new StringBuilder();
- updateAmountDisplay();
- updateButtonState();
- confirmButton.setText("Konfirmasi");
- }
-
- // Animation methods (only for numpad interactions)
- private void addClickAnimation(View view) {
- ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f, 1f);
- ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f, 1f);
-
- AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(scaleX, scaleY);
- animatorSet.setDuration(150);
- animatorSet.start();
- }
-
- private void addButtonClickAnimation(View view) {
- ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.98f, 1f);
- ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.98f, 1f);
-
- AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(scaleX, scaleY);
- animatorSet.setDuration(200);
- animatorSet.start();
- }
-
- private void addInputFeedback() {
- ObjectAnimator fadeIn = ObjectAnimator.ofFloat(editTextAmount, "alpha", 0.7f, 1f);
- fadeIn.setDuration(200);
- fadeIn.start();
- }
-
- private void addDeleteFeedback() {
- ObjectAnimator shake = ObjectAnimator.ofFloat(editTextAmount, "translationX", 0f, -10f, 10f, 0f);
- shake.setDuration(300);
- shake.start();
- }
-
- // Utility methods
- private void showToast(String message) {
- Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
- }
-
- @Override
- public void onBackPressed() {
- // Check if modal is showing, dismiss it first
- if (paymentModal != null && paymentModal.isShowing()) {
- dismissModal();
- } else {
- navigateBack();
- }
-
- super.onBackPressed();
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- // Handle result from PIN Activity
- if (resultCode == RESULT_OK && data != null) {
- boolean pinVerified = data.getBooleanExtra("pin_verified", false);
- if (pinVerified) {
- // PIN verification successful, process payment
- processCardPayment();
- }
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (animationHandler != null) {
- animationHandler.removeCallbacksAndMessages(null);
- }
-
- // Clean up modal
- if (paymentModal != null && paymentModal.isShowing()) {
- paymentModal.dismiss();
- }
- }
-
- // Public methods for testing
- public String getCurrentAmount() {
- return currentAmount.toString();
- }
-
- public boolean isConfirmButtonEnabled() {
- return confirmButton.isEnabled();
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/PinActivity.java b/app/src/main/java/com/example/bdkipoc/PinActivity.java
deleted file mode 100644
index 2bdb9d1..0000000
--- a/app/src/main/java/com/example/bdkipoc/PinActivity.java
+++ /dev/null
@@ -1,558 +0,0 @@
-package com.example.bdkipoc;
-
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Intent;
-import android.graphics.Color;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.text.TextUtils;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import android.widget.Toast;
-import androidx.appcompat.app.AppCompatActivity;
-
-public class PinActivity extends AppCompatActivity {
-
- // Intent Extra Keys
- public static final String EXTRA_TITLE = "extra_title";
- public static final String EXTRA_SUBTITLE = "extra_subtitle";
- public static final String EXTRA_AMOUNT = "extra_amount";
- public static final String EXTRA_SOURCE_ACTIVITY = "extra_source_activity";
-
- // Views
- private EditText editTextPin;
- private Button confirmButton;
- private LinearLayout backNavigation;
- private ImageView backArrow;
- private TextView toolbarTitle;
-
- // Success screen views
- private View successScreen;
- private ImageView successIcon;
- private TextView successMessage;
-
- // Numpad buttons
- private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000;
- private ImageView btnDelete;
-
- // Data
- private StringBuilder currentPin = new StringBuilder();
- private static final int MAX_PIN_LENGTH = 6;
- private static final int MIN_PIN_LENGTH = 4;
-
- // Extra data from intent
- private String sourceActivity;
- private String amount;
-
- // Animation
- private Handler animationHandler = new Handler(Looper.getMainLooper());
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // Set status bar color programmatically
- setStatusBarColor();
-
- setContentView(R.layout.activity_pin);
-
- // Get intent extras
- getIntentExtras();
-
- initializeViews();
- setupClickListeners();
- setupInitialStates();
- setupSuccessScreen();
- }
-
- private void setStatusBarColor() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- Window window = getWindow();
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- window.setStatusBarColor(Color.parseColor("#E31937")); // Red color
-
- // Make status bar icons white (for dark red background)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- View decorView = window.getDecorView();
- decorView.setSystemUiVisibility(0); // Clear light status bar flag
- }
- }
- }
-
- private void getIntentExtras() {
- Intent intent = getIntent();
- if (intent != null) {
- sourceActivity = intent.getStringExtra(EXTRA_SOURCE_ACTIVITY);
- amount = intent.getStringExtra(EXTRA_AMOUNT);
- }
- }
-
- private void initializeViews() {
- // Main views
- editTextPin = findViewById(R.id.editTextPin);
- confirmButton = findViewById(R.id.confirmButton);
- backNavigation = findViewById(R.id.back_navigation);
- backArrow = findViewById(R.id.backArrow);
- toolbarTitle = findViewById(R.id.toolbarTitle);
-
- // Success screen views
- successScreen = findViewById(R.id.success_screen);
- successIcon = findViewById(R.id.success_icon);
- successMessage = findViewById(R.id.success_message);
-
- // Numpad buttons
- btn1 = findViewById(R.id.btn1);
- btn2 = findViewById(R.id.btn2);
- btn3 = findViewById(R.id.btn3);
- btn4 = findViewById(R.id.btn4);
- btn5 = findViewById(R.id.btn5);
- btn6 = findViewById(R.id.btn6);
- btn7 = findViewById(R.id.btn7);
- btn8 = findViewById(R.id.btn8);
- btn9 = findViewById(R.id.btn9);
- btn0 = findViewById(R.id.btn0);
- btn000 = findViewById(R.id.btn000);
- btnDelete = findViewById(R.id.btnDelete);
- }
-
- private void setupSuccessScreen() {
- // Initially hide success screen
- if (successScreen != null) {
- successScreen.setVisibility(View.GONE);
- }
- }
-
- private void setupClickListeners() {
- // Back navigation - entire LinearLayout is clickable
- backNavigation.setOnClickListener(v -> {
- addClickAnimation(v);
- navigateBack();
- });
-
- // Individual back arrow (for additional touch area)
- backArrow.setOnClickListener(v -> {
- addClickAnimation(v);
- navigateBack();
- });
-
- // Toolbar title (also clickable for back navigation)
- toolbarTitle.setOnClickListener(v -> {
- addClickAnimation(v);
- navigateBack();
- });
-
- // Numpad listeners
- btn1.setOnClickListener(v -> handleNumpadClick(v, "1"));
- btn2.setOnClickListener(v -> handleNumpadClick(v, "2"));
- btn3.setOnClickListener(v -> handleNumpadClick(v, "3"));
- btn4.setOnClickListener(v -> handleNumpadClick(v, "4"));
- btn5.setOnClickListener(v -> handleNumpadClick(v, "5"));
- btn6.setOnClickListener(v -> handleNumpadClick(v, "6"));
- btn7.setOnClickListener(v -> handleNumpadClick(v, "7"));
- btn8.setOnClickListener(v -> handleNumpadClick(v, "8"));
- btn9.setOnClickListener(v -> handleNumpadClick(v, "9"));
- btn0.setOnClickListener(v -> handleNumpadClick(v, "0"));
- btn000.setOnClickListener(v -> handleNumpadClick(v, "000"));
-
- // Delete button
- btnDelete.setOnClickListener(v -> {
- addClickAnimation(v);
- deleteLastDigit();
- });
-
- // Confirm button
- confirmButton.setOnClickListener(v -> {
- if (confirmButton.isEnabled()) {
- addButtonClickAnimation(v);
- handleConfirmPin();
- }
- });
- }
-
- private void navigateBack() {
- finish();
- }
-
- private void handleNumpadClick(View view, String digit) {
- addClickAnimation(view);
- addDigit(digit);
- }
-
- private void setupInitialStates() {
- // Set initial PIN display
- editTextPin.setText("");
-
- // Set initial button state
- updateButtonState();
-
- // Disable EditText input (only numpad input allowed)
- editTextPin.setFocusable(false);
- editTextPin.setClickable(false);
- editTextPin.setCursorVisible(false);
- }
-
- private void addDigit(String digit) {
- // Validate input length
- if (currentPin.length() >= MAX_PIN_LENGTH) {
- showToast("Maksimal " + MAX_PIN_LENGTH + " digit");
- return;
- }
-
- // Handle special case for 000
- if (digit.equals("000")) {
- if (currentPin.length() + 3 <= MAX_PIN_LENGTH) {
- currentPin.append("000");
- } else {
- int remainingLength = MAX_PIN_LENGTH - currentPin.length();
- if (remainingLength > 0) {
- currentPin.append("0".repeat(remainingLength));
- }
- }
- } else {
- currentPin.append(digit);
- }
-
- updatePinDisplay();
- updateButtonState();
- addInputFeedback();
- }
-
- private void deleteLastDigit() {
- if (currentPin.length() > 0) {
- String current = currentPin.toString();
-
- // If current ends with 000, remove all three digits
- if (current.endsWith("000") && current.length() >= 3) {
- currentPin.delete(currentPin.length() - 3, currentPin.length());
- } else {
- currentPin.deleteCharAt(currentPin.length() - 1);
- }
-
- updatePinDisplay();
- updateButtonState();
- addDeleteFeedback();
- }
- }
-
- private void updatePinDisplay() {
- String pin = currentPin.toString();
-
- if (pin.isEmpty()) {
- editTextPin.setText("");
- } else {
- // Convert digits to asterisks for security
- String maskedPin = "*".repeat(pin.length());
- editTextPin.setText(maskedPin);
- }
- }
-
- private void updateButtonState() {
- boolean hasValidPin = currentPin.length() >= MIN_PIN_LENGTH;
-
- confirmButton.setEnabled(hasValidPin);
-
- if (hasValidPin) {
- // Active state
- confirmButton.setBackgroundResource(R.drawable.button_active_background);
- confirmButton.setTextColor(Color.WHITE);
- confirmButton.setAlpha(1.0f);
- } else {
- // Inactive state
- confirmButton.setBackgroundResource(R.drawable.button_inactive_background);
- confirmButton.setTextColor(Color.parseColor("#999999"));
- confirmButton.setAlpha(0.6f);
- }
- }
-
- private void handleConfirmPin() {
- String pin = currentPin.toString();
-
- if (TextUtils.isEmpty(pin)) {
- showToast("Masukkan PIN");
- return;
- }
-
- if (pin.length() < MIN_PIN_LENGTH) {
- showToast("PIN minimal " + MIN_PIN_LENGTH + " digit");
- return;
- }
-
- // Process PIN verification
- verifyPin(pin);
- }
-
- private void verifyPin(String pin) {
- // Show loading state
- confirmButton.setText("Memverifikasi...");
- confirmButton.setEnabled(false);
-
- // Simulate PIN verification
- animationHandler.postDelayed(() -> {
- // For demo purposes, accept any PIN with length >= 4
- // In real implementation, this would call backend API
-
- if (isValidPin(pin)) {
- // Show success screen instead of toast
- handleSuccessfulVerification();
- } else {
- showToast("PIN tidak valid. Silakan coba lagi.");
- resetPinState();
- }
- }, 2000);
- }
-
- private boolean isValidPin(String pin) {
- // Demo validation - in real app, this would validate against backend
- // For now, reject simple patterns like "1111", "1234", etc.
- return !pin.equals("1111") &&
- !pin.equals("1234") &&
- !pin.equals("0000") &&
- pin.length() >= MIN_PIN_LENGTH;
- }
-
- private void handleSuccessfulVerification() {
- // Show full screen success message
- showSuccessScreen();
-
- // Navigate to receipt page after 2.5 seconds
- animationHandler.postDelayed(() -> {
- navigateToReceiptPage();
- }, 2500);
- }
-
- private void showSuccessScreen() {
- if (successScreen != null) {
- // Hide all other UI components first
- hideMainUIComponents();
-
- // Set success message
- if (successMessage != null) {
- successMessage.setText("Pembayaran Berhasil");
- }
-
- // Show success screen with fade in animation
- successScreen.setVisibility(View.VISIBLE);
- successScreen.setAlpha(0f);
-
- // Fade in the background
- ObjectAnimator backgroundFadeIn = ObjectAnimator.ofFloat(successScreen, "alpha", 0f, 1f);
- backgroundFadeIn.setDuration(500);
- backgroundFadeIn.start();
-
- // Add scale and bounce animation to success icon
- if (successIcon != null) {
- // Start with invisible icon
- successIcon.setScaleX(0f);
- successIcon.setScaleY(0f);
- successIcon.setAlpha(0f);
-
- // Scale animation with bounce effect
- ObjectAnimator scaleX = ObjectAnimator.ofFloat(successIcon, "scaleX", 0f, 1.2f, 1f);
- ObjectAnimator scaleY = ObjectAnimator.ofFloat(successIcon, "scaleY", 0f, 1.2f, 1f);
- ObjectAnimator iconFadeIn = ObjectAnimator.ofFloat(successIcon, "alpha", 0f, 1f);
-
- AnimatorSet iconAnimation = new AnimatorSet();
- iconAnimation.playTogether(scaleX, scaleY, iconFadeIn);
- iconAnimation.setDuration(800);
- iconAnimation.setStartDelay(300);
- iconAnimation.setInterpolator(new android.view.animation.OvershootInterpolator(1.2f));
- iconAnimation.start();
- }
-
- // Add slide up animation to success message
- if (successMessage != null) {
- successMessage.setAlpha(0f);
- successMessage.setTranslationY(50f);
-
- ObjectAnimator messageSlideUp = ObjectAnimator.ofFloat(successMessage, "translationY", 50f, 0f);
- ObjectAnimator messageFadeIn = ObjectAnimator.ofFloat(successMessage, "alpha", 0f, 1f);
-
- AnimatorSet messageAnimation = new AnimatorSet();
- messageAnimation.playTogether(messageSlideUp, messageFadeIn);
- messageAnimation.setDuration(600);
- messageAnimation.setStartDelay(600);
- messageAnimation.setInterpolator(new android.view.animation.DecelerateInterpolator());
- messageAnimation.start();
- }
- }
- }
-
- private void hideMainUIComponents() {
- // Hide all main UI components to create clean full screen success
- if (backNavigation != null) {
- backNavigation.setVisibility(View.GONE);
- }
-
- // Hide the red header backgrounds
- View redStatusBar = findViewById(R.id.red_status_bar);
- View redHeaderBackground = findViewById(R.id.red_header_background);
- if (redStatusBar != null) {
- redStatusBar.setVisibility(View.GONE);
- }
- if (redHeaderBackground != null) {
- redHeaderBackground.setVisibility(View.GONE);
- }
-
- // Hide PIN card
- View pinCard = findViewById(R.id.pin_card);
- if (pinCard != null) {
- pinCard.setVisibility(View.GONE);
- }
-
- // Hide numpad
- View numpadGrid = findViewById(R.id.numpad_grid);
- if (numpadGrid != null) {
- numpadGrid.setVisibility(View.GONE);
- }
-
- // Hide confirm button
- if (confirmButton != null) {
- confirmButton.setVisibility(View.GONE);
- }
- }
-
- private void navigateToReceiptPage() {
- // Create intent to navigate to receipt/struk page
- Intent intent = new Intent(this, ReceiptActivity.class);
-
- // Pass transaction data
- intent.putExtra("transaction_amount", amount);
- intent.putExtra("pin_verified", true);
- intent.putExtra("source_activity", sourceActivity);
-
- // Add transaction details (you can customize these)
- intent.putExtra("merchant_name", "TOKO KLONTONG PAK EKO");
- intent.putExtra("merchant_location", "Ciputat Baru, Tangsel");
- intent.putExtra("transaction_id", generateTransactionId());
- intent.putExtra("transaction_date", getCurrentDateTime());
- intent.putExtra("payment_method", "Kartu Kredit");
- intent.putExtra("card_type", "BCA");
- intent.putExtra("tax_percentage", "11%");
- intent.putExtra("service_fee", "500");
-
- startActivity(intent);
-
- // Set result for calling activity
- Intent resultIntent = new Intent();
- resultIntent.putExtra("pin_verified", true);
- resultIntent.putExtra("pin_length", currentPin.length());
-
- if (!TextUtils.isEmpty(amount)) {
- resultIntent.putExtra(EXTRA_AMOUNT, amount);
- }
-
- setResult(RESULT_OK, resultIntent);
-
- // Finish this activity
- finish();
- }
-
- private String generateTransactionId() {
- // Generate a simple transaction ID (in real app, this would come from backend)
- return String.valueOf(System.currentTimeMillis() % 1000000000L);
- }
-
- private String getCurrentDateTime() {
- // Get current date and time (in real app, use proper date formatting)
- java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd MMMM yyyy HH:mm", java.util.Locale.getDefault());
- return sdf.format(new java.util.Date());
- }
-
- private void resetPinState() {
- currentPin = new StringBuilder();
- updatePinDisplay();
- updateButtonState();
- confirmButton.setText("Konfirmasi");
- confirmButton.setEnabled(false);
- }
-
- // Animation methods
- private void addClickAnimation(View view) {
- ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f, 1f);
- ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f, 1f);
-
- AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(scaleX, scaleY);
- animatorSet.setDuration(150);
- animatorSet.start();
- }
-
- private void addButtonClickAnimation(View view) {
- ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.98f, 1f);
- ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.98f, 1f);
-
- AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(scaleX, scaleY);
- animatorSet.setDuration(200);
- animatorSet.start();
- }
-
- private void addInputFeedback() {
- ObjectAnimator fadeIn = ObjectAnimator.ofFloat(editTextPin, "alpha", 0.7f, 1f);
- fadeIn.setDuration(200);
- fadeIn.start();
- }
-
- private void addDeleteFeedback() {
- ObjectAnimator shake = ObjectAnimator.ofFloat(editTextPin, "translationX", 0f, -10f, 10f, 0f);
- shake.setDuration(300);
- shake.start();
- }
-
- // Utility methods
- private void showToast(String message) {
- Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
- }
-
- @Override
- public void onBackPressed() {
- // Prevent back press when success screen is showing
- if (successScreen != null && successScreen.getVisibility() == View.VISIBLE) {
- return;
- }
- navigateBack();
- super.onBackPressed();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (animationHandler != null) {
- animationHandler.removeCallbacksAndMessages(null);
- }
- }
-
- // Public methods for testing
- public String getCurrentPin() {
- return currentPin.toString();
- }
-
- public boolean isConfirmButtonEnabled() {
- return confirmButton.isEnabled();
- }
-
- // Static helper method to launch PinActivity
- public static void launch(android.content.Context context, String sourceActivity, String amount) {
- Intent intent = new Intent(context, PinActivity.class);
- intent.putExtra(EXTRA_SOURCE_ACTIVITY, sourceActivity);
- if (!TextUtils.isEmpty(amount)) {
- intent.putExtra(EXTRA_AMOUNT, amount);
- }
-
- // Launch for result if context is an Activity
- if (context instanceof AppCompatActivity) {
- ((AppCompatActivity) context).startActivityForResult(intent, 100);
- } else {
- context.startActivity(intent);
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java b/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java
deleted file mode 100644
index 0eb3db2..0000000
--- a/app/src/main/java/com/example/bdkipoc/QrisResultActivity.java
+++ /dev/null
@@ -1,689 +0,0 @@
-package com.example.bdkipoc;
-
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.view.View;
-import android.widget.*;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import org.json.JSONArray;
-import org.json.JSONObject;
-import java.io.*;
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.URL;
-import java.text.NumberFormat;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
-public class QrisResultActivity extends AppCompatActivity {
- // UI Components
- private ImageView qrImageView;
- private TextView amountTextView, referenceTextView, statusTextView;
- private TextView timerTextView, qrStatusTextView;
- private Button downloadQrisButton, checkStatusButton, returnMainButton;
- private ProgressBar progressBar;
-
- // QR Refresh Components
- private Handler qrRefreshHandler;
- private Runnable qrRefreshRunnable;
- private int countdownSeconds = 60;
- private boolean isQrRefreshActive = true;
-
- // Transaction Data
- private String orderId, grossAmount, referenceId, transactionId;
- private String transactionTime, acquirer, merchantId, currentQrImageUrl;
- private int originalAmount;
- private List allOrderIds = new ArrayList<>();
-
- // Configuration
- private static final String BACKEND_BASE = "https://be-edc.msvc.app";
- private static final String WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
- private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM=";
- private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_qris_result);
-
- initializeViews();
- extractIntentData();
- validateAndSetupUI();
- startMonitoring();
- setupClickListeners();
- }
-
- private void initializeViews() {
- qrImageView = findViewById(R.id.qrImageView);
- amountTextView = findViewById(R.id.amountTextView);
- referenceTextView = findViewById(R.id.referenceTextView);
- downloadQrisButton = findViewById(R.id.downloadQrisButton);
- checkStatusButton = findViewById(R.id.checkStatusButton);
- statusTextView = findViewById(R.id.statusTextView);
- returnMainButton = findViewById(R.id.returnMainButton);
- progressBar = findViewById(R.id.progressBar);
- timerTextView = findViewById(R.id.timerTextView);
- qrStatusTextView = findViewById(R.id.qrStatusTextView);
- qrRefreshHandler = new Handler(Looper.getMainLooper());
- }
-
- private void extractIntentData() {
- Intent intent = getIntent();
- currentQrImageUrl = intent.getStringExtra("qrImageUrl");
- originalAmount = intent.getIntExtra("amount", 0);
- referenceId = intent.getStringExtra("referenceId");
- orderId = intent.getStringExtra("orderId");
- grossAmount = intent.getStringExtra("grossAmount");
- transactionId = intent.getStringExtra("transactionId");
- transactionTime = intent.getStringExtra("transactionTime");
- acquirer = intent.getStringExtra("acquirer");
- merchantId = intent.getStringExtra("merchantId");
-
- allOrderIds.add(orderId);
- Log.d("QrisResult", "Initialized with Order ID: " + orderId);
- }
-
- private void validateAndSetupUI() {
- if (orderId == null || transactionId == null) {
- Toast.makeText(this, "Missing transaction details!", Toast.LENGTH_LONG).show();
- finish();
- return;
- }
-
- String formattedAmount = formatCurrency(grossAmount != null ? grossAmount : String.valueOf(originalAmount));
- amountTextView.setText(formattedAmount);
- referenceTextView.setText("Reference ID: " + referenceId);
-
- loadQrImage(currentQrImageUrl);
-
- checkStatusButton.setEnabled(false);
- statusTextView.setText("Waiting for payment...");
- qrStatusTextView.setText("QR Code akan refresh dalam");
- }
-
- private void startMonitoring() {
- startQrRefreshTimer();
- startPaymentMonitoring();
- pollPendingPaymentLog(orderId);
- }
-
- private void startQrRefreshTimer() {
- countdownSeconds = 60;
- isQrRefreshActive = true;
-
- qrRefreshRunnable = new Runnable() {
- @Override
- public void run() {
- if (!isQrRefreshActive) return;
-
- if (countdownSeconds > 0) {
- timerTextView.setText(String.valueOf(countdownSeconds));
- countdownSeconds--;
- qrRefreshHandler.postDelayed(this, 1000);
- } else {
- refreshQrCode();
- }
- }
- };
- qrRefreshHandler.post(qrRefreshRunnable);
- }
-
- private void refreshQrCode() {
- if (!isQrRefreshActive) return;
-
- timerTextView.setText("...");
- qrStatusTextView.setText("Generating new QR Code...");
-
- new Thread(() -> {
- try {
- String newQrUrl = generateNewQrCode();
- runOnUiThread(() -> {
- if (newQrUrl != null) {
- currentQrImageUrl = newQrUrl;
- loadQrImage(newQrUrl);
- allOrderIds.add(orderId);
- updateUIAfterRefresh();
- countdownSeconds = 60;
- qrStatusTextView.setText("QR Code akan refresh dalam");
- Toast.makeText(this, "QR Code refreshed", Toast.LENGTH_SHORT).show();
- } else {
- qrStatusTextView.setText("Failed to refresh QR - trying again in 30s");
- countdownSeconds = 30;
- }
- qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000);
- });
- } catch (Exception e) {
- Log.e("QrisResult", "QR refresh error: " + e.getMessage(), e);
- runOnUiThread(() -> {
- qrStatusTextView.setText("QR refresh error - retrying in 30s");
- countdownSeconds = 30;
- qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000);
- });
- }
- }).start();
- }
-
- private String generateNewQrCode() {
- try {
- String newOrderId = java.util.UUID.randomUUID().toString();
-
- JSONObject customField = new JSONObject();
- customField.put("refresh_of", orderId);
- customField.put("refresh_time", getCurrentISOTime());
- customField.put("original_reference", referenceId);
-
- JSONObject payload = createQrisPayload(newOrderId, customField);
- String response = sendMidtransRequest(payload);
-
- if (response != null) {
- JSONObject jsonResponse = new JSONObject(response);
- if (jsonResponse.has("actions")) {
- JSONArray actions = jsonResponse.getJSONArray("actions");
- if (actions.length() > 0) {
- String newQrUrl = actions.getJSONObject(0).getString("url");
-
- // Update transaction info
- this.transactionId = jsonResponse.optString("transaction_id", transactionId);
- this.transactionTime = jsonResponse.optString("transaction_time", transactionTime);
- this.orderId = newOrderId;
-
- return newQrUrl;
- }
- }
- }
- } catch (Exception e) {
- Log.e("QrisResult", "Generate QR error: " + e.getMessage(), e);
- }
- return null;
- }
-
- private JSONObject createQrisPayload(String orderIdParam, JSONObject customField) throws Exception {
- JSONObject payload = new JSONObject();
- payload.put("payment_type", "qris");
-
- JSONObject transactionDetails = new JSONObject();
- transactionDetails.put("order_id", orderIdParam);
- transactionDetails.put("gross_amount", originalAmount);
- 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_refresh_" + System.currentTimeMillis());
- item.put("price", originalAmount);
- item.put("quantity", 1);
- item.put("name", "QRIS Payment - Refreshed (Ref: " + referenceId + ")");
- itemDetails.put(item);
- payload.put("item_details", itemDetails);
-
- payload.put("custom_field1", customField.toString());
-
- return payload;
- }
-
- private String sendMidtransRequest(JSONObject payload) {
- try {
- 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", WEBHOOK_URL);
- 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();
- if (responseCode == 200 || responseCode == 201) {
- BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
- StringBuilder response = new StringBuilder();
- String line;
- while ((line = br.readLine()) != null) {
- response.append(line.trim());
- }
- return response.toString();
- }
- } catch (Exception e) {
- Log.e("QrisResult", "Midtrans request error: " + e.getMessage(), e);
- }
- return null;
- }
-
- private void startPaymentMonitoring() {
- Handler paymentHandler = new Handler(Looper.getMainLooper());
- Runnable paymentRunnable = new Runnable() {
- @Override
- public void run() {
- checkAllOrderIdsStatus();
- if (!isFinishing() && isQrRefreshActive) {
- paymentHandler.postDelayed(this, 3000);
- }
- }
- };
- paymentHandler.post(paymentRunnable);
- }
-
- private void checkAllOrderIdsStatus() {
- new Thread(() -> {
- try {
- for (String checkOrderId : allOrderIds) {
- if (checkOrderId == null || checkOrderId.isEmpty()) continue;
-
- if (checkPaymentStatus(checkOrderId)) {
- runOnUiThread(() -> {
- stopQrRefresh();
- syncTransactionStatusToBackend("PAID");
- showPaymentSuccess();
- Toast.makeText(this, "Payment Successful! 🎉", Toast.LENGTH_LONG).show();
- });
- return;
- }
- }
- } catch (Exception e) {
- Log.e("QrisResult", "Payment status check error: " + e.getMessage(), e);
- }
- }).start();
- }
-
- private boolean checkPaymentStatus(String checkOrderId) {
- try {
- String urlStr = BACKEND_BASE + "/api-logs?request_body_search_strict=" +
- java.net.URLEncoder.encode("{\"order_id\":\"" + checkOrderId + "\"}", "UTF-8");
-
- URL url = new URL(urlStr);
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setRequestMethod("GET");
- conn.setRequestProperty("Accept", "application/json");
- conn.setConnectTimeout(5000);
- conn.setReadTimeout(5000);
-
- if (conn.getResponseCode() == 200) {
- BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
- StringBuilder response = new StringBuilder();
- String line;
- while ((line = br.readLine()) != null) {
- response.append(line);
- }
-
- JSONObject json = new JSONObject(response.toString());
- 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 (checkOrderId.equals(logOrderId) &&
- (transactionStatus.equals("settlement") ||
- transactionStatus.equals("capture") ||
- transactionStatus.equals("success"))) {
- return true;
- }
- }
- }
- }
- }
- } catch (Exception e) {
- Log.e("QrisResult", "Payment check error: " + e.getMessage(), e);
- }
- return false;
- }
-
- private void loadQrImage(String qrImageUrl) {
- if (qrImageUrl != null && !qrImageUrl.isEmpty()) {
- new DownloadImageTask(qrImageView).execute(qrImageUrl);
- } else {
- qrImageView.setVisibility(View.GONE);
- downloadQrisButton.setEnabled(false);
- }
- }
-
- private void setupClickListeners() {
- downloadQrisButton.setOnClickListener(v -> downloadQrCode());
- checkStatusButton.setOnClickListener(v -> {
- stopQrRefresh();
- simulateWebhook();
- });
- returnMainButton.setOnClickListener(v -> returnToMain());
- }
-
- private void stopQrRefresh() {
- isQrRefreshActive = false;
- if (qrRefreshHandler != null && qrRefreshRunnable != null) {
- qrRefreshHandler.removeCallbacks(qrRefreshRunnable);
- }
- timerTextView.setVisibility(View.GONE);
- qrStatusTextView.setVisibility(View.GONE);
- }
-
- private void showPaymentSuccess() {
- stopQrRefresh();
-
- qrImageView.setVisibility(View.GONE);
- amountTextView.setVisibility(View.GONE);
- referenceTextView.setVisibility(View.GONE);
- downloadQrisButton.setVisibility(View.GONE);
- checkStatusButton.setVisibility(View.GONE);
-
- statusTextView.setText("✅ Payment Successful!\n\nTransaction ID: " + transactionId +
- "\nReference: " + referenceId +
- "\nAmount: " + formatCurrency(grossAmount));
-
- returnMainButton.setVisibility(View.VISIBLE);
-
- new Handler(Looper.getMainLooper()).postDelayed(this::launchReceiptActivity, 2000);
- }
-
- private void syncTransactionStatusToBackend(String status) {
- new Thread(() -> {
- try {
- JSONObject updatePayload = new JSONObject();
- updatePayload.put("status", status);
- updatePayload.put("transaction_status", status);
- updatePayload.put("updated_at", getCurrentISOTime());
- updatePayload.put("settlement_time", getCurrentISOTime());
-
- JSONObject body = new JSONObject();
- body.put("reference_id", referenceId);
- body.put("update_data", updatePayload);
-
- String updateUrl = BACKEND_BASE + "/transactions/update-by-reference";
- URL url = new URI(updateUrl).toURL();
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setRequestMethod("POST");
- conn.setRequestProperty("Content-Type", "application/json");
- conn.setDoOutput(true);
-
- try (OutputStream os = conn.getOutputStream()) {
- byte[] input = body.toString().getBytes("utf-8");
- os.write(input, 0, input.length);
- }
-
- int responseCode = conn.getResponseCode();
- Log.d("QrisResult", "Backend sync response: " + responseCode);
-
- } catch (Exception e) {
- Log.e("QrisResult", "Backend sync error: " + e.getMessage(), e);
- }
- }).start();
- }
-
- private void simulateWebhook() {
- progressBar.setVisibility(View.VISIBLE);
- statusTextView.setText("Simulating payment...");
- checkStatusButton.setEnabled(false);
- stopQrRefresh();
-
- new Thread(() -> {
- try {
- JSONObject payload = createWebhookPayload();
- sendWebhookRequest(payload);
- Thread.sleep(2000);
- } catch (Exception e) {
- Log.e("QrisResult", "Webhook simulation error: " + e.getMessage(), e);
- }
-
- runOnUiThread(() -> {
- progressBar.setVisibility(View.GONE);
- showPaymentSuccess();
- });
- }).start();
- }
-
- private JSONObject createWebhookPayload() throws Exception {
- String serverKey = getServerKey();
- String signatureKey = generateSignature(orderId, "200", grossAmount, serverKey);
-
- JSONObject payload = new JSONObject();
- payload.put("transaction_type", "on-us");
- payload.put("transaction_time", transactionTime != null ? transactionTime : getCurrentISOTime());
- payload.put("transaction_status", "settlement");
- payload.put("transaction_id", transactionId);
- payload.put("status_message", "midtrans payment notification");
- payload.put("status_code", "200");
- payload.put("signature_key", signatureKey);
- payload.put("payment_type", "qris");
- payload.put("order_id", orderId);
- payload.put("gross_amount", grossAmount);
- payload.put("reference_id", referenceId);
-
- return payload;
- }
-
- private void sendWebhookRequest(JSONObject payload) {
- try {
- URL url = new URL(WEBHOOK_URL);
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setRequestMethod("POST");
- conn.setRequestProperty("Content-Type", "application/json");
- conn.setDoOutput(true);
-
- try (OutputStream os = conn.getOutputStream()) {
- os.write(payload.toString().getBytes());
- }
-
- Log.d("QrisResult", "Webhook response: " + conn.getResponseCode());
- } catch (Exception e) {
- Log.e("QrisResult", "Webhook request error: " + e.getMessage(), e);
- }
- }
-
- // Utility Methods
- private String formatCurrency(String amount) {
- try {
- double amountDouble = Double.parseDouble(amount);
- NumberFormat formatter = NumberFormat.getCurrencyInstance(new Locale("id", "ID"));
- return formatter.format(amountDouble);
- } catch (NumberFormatException e) {
- return "IDR " + amount;
- }
- }
-
- private String getCurrentISOTime() {
- return new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
- .format(new java.util.Date());
- }
-
- private String getServerKey() {
- try {
- String base64 = MIDTRANS_AUTH.replace("Basic ", "");
- byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT);
- return new String(decoded).replace(":", "");
- } catch (Exception e) {
- return "";
- }
- }
-
- private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
- String input = orderId + statusCode + grossAmount + serverKey;
- try {
- java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-512");
- byte[] messageDigest = md.digest(input.getBytes());
- StringBuilder hexString = new StringBuilder();
- for (byte b : messageDigest) {
- String hex = Integer.toHexString(0xff & b);
- if (hex.length() == 1) hexString.append('0');
- hexString.append(hex);
- }
- return hexString.toString();
- } catch (Exception e) {
- return "dummy_signature";
- }
- }
-
- // Navigation and Lifecycle
- private void returnToMain() {
- stopQrRefresh();
- Intent intent = new Intent(this, MainActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- finishAffinity();
- }
-
- private void launchReceiptActivity() {
- Intent intent = new Intent(this, ReceiptActivity.class);
- intent.putExtra("calling_activity", "QrisResultActivity");
- intent.putExtra("transaction_id", transactionId);
- intent.putExtra("reference_id", referenceId);
- intent.putExtra("order_id", orderId);
- intent.putExtra("transaction_amount", String.valueOf(originalAmount));
- intent.putExtra("gross_amount", grossAmount != null ? grossAmount : String.valueOf(originalAmount));
- intent.putExtra("created_at", getCurrentISOTime());
- intent.putExtra("payment_method", "QRIS");
- intent.putExtra("acquirer", acquirer != null ? acquirer : "qris");
- intent.putExtra("mid", "71000026521");
- intent.putExtra("tid", "73001500");
- startActivity(intent);
- }
-
- private void downloadQrCode() {
- try {
- qrImageView.setDrawingCacheEnabled(true);
- qrImageView.buildDrawingCache();
- Bitmap bitmap = qrImageView.getDrawingCache();
- if (bitmap != null) {
- saveImageToGallery(bitmap, "qris_code_" + System.currentTimeMillis());
- } else {
- Toast.makeText(this, "Unable to capture QR code image", Toast.LENGTH_SHORT).show();
- }
- } catch (Exception e) {
- Toast.makeText(this, "Error downloading QR code", Toast.LENGTH_LONG).show();
- } finally {
- qrImageView.setDrawingCacheEnabled(false);
- }
- }
-
- private void saveImageToGallery(Bitmap bitmap, String fileName) {
- try {
- String savedImageURL = android.provider.MediaStore.Images.Media.insertImage(
- getContentResolver(), bitmap, fileName, "QRIS Payment QR Code");
- if (savedImageURL != null) {
- Toast.makeText(this, "QRIS saved to gallery", Toast.LENGTH_SHORT).show();
- } else {
- Toast.makeText(this, "Failed to save QRIS", Toast.LENGTH_SHORT).show();
- }
- } catch (Exception e) {
- Toast.makeText(this, "Error saving QRIS", Toast.LENGTH_LONG).show();
- }
- }
-
- private void updateUIAfterRefresh() {
- String refreshTime = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date());
- referenceTextView.setText("Reference ID: " + referenceId + " (Refreshed at " + refreshTime + ")");
- }
-
- private void pollPendingPaymentLog(String orderId) {
- progressBar.setVisibility(View.VISIBLE);
- statusTextView.setText("Checking payment status...");
-
- new Thread(() -> {
- int maxAttempts = 12;
- boolean found = false;
-
- for (int attempt = 0; attempt < maxAttempts && !found; attempt++) {
- try {
- found = checkPaymentStatus(orderId);
- if (!found && attempt < maxAttempts - 1) {
- Thread.sleep(2000);
- }
- } catch (Exception e) {
- Log.e("QrisResult", "Polling error: " + e.getMessage());
- }
- }
-
- final boolean logFound = found;
- runOnUiThread(() -> {
- progressBar.setVisibility(View.GONE);
- if (logFound) {
- checkStatusButton.setEnabled(true);
- statusTextView.setText("Ready to simulate payment");
- Toast.makeText(this, "Payment log found!", Toast.LENGTH_SHORT).show();
- } else {
- statusTextView.setText("Payment log not found");
- checkStatusButton.setEnabled(true);
- }
- });
- }).start();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- stopQrRefresh();
- }
-
- @Override
- public void onBackPressed() {
- stopQrRefresh();
- returnToMain();
- super.onBackPressed();
- }
-
- // AsyncTask for downloading QR image
- private static class DownloadImageTask extends AsyncTask {
- private ImageView imageView;
- private String errorMessage;
-
- DownloadImageTask(ImageView imageView) {
- this.imageView = imageView;
- }
-
- @Override
- protected Bitmap doInBackground(String... urls) {
- try {
- URL url = new URI(urls[0]).toURL();
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- connection.setDoInput(true);
- connection.setConnectTimeout(10000);
- connection.setReadTimeout(10000);
- connection.connect();
-
- if (connection.getResponseCode() == 200) {
- InputStream input = connection.getInputStream();
- return BitmapFactory.decodeStream(input);
- } else {
- errorMessage = "Failed to download QR code";
- }
- } catch (Exception e) {
- errorMessage = "Error downloading QR code: " + e.getMessage();
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Bitmap result) {
- if (result != null) {
- imageView.setImageBitmap(result);
- } else {
- imageView.setImageResource(android.R.drawable.ic_menu_report_image);
- if (errorMessage != null && imageView.getContext() != null) {
- Toast.makeText(imageView.getContext(), errorMessage, Toast.LENGTH_LONG).show();
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/ReceiptActivity.java b/app/src/main/java/com/example/bdkipoc/ReceiptActivity.java
index 096aaae..b4521ea 100644
--- a/app/src/main/java/com/example/bdkipoc/ReceiptActivity.java
+++ b/app/src/main/java/com/example/bdkipoc/ReceiptActivity.java
@@ -23,6 +23,10 @@ import java.net.HttpURLConnection;
import java.io.OutputStream;
import java.net.URL;
import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.example.bdkipoc.cetakulang.ReprintActivity;
public class ReceiptActivity extends AppCompatActivity {
@@ -50,6 +54,28 @@ public class ReceiptActivity extends AppCompatActivity {
private LinearLayout emailButton;
private Button finishButton;
+ // ✅ ENHANCED: Mapping dari technical issuer ke display name
+ private static final Map ISSUER_DISPLAY_MAP = new HashMap() {{
+ 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");
+ }};
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -116,10 +142,27 @@ public class ReceiptActivity extends AppCompatActivity {
finishButton.setOnClickListener(v -> handleFinish());
}
+ /**
+ * ✅ ENHANCED: Load transaction data with support for both EMV/Card and QRIS
+ */
private void loadTransactionData() {
Intent intent = getIntent();
if (intent != null) {
- Log.d("ReceiptActivity", "=== LOADING TRANSACTION DATA ===");
+ Log.d("ReceiptActivity", "=== LOADING ENHANCED TRANSACTION DATA ===");
+
+ // ✅ DETECT TRANSACTION TYPE
+ String callingActivity = intent.getStringExtra("calling_activity");
+ String channelCategory = intent.getStringExtra("channel_category");
+ String channelCode = intent.getStringExtra("channel_code");
+ boolean isEmvTransaction = "EMV_CARD".equals(channelCategory) || "ResultTransactionActivity".equals(callingActivity);
+ boolean isQrisTransaction = "QRIS".equalsIgnoreCase(channelCode) || "QrisResultActivity".equals(callingActivity);
+
+ Log.d("ReceiptActivity", "🔍 TRANSACTION TYPE DETECTION:");
+ Log.d("ReceiptActivity", " Calling Activity: " + callingActivity);
+ Log.d("ReceiptActivity", " Channel Category: " + channelCategory);
+ Log.d("ReceiptActivity", " Channel Code: " + channelCode);
+ Log.d("ReceiptActivity", " Is EMV Transaction: " + isEmvTransaction);
+ Log.d("ReceiptActivity", " Is QRIS Transaction: " + isQrisTransaction);
// Get all available data from intent
String amount = intent.getStringExtra("transaction_amount");
@@ -133,99 +176,154 @@ public class ReceiptActivity extends AppCompatActivity {
String createdAt = intent.getStringExtra("created_at");
String paymentMethodStr = intent.getStringExtra("payment_method");
String cardTypeStr = intent.getStringExtra("card_type");
- String channelCode = intent.getStringExtra("channel_code");
- String channelCategory = intent.getStringExtra("channel_category");
String acquirer = intent.getStringExtra("acquirer");
String mid = intent.getStringExtra("mid");
String tid = intent.getStringExtra("tid");
+ // ✅ EMV SPECIFIC DATA
+ String emvCardholderName = intent.getStringExtra("emv_cardholder_name");
+ String emvAid = intent.getStringExtra("emv_aid");
+ String emvExpiry = intent.getStringExtra("emv_expiry");
+ String cardNumber = intent.getStringExtra("card_number");
+ String midtransResponse = intent.getStringExtra("midtrans_response");
+ boolean emvMode = intent.getBooleanExtra("emv_mode", false);
+
+ // ✅ QRIS SPECIFIC DATA
+ String qrString = intent.getStringExtra("qr_string");
+
// Log received data for debugging
Log.d("ReceiptActivity", "🔍 RECEIVED DATA:");
Log.d("ReceiptActivity", " amount: " + amount);
Log.d("ReceiptActivity", " referenceId: " + referenceId);
Log.d("ReceiptActivity", " orderId: " + orderId);
- Log.d("ReceiptActivity", " channelCode: " + channelCode);
Log.d("ReceiptActivity", " acquirer (from intent): " + acquirer);
Log.d("ReceiptActivity", " createdAt: " + createdAt);
+ Log.d("ReceiptActivity", " EMV Mode: " + emvMode);
+ Log.d("ReceiptActivity", " EMV Cardholder: " + emvCardholderName);
+ Log.d("ReceiptActivity", " Card Number: " + (cardNumber != null ? cardNumber : "N/A"));
+ Log.d("ReceiptActivity", " QR String Available: " + (qrString != null && !qrString.isEmpty()));
// 1. Set merchant data with defaults
- merchantName.setText(merchantNameStr != null ? merchantNameStr : "Marcel Panjaitan");
- merchantLocation.setText(merchantLocationStr != null ? merchantLocationStr : "Jakarta, Indonesia");
+ merchantName.setText(merchantNameStr != null ? merchantNameStr : "TOKO KLONTONG PAK EKO");
+ merchantLocation.setText(merchantLocationStr != null ? merchantLocationStr : "Ciputat Baru, Tangsel");
// 2. Set MID and TID
- midText.setText(mid != null ? mid : "71000026521");
- tidText.setText(tid != null ? tid : "73001500");
+ midText.setText("MID: " + (mid != null ? mid : "123456789901"));
+ tidText.setText("TID: " + (tid != null ? tid : "123456789901"));
// 3. Set transaction number
- String displayTransactionNumber = null;
- if (referenceId != null && !referenceId.isEmpty()) {
- displayTransactionNumber = referenceId;
- } else if (transactionId != null && !transactionId.isEmpty()) {
- displayTransactionNumber = transactionId;
- } else if (orderId != null && !orderId.isEmpty()) {
- displayTransactionNumber = orderId;
- }
- transactionNumber.setText(displayTransactionNumber != null ? displayTransactionNumber : "N/A");
+ String displayTransactionNumber = getDisplayTransactionNumber(referenceId, transactionId, orderId);
+ transactionNumber.setText(displayTransactionNumber);
// 4. Set transaction date
- String displayDate = null;
- if (createdAt != null && !createdAt.isEmpty()) {
- displayDate = formatDateFromCreatedAt(createdAt);
- } else if (transactionDateStr != null && !transactionDateStr.isEmpty()) {
- displayDate = transactionDateStr;
- } else {
- displayDate = getCurrentDateTime();
- }
+ String displayDate = getDisplayTransactionDate(createdAt, transactionDateStr, isEmvTransaction);
transactionDate.setText(displayDate);
- // 5. Set payment method
- String displayPaymentMethod = getPaymentMethodFromChannelCode(channelCode, paymentMethodStr);
+ // 5. ✅ ENHANCED: Set payment method based on transaction type
+ String displayPaymentMethod = getDisplayPaymentMethod(channelCode, paymentMethodStr, isEmvTransaction, emvMode);
paymentMethod.setText(displayPaymentMethod);
- // 6. ✅ IMPROVED: Enhanced card type detection for QRIS
- String displayCardType = null;
-
- if (channelCode != null && channelCode.equalsIgnoreCase("QRIS")) {
- Log.d("ReceiptActivity", "🔍 QRIS transaction detected - searching for real acquirer");
-
- // For QRIS, try to get real acquirer from webhook data
- if (referenceId != null && !referenceId.isEmpty()) {
- String realAcquirer = fetchRealAcquirerSync(referenceId);
- if (realAcquirer != null && !realAcquirer.isEmpty() && !realAcquirer.equalsIgnoreCase("qris")) {
- displayCardType = getCardTypeFromAcquirer(realAcquirer, null, null);
- Log.d("ReceiptActivity", "✅ QRIS real acquirer found: " + realAcquirer + " -> " + displayCardType);
- } else {
- Log.w("ReceiptActivity", "⚠️ QRIS real acquirer not found, using generic QRIS");
- displayCardType = "QRIS";
-
- // Start async search for better results
- fetchRealAcquirerFromWebhook(referenceId);
- }
- } else {
- displayCardType = "QRIS";
- }
- } else {
- // Non-QRIS transaction
- displayCardType = getCardTypeFromAcquirer(acquirer, channelCode, cardTypeStr);
- }
-
+ // 6. ✅ ENHANCED: Set card type with EMV and QRIS priority
+ String displayCardType = getDisplayCardType(cardTypeStr, acquirer, channelCode, isEmvTransaction,
+ isQrisTransaction, midtransResponse, referenceId);
cardType.setText(displayCardType);
- Log.d("ReceiptActivity", "💳 FINAL CARD TYPE: " + displayCardType);
- // 7. Format and set amounts
- setAmountData(amount, grossAmount);
+ // 7. ✅ Format and set amounts with proper calculation
+ setAmountDataEnhanced(amount, grossAmount, isEmvTransaction, isQrisTransaction);
- Log.d("ReceiptActivity", "=== TRANSACTION DATA LOADED ===");
+ Log.d("ReceiptActivity", "💳 FINAL DISPLAY VALUES:");
+ Log.d("ReceiptActivity", " Payment Method: " + displayPaymentMethod);
+ Log.d("ReceiptActivity", " Card Type: " + displayCardType);
+ Log.d("ReceiptActivity", " Transaction Number: " + displayTransactionNumber);
+ Log.d("ReceiptActivity", "=== ENHANCED TRANSACTION DATA LOADED ===");
}
}
-
+
+ /**
+ * Get display transaction number with priority
+ */
+ private String getDisplayTransactionNumber(String referenceId, String transactionId, String orderId) {
+ if (referenceId != null && !referenceId.isEmpty()) {
+ return referenceId;
+ } else if (transactionId != null && !transactionId.isEmpty()) {
+ // For long transaction IDs, show last 10 characters
+ return transactionId.length() > 10 ?
+ transactionId.substring(transactionId.length() - 10) : transactionId;
+ } else if (orderId != null && !orderId.isEmpty()) {
+ return orderId.length() > 10 ?
+ orderId.substring(orderId.length() - 10) : orderId;
+ }
+ return String.valueOf(System.currentTimeMillis() % 10000000000L);
+ }
+
+ /**
+ * Get display transaction date with proper formatting
+ */
+ private String getDisplayTransactionDate(String createdAt, String transactionDateStr, boolean isEmvTransaction) {
+ String dateToFormat = null;
+
+ if (createdAt != null && !createdAt.isEmpty()) {
+ dateToFormat = createdAt;
+ } else if (transactionDateStr != null && !transactionDateStr.isEmpty()) {
+ dateToFormat = transactionDateStr;
+ }
+
+ if (dateToFormat != null) {
+ if (isEmvTransaction) {
+ return formatDateForEmvTransaction(dateToFormat);
+ } else {
+ return formatDateFromCreatedAt(dateToFormat);
+ }
+ }
+
+ return getCurrentDateTime();
+ }
+
+ /**
+ * Format date specifically for EMV transactions
+ */
+ private String formatDateForEmvTransaction(String dateString) {
+ try {
+ // EMV transactions might use different date formats
+ String[] inputFormats = {
+ "dd MMMM yyyy HH:mm",
+ "yyyy-MM-dd HH:mm:ss",
+ "yyyy-MM-dd'T'HH:mm:ss'Z'",
+ "dd/MM/yyyy HH:mm"
+ };
+
+ SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMMM yyyy HH:mm", new Locale("id", "ID"));
+
+ for (String format : inputFormats) {
+ try {
+ SimpleDateFormat inputFormat = new SimpleDateFormat(format,
+ format.contains("MMMM") ? new Locale("id", "ID") : Locale.getDefault());
+ Date date = inputFormat.parse(dateString);
+ String formatted = outputFormat.format(date);
+ Log.d("ReceiptActivity", "EMV Date formatting: '" + dateString + "' -> '" + formatted + "'");
+ return formatted;
+ } catch (Exception ignored) {
+ // Try next format
+ }
+ }
+
+ // If all formats fail, return as-is
+ Log.w("ReceiptActivity", "Could not format EMV date: " + dateString);
+ return dateString;
+
+ } catch (Exception e) {
+ Log.e("ReceiptActivity", "Error formatting EMV date: " + dateString, e);
+ return dateString;
+ }
+ }
+
private String formatDateFromCreatedAt(String createdAt) {
try {
// Input format from database: "yyyy-MM-dd HH:mm:ss"
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
// Output format for receipt: "dd/MM/yyyy HH:mm"
- SimpleDateFormat outputFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm", new Locale("id", "ID"));
+ SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMMM yyyy HH:mm", new Locale("id", "ID"));
Date date = inputFormat.parse(createdAt);
String formatted = outputFormat.format(date);
@@ -240,6 +338,39 @@ public class ReceiptActivity extends AppCompatActivity {
}
}
+ /**
+ * Get display payment method with EMV support
+ */
+ private String getDisplayPaymentMethod(String channelCode, String fallbackPaymentMethod,
+ boolean isEmvTransaction, boolean emvMode) {
+ if (isEmvTransaction) {
+ // For EMV transactions, be more specific
+ if (emvMode) {
+ if (channelCode != null) {
+ switch (channelCode.toUpperCase()) {
+ case "CREDIT": return "Kartu Kredit (EMV)";
+ case "DEBIT": return "Kartu Debit (EMV)";
+ default: return "Kartu Kredit (EMV)";
+ }
+ }
+ return "Kartu Kredit (EMV)";
+ } else {
+ // Magnetic stripe
+ if (channelCode != null) {
+ switch (channelCode.toUpperCase()) {
+ case "CREDIT": return "Kartu Kredit";
+ case "DEBIT": return "Kartu Debit";
+ default: return "Kartu Kredit";
+ }
+ }
+ return "Kartu Kredit";
+ }
+ } else {
+ // For QRIS and other transactions, use existing logic
+ return getPaymentMethodFromChannelCode(channelCode, fallbackPaymentMethod);
+ }
+ }
+
/**
* Get payment method name from channel_code with comprehensive mapping
*/
@@ -289,6 +420,78 @@ public class ReceiptActivity extends AppCompatActivity {
return fallbackPaymentMethod != null ? fallbackPaymentMethod : "QRIS";
}
+ /**
+ * ✅ ENHANCED: Card type detection with EMV and QRIS priority
+ */
+ private String getDisplayCardType(String cardTypeStr, String acquirer, String channelCode,
+ boolean isEmvTransaction, boolean isQrisTransaction,
+ String midtransResponse, String referenceId) {
+
+ Log.d("ReceiptActivity", "🔍 ENHANCED CARD TYPE DETECTION:");
+ Log.d("ReceiptActivity", " Input Card Type: " + cardTypeStr);
+ Log.d("ReceiptActivity", " Input Acquirer: " + acquirer);
+ Log.d("ReceiptActivity", " Is EMV Transaction: " + isEmvTransaction);
+ Log.d("ReceiptActivity", " Is QRIS Transaction: " + isQrisTransaction);
+
+ if (isEmvTransaction) {
+ // ✅ FOR EMV TRANSACTIONS: Priority to cardTypeStr from ResultTransactionActivity
+ if (cardTypeStr != null && !cardTypeStr.isEmpty() && !cardTypeStr.equalsIgnoreCase("unknown")) {
+ Log.d("ReceiptActivity", "✅ EMV: Using provided card type: " + cardTypeStr);
+ return cardTypeStr;
+ }
+
+ // ✅ FALLBACK: Try to extract from Midtrans response
+ if (midtransResponse != null && !midtransResponse.isEmpty()) {
+ String bankFromMidtrans = extractBankFromMidtransResponse(midtransResponse);
+ if (bankFromMidtrans != null && !bankFromMidtrans.isEmpty()) {
+ Log.d("ReceiptActivity", "✅ EMV: Using bank from Midtrans response: " + bankFromMidtrans);
+ return bankFromMidtrans;
+ }
+ }
+
+ // ✅ FALLBACK: Use acquirer
+ if (acquirer != null && !acquirer.isEmpty() && !acquirer.equalsIgnoreCase("qris")) {
+ String mappedAcquirer = getCardTypeFromAcquirer(acquirer, channelCode, null);
+ Log.d("ReceiptActivity", "✅ EMV: Using mapped acquirer: " + mappedAcquirer);
+ return mappedAcquirer;
+ }
+
+ Log.d("ReceiptActivity", "⚠️ EMV: Using default fallback: BCA");
+ return "BCA"; // Default for EMV
+
+ } else if (isQrisTransaction) {
+ // ✅ FOR QRIS TRANSACTIONS: Enhanced detection
+ Log.d("ReceiptActivity", "🔍 QRIS transaction detected - searching for real acquirer");
+
+ // Priority 1: Use provided cardTypeStr if it's not generic
+ if (cardTypeStr != null && !cardTypeStr.isEmpty() &&
+ !cardTypeStr.equalsIgnoreCase("qris") && !cardTypeStr.equalsIgnoreCase("unknown")) {
+ Log.d("ReceiptActivity", "✅ QRIS: Using provided specific card type: " + cardTypeStr);
+ return cardTypeStr;
+ }
+
+ // Priority 2: Search webhook logs for real acquirer
+ if (referenceId != null && !referenceId.isEmpty()) {
+ String realAcquirer = fetchRealAcquirerSync(referenceId);
+ if (realAcquirer != null && !realAcquirer.isEmpty() && !realAcquirer.equalsIgnoreCase("qris")) {
+ String mappedQrisAcquirer = getCardTypeFromAcquirer(realAcquirer, null, null);
+ Log.d("ReceiptActivity", "✅ QRIS real acquirer found: " + realAcquirer + " -> " + mappedQrisAcquirer);
+ return mappedQrisAcquirer;
+ } else {
+ Log.w("ReceiptActivity", "⚠️ QRIS real acquirer not found, using generic QRIS");
+ // Start async search for better results
+ fetchRealAcquirerFromWebhook(referenceId);
+ return "QRIS";
+ }
+ } else {
+ return "QRIS";
+ }
+ } else {
+ // ✅ FOR OTHER TRANSACTIONS: Use standard logic
+ return getCardTypeFromAcquirer(acquirer, channelCode, cardTypeStr);
+ }
+ }
+
private String getCardTypeFromAcquirer(String acquirer, String channelCode, String fallbackCardType) {
// STEP 1: If we have a valid acquirer that's not generic "qris", use it
if (acquirer != null && !acquirer.isEmpty() && !acquirer.equalsIgnoreCase("qris")) {
@@ -297,88 +500,14 @@ public class ReceiptActivity extends AppCompatActivity {
Log.d("ReceiptActivity", "🔍 Mapping acquirer: '" + acquirer + "' -> '" + acq + "'");
// ✅ COMPREHENSIVE acquirer mapping (case-insensitive)
+ String mappedName = ISSUER_DISPLAY_MAP.get(acq);
+ if (mappedName != null) {
+ Log.d("ReceiptActivity", "✅ Mapped acquirer: " + acquirer + " -> " + mappedName);
+ return mappedName;
+ }
+
+ // Additional mapping for variations not in the map
switch (acq) {
- // E-Wallet acquirers (most common for QRIS)
- case "gopay":
- case "go-pay":
- case "gojek": return "GoPay";
-
- case "shopeepay":
- case "shopee_pay":
- case "shopee": return "ShopeePay";
-
- case "ovo": return "OVO";
-
- case "dana": return "DANA";
-
- case "linkaja":
- case "link_aja":
- case "tcash": return "LinkAja";
-
- case "jenius":
- case "btpn": return "Jenius";
-
- case "kaspro":
- case "kas_pro": return "KasPro";
-
- case "sakuku":
- case "saku_ku": return "SakuKu";
-
- case "doku":
- case "doku_wallet": return "DOKU";
-
- case "paymi":
- case "pay_mi": return "PayMi";
-
- case "isaku":
- case "i_saku": return "i.Saku";
-
- // Bank acquirers
- case "bca":
- case "bank_bca": return "BCA";
-
- case "mandiri":
- case "bank_mandiri":
- case "mandiri_bill": return "Mandiri";
-
- case "bni":
- case "bank_bni":
- case "bni_va": return "BNI";
-
- case "bri":
- case "bank_bri":
- case "bri_va": return "BRI";
-
- case "permata":
- case "bank_permata":
- case "permata_va": return "Permata";
-
- case "cimb":
- case "cimb_niaga":
- case "bank_cimb":
- case "cimb_va": return "CIMB Niaga";
-
- case "danamon":
- case "bank_danamon":
- case "danamon_va": return "Danamon";
-
- case "bsi":
- case "bank_bsi":
- case "bsi_va":
- case "syariah_indonesia": return "BSI";
-
- case "maybank":
- case "bank_maybank": return "Maybank";
-
- case "bca_digital":
- case "blu": return "BCA Digital";
-
- case "jago":
- case "bank_jago": return "Bank Jago";
-
- case "seabank":
- case "sea_bank": return "SeaBank";
-
// Credit card acquirers
case "visa": return "Visa";
case "mastercard":
@@ -738,6 +867,78 @@ public class ReceiptActivity extends AppCompatActivity {
return null; // No acquirer found for specified criteria
}
+ /**
+ * Extract bank from Midtrans response JSON string
+ */
+ private String extractBankFromMidtransResponse(String midtransResponse) {
+ try {
+ org.json.JSONObject response = new org.json.JSONObject(midtransResponse);
+
+ // Try different possible bank fields
+ String[] bankFields = {"bank", "issuer", "acquiring_bank", "issuer_bank"};
+
+ for (String field : bankFields) {
+ if (response.has(field)) {
+ String bankValue = response.getString(field);
+ if (bankValue != null && !bankValue.trim().isEmpty() && !bankValue.equalsIgnoreCase("qris")) {
+ Log.d("ReceiptActivity", "Found bank in Midtrans response (" + field + "): " + bankValue);
+ return formatBankNameForReceipt(bankValue);
+ }
+ }
+ }
+
+ Log.w("ReceiptActivity", "No valid bank found in Midtrans response");
+ return null;
+
+ } catch (Exception e) {
+ Log.e("ReceiptActivity", "Error extracting bank from Midtrans response: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Format bank name specifically for receipt display
+ */
+ private String formatBankNameForReceipt(String bankName) {
+ if (bankName == null || bankName.trim().isEmpty()) {
+ return "BCA"; // Default
+ }
+
+ String formatted = bankName.trim();
+
+ // Common bank name mappings for receipt display
+ switch (formatted.toUpperCase()) {
+ case "BCA":
+ case "BANK BCA":
+ case "BANK CENTRAL ASIA": return "BCA";
+
+ case "MANDIRI":
+ case "BANK MANDIRI": return "Mandiri";
+
+ case "BNI":
+ case "BANK BNI":
+ case "BANK NEGARA INDONESIA": return "BNI";
+
+ case "BRI":
+ case "BANK BRI":
+ case "BANK RAKYAT INDONESIA": return "BRI";
+
+ case "CIMB":
+ case "CIMB NIAGA":
+ case "BANK CIMB NIAGA": return "CIMB Niaga";
+
+ case "DANAMON":
+ case "BANK DANAMON": return "Danamon";
+
+ case "PERMATA":
+ case "BANK PERMATA": return "Permata";
+
+ default:
+ // Return capitalized version
+ return capitalizeFirstLetter(formatted);
+ }
+ }
+
private String capitalizeFirstLetter(String input) {
if (input == null || input.isEmpty()) {
return input;
@@ -756,51 +957,79 @@ public class ReceiptActivity extends AppCompatActivity {
return cleaned.substring(0, 1).toUpperCase() + cleaned.substring(1).toLowerCase();
}
- private void setAmountData(String amount, String grossAmount) {
- // Prioritize 'amount' over 'grossAmount' for transaction data
+ /**
+ * ✅ ENHANCED: Set amount data with EMV and QRIS support
+ */
+ private void setAmountDataEnhanced(String amount, String grossAmount, boolean isEmvTransaction, boolean isQrisTransaction) {
String amountToUse = amount != null ? amount : grossAmount;
- Log.d("ReceiptActivity", "Setting amount data - amount: " + amount +
- ", grossAmount: " + grossAmount + ", using: " + amountToUse);
+ Log.d("ReceiptActivity", "Setting enhanced amount data - amount: " + amount +
+ ", grossAmount: " + grossAmount + ", using: " + amountToUse +
+ ", isEMV: " + isEmvTransaction + ", isQRIS: " + isQrisTransaction);
if (amountToUse != null) {
try {
- // Clean and parse the amount
String cleanAmount = cleanAmountString(amountToUse);
Log.d("ReceiptActivity", "Cleaned amount: " + cleanAmount);
- // Parse as long integer (Indonesian Rupiah doesn't use decimal cents)
long amountLong = Long.parseLong(cleanAmount);
// Set transaction total
- transactionTotal.setText("Rp " + formatCurrency(amountLong));
+ transactionTotal.setText(formatCurrency(amountLong));
- // Calculate tax and service fee (for QRIS, typically no additional fees)
- long tax = 0; // QRIS usually doesn't have tax
- long serviceFeeValue = 0; // QRIS usually doesn't have service fee
- long total = amountLong + tax + serviceFeeValue;
+ // ✅ CALCULATE FEES BASED ON TRANSACTION TYPE
+ long tax = 0;
+ long serviceFeeValue = 0;
+ long total = amountLong;
+
+ if (isEmvTransaction) {
+ // For EMV transactions, check if gross amount includes additional fees
+ if (grossAmount != null && !grossAmount.equals(amount)) {
+ try {
+ long grossAmountLong = Long.parseLong(cleanAmountString(grossAmount));
+ long difference = grossAmountLong - amountLong;
+
+ if (difference > 0) {
+ // Assume 11% tax and 500 service fee (adjust based on your business logic)
+ tax = Math.round(amountLong * 0.11);
+ serviceFeeValue = 500;
+ total = grossAmountLong;
+
+ Log.d("ReceiptActivity", "EMV: Calculated tax=" + tax + ", service=" + serviceFeeValue + ", total=" + total);
+ }
+ } catch (Exception e) {
+ Log.w("ReceiptActivity", "Could not parse gross amount for EMV: " + grossAmount);
+ }
+ }
+ } else if (isQrisTransaction) {
+ // For QRIS, typically no additional fees (tax=0, service=0)
+ tax = 0;
+ serviceFeeValue = 0;
+ total = amountLong;
+ Log.d("ReceiptActivity", "QRIS: No additional fees - total=" + total);
+ }
// Set calculated values
- taxPercentage.setText("Rp 0");
- serviceFee.setText("Rp 0");
- finalTotal.setText("Rp " + formatCurrency(total));
+ taxPercentage.setText(tax > 0 ? "11%" : "0%");
+ serviceFee.setText(formatCurrency(serviceFeeValue));
+ finalTotal.setText(formatCurrency(total));
- Log.d("ReceiptActivity", "Amount formatting successful: " + amountLong + " -> Rp " + formatCurrency(total));
+ Log.d("ReceiptActivity", "Enhanced amount formatting successful: " + amountLong + " -> " + formatCurrency(total));
} catch (NumberFormatException e) {
- Log.e("ReceiptActivity", "Error parsing amount: " + amountToUse, e);
+ Log.e("ReceiptActivity", "Error parsing enhanced amount: " + amountToUse, e);
// Fallback if parsing fails
- transactionTotal.setText("Rp " + amountToUse);
- taxPercentage.setText("Rp 0");
- serviceFee.setText("Rp 0");
- finalTotal.setText("Rp " + amountToUse);
+ transactionTotal.setText(amountToUse);
+ taxPercentage.setText("0%");
+ serviceFee.setText("0");
+ finalTotal.setText(amountToUse);
}
} else {
// Default values if no amount provided
- transactionTotal.setText("Rp 0");
- taxPercentage.setText("Rp 0");
- serviceFee.setText("Rp 0");
- finalTotal.setText("Rp 0");
+ transactionTotal.setText("0");
+ taxPercentage.setText("0%");
+ serviceFee.setText("0");
+ finalTotal.setText("0");
}
}
@@ -874,7 +1103,7 @@ public class ReceiptActivity extends AppCompatActivity {
}
private String getCurrentDateTime() {
- SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm", new Locale("id", "ID"));
+ SimpleDateFormat sdf = new SimpleDateFormat("dd MMMM yyyy HH:mm", new Locale("id", "ID"));
return sdf.format(new Date());
}
@@ -902,9 +1131,9 @@ public class ReceiptActivity extends AppCompatActivity {
if (callingActivity != null) {
switch (callingActivity) {
- case "TransactionActivity":
+ case "ReprintActivity":
// Go back to transaction list
- Intent transactionIntent = new Intent(this, TransactionActivity.class);
+ Intent transactionIntent = new Intent(this, ReprintActivity.class);
transactionIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(transactionIntent);
break;
@@ -914,6 +1143,11 @@ public class ReceiptActivity extends AppCompatActivity {
navigateToHomePage();
break;
+ case "ResultTransactionActivity":
+ // ✅ NEW: Handle back from ResultTransactionActivity
+ navigateToHomePage();
+ break;
+
case "PaymentActivity":
case "QrisActivity":
// Go back to payment/qris activity
diff --git a/app/src/main/java/com/example/bdkipoc/TransactionActivity.java b/app/src/main/java/com/example/bdkipoc/cetakulang/ReprintActivity.java
similarity index 90%
rename from app/src/main/java/com/example/bdkipoc/TransactionActivity.java
rename to app/src/main/java/com/example/bdkipoc/cetakulang/ReprintActivity.java
index 5d0e01b..2030876 100644
--- a/app/src/main/java/com/example/bdkipoc/TransactionActivity.java
+++ b/app/src/main/java/com/example/bdkipoc/cetakulang/ReprintActivity.java
@@ -1,5 +1,6 @@
-package com.example.bdkipoc;
+package com.example.bdkipoc.cetakulang;
+import com.example.bdkipoc.R;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -50,9 +51,12 @@ import java.util.TimeZone;
import android.app.DatePickerDialog;
import android.widget.DatePicker;
-public class TransactionActivity extends AppCompatActivity implements TransactionAdapter.OnPrintClickListener {
+import com.example.bdkipoc.ReceiptActivity;
+import com.example.bdkipoc.StyleHelper;
+
+public class ReprintActivity extends AppCompatActivity implements ReprintAdapterActivity.OnPrintClickListener {
private RecyclerView recyclerView;
- private TransactionAdapter adapter;
+ private ReprintAdapterActivity adapter;
private List transactionList;
private List filteredList;
private ProgressBar progressBar;
@@ -89,7 +93,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_transaction);
+ setContentView(R.layout.activity_reprint);
// ✅ Initialize SharedPreferences for local tracking
prefs = getSharedPreferences("transaction_prefs", MODE_PRIVATE);
@@ -159,7 +163,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
transactionList = new ArrayList<>();
filteredList = new ArrayList<>();
- adapter = new TransactionAdapter(filteredList);
+ adapter = new ReprintAdapterActivity(filteredList);
adapter.setPrintClickListener(this);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
@@ -342,13 +346,13 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
filterButtonText.setTextColor(getResources().getColor(android.R.color.holo_blue_dark));
filterButtonText.setTextSize(12); // Smaller text when filter is active
- Log.d("TransactionActivity", "🎨 Filter button updated: " + displayText);
+ Log.d("ReprintActivity", "🎨 Filter button updated: " + displayText);
}
}
// ✅ NEW METHOD: Apply date filter
private void applyDateFilter() {
- Log.d("TransactionActivity", "🗓️ Applying date filter: " + fromDate + " to " + toDate);
+ Log.d("ReprintActivity", "🗓️ Applying date filter: " + fromDate + " to " + toDate);
// Reset to first page and reload data
currentPage = 1;
@@ -366,7 +370,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
filterButtonText.setTextSize(14); // Reset to normal size
}
- Log.d("TransactionActivity", "🗓️ Date filter cleared");
+ Log.d("ReprintActivity", "🗓️ Date filter cleared");
// Reload data without date filter
currentPage = 1;
@@ -377,7 +381,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
return;
}
- Log.d("TransactionActivity", "🔄 Navigating to page " + page);
+ Log.d("ReprintActivity", "🔄 Navigating to page " + page);
if (currentSearchQuery.isEmpty()) {
// Load from API
@@ -410,7 +414,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
// Scroll to top
recyclerView.scrollToPosition(0);
- Log.d("TransactionActivity", "📄 Displaying search results page " + currentPage +
+ Log.d("ReprintActivity", "📄 Displaying search results page " + currentPage +
" (items " + (startIndex + 1) + "-" + endIndex + " of " + filteredList.size() + ")");
}
@@ -452,10 +456,10 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
totalPages = (int) Math.ceil((double) totalRecords / itemsPerPage);
// ✅ PASTIKAN TIDAK PERLU SORT LAGI karena sudah sorted dari API response
- Log.d("TransactionActivity", "📋 FILTERED LIST ORDER (no search - maintaining API order):");
+ Log.d("ReprintActivity", "📋 FILTERED LIST ORDER (no search - maintaining API order):");
for (int i = 0; i < Math.min(5, filteredList.size()); i++) {
Transaction tx = filteredList.get(i);
- Log.d("TransactionActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
+ Log.d("ReprintActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
}
} else {
// ✅ SEARCH MODE: Filter all available data
@@ -518,7 +522,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
paginationControls.setVisibility(View.GONE);
}
- Log.d("TransactionActivity", "📊 Pagination updated: " +
+ Log.d("ReprintActivity", "📊 Pagination updated: " +
"Page " + currentPage + "/" + totalPages + ", Total: " + totalRecords);
}
@@ -584,7 +588,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
pageNumbersContainer.addView(pageButton);
}
- Log.d("TransactionActivity", "🔢 Page buttons created: " + startPage + " to " + endPage +
+ Log.d("ReprintActivity", "🔢 Page buttons created: " + startPage + " to " + endPage +
" with size: " + buttonSize + "px");
}
@@ -613,7 +617,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
String urlString = "https://be-edc.msvc.app/transactions?page=" + apiPage +
"&limit=" + itemsPerPage + "&sortOrder=DESC&from_date=" + fromDate + "&to_date=" + toDate + "&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=created_at";
- Log.d("TransactionActivity", "🔍 Fetching transactions page " + pageToLoad +
+ Log.d("ReprintActivity", "🔍 Fetching transactions page " + pageToLoad +
" (API page " + apiPage + ") with limit " + itemsPerPage + " - SORT: DESC by created_at" +
" - Date Filter: " + fromDate + " to " + toDate);
@@ -642,7 +646,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
apiTotal = results.getInt("total");
JSONArray data = results.getJSONArray("data");
- Log.d("TransactionActivity", "📊 API response: " + data.length() +
+ Log.d("ReprintActivity", "📊 API response: " + data.length() +
" records, total: " + apiTotal);
// ✅ STEP 1: Parse all transactions from API
@@ -667,14 +671,14 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
// ✅ STEP 2: Apply intelligent deduplication
result = applyAdvancedDeduplication(rawTransactions);
- Log.d("TransactionActivity", "✅ After advanced deduplication: " + result.size() + " unique transactions");
+ Log.d("ReprintActivity", "✅ After advanced deduplication: " + result.size() + " unique transactions");
} else {
- Log.e("TransactionActivity", "❌ HTTP Error: " + responseCode);
+ Log.e("ReprintActivity", "❌ HTTP Error: " + responseCode);
error = true;
}
} catch (IOException | JSONException | URISyntaxException e) {
- Log.e("TransactionActivity", "❌ Exception: " + e.getMessage(), e);
+ Log.e("ReprintActivity", "❌ Exception: " + e.getMessage(), e);
error = true;
}
return result;
@@ -687,7 +691,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
progressBar.setVisibility(View.GONE);
if (error) {
- Toast.makeText(TransactionActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
+ Toast.makeText(ReprintActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
updatePaginationDisplay();
return;
}
@@ -705,11 +709,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
if (date1 != null && date2 != null) {
int comparison = date2.compareTo(date1); // Newest first
- Log.d("TransactionActivity", "🔄 Sorting: " + t2.createdAt + " vs " + t1.createdAt + " = " + comparison);
+ Log.d("ReprintActivity", "🔄 Sorting: " + t2.createdAt + " vs " + t1.createdAt + " = " + comparison);
return comparison;
}
} catch (Exception e) {
- Log.w("TransactionActivity", "Date comparison error: " + e.getMessage());
+ Log.w("ReprintActivity", "Date comparison error: " + e.getMessage());
}
return Integer.compare(t2.id, t1.id); // Fallback by ID (higher ID = newer)
});
@@ -718,14 +722,14 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
transactionList.clear();
transactionList.addAll(transactions);
- Log.d("TransactionActivity", "📋 Page " + currentPage + " loaded and sorted: " +
+ Log.d("ReprintActivity", "📋 Page " + currentPage + " loaded and sorted: " +
transactions.size() + " transactions. Total: " + totalRecords + "/" + totalPages + " pages");
// ✅ VERIFIKASI SORTING ORDER
- Log.d("TransactionActivity", "📋 SORTED ORDER VERIFICATION:");
+ Log.d("ReprintActivity", "📋 SORTED ORDER VERIFICATION:");
for (int i = 0; i < Math.min(5, transactionList.size()); i++) {
Transaction tx = transactionList.get(i);
- Log.d("TransactionActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
+ Log.d("ReprintActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
}
// Update filtered list based on current search
@@ -761,7 +765,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // Handle timezone properly
Date parsed = sdf.parse(rawDate);
- Log.d("TransactionActivity", "✅ Date parsed successfully: " + rawDate + " -> " + parsed + " using format: " + format);
+ Log.d("ReprintActivity", "✅ Date parsed successfully: " + rawDate + " -> " + parsed + " using format: " + format);
return parsed;
} catch (Exception e) {
// Continue to next format
@@ -779,10 +783,10 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
Date parsed = sdf.parse(cleanedDate);
- Log.d("TransactionActivity", "✅ Date parsed with fallback: " + rawDate + " -> " + parsed);
+ Log.d("ReprintActivity", "✅ Date parsed with fallback: " + rawDate + " -> " + parsed);
return parsed;
} catch (Exception e) {
- Log.w("TransactionActivity", "❌ Could not parse date: " + rawDate + " - Error: " + e.getMessage());
+ Log.w("ReprintActivity", "❌ Could not parse date: " + rawDate + " - Error: " + e.getMessage());
return null;
}
}
@@ -791,11 +795,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
* ✅ ADVANCED DEDUPLICATION: Enhanced algorithm with multiple strategies
*/
private List applyAdvancedDeduplication(List rawTransactions) {
- Log.d("TransactionActivity", "🧠 Starting advanced deduplication...");
- Log.d("TransactionActivity", "📥 Input transactions order (first 5):");
+ Log.d("ReprintActivity", "🧠 Starting advanced deduplication...");
+ Log.d("ReprintActivity", "📥 Input transactions order (first 5):");
for (int i = 0; i < Math.min(5, rawTransactions.size()); i++) {
Transaction tx = rawTransactions.get(i);
- Log.d("TransactionActivity", " " + (i+1) + ". ID:" + tx.id + " Date:" + tx.createdAt + " Ref:" + tx.referenceId);
+ Log.d("ReprintActivity", " " + (i+1) + ". ID:" + tx.id + " Date:" + tx.createdAt + " Ref:" + tx.referenceId);
}
// Strategy 1: Group by reference_id
@@ -823,7 +827,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
if (group.size() == 1) {
// No duplicates for this reference
deduplicatedList.add(group.get(0));
- Log.d("TransactionActivity", "✅ Unique transaction: " + referenceId);
+ Log.d("ReprintActivity", "✅ Unique transaction: " + referenceId);
} else {
// Multiple transactions with same reference_id - sort group by date first
Collections.sort(group, (t1, t2) -> {
@@ -843,15 +847,15 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
deduplicatedList.add(bestTransaction);
duplicatesRemoved += (group.size() - 1);
- Log.d("TransactionActivity", "🔄 Deduplicated " + group.size() + " → 1 for ref: " + referenceId +
+ Log.d("ReprintActivity", "🔄 Deduplicated " + group.size() + " → 1 for ref: " + referenceId +
" (kept ID: " + bestTransaction.id + ", status: " + bestTransaction.status + ", date: " + bestTransaction.createdAt + ")");
}
}
- Log.d("TransactionActivity", "✅ Advanced deduplication complete:");
- Log.d("TransactionActivity", " 📥 Input: " + rawTransactions.size() + " transactions");
- Log.d("TransactionActivity", " 📤 Output: " + deduplicatedList.size() + " unique transactions");
- Log.d("TransactionActivity", " 🗑️ Removed: " + duplicatesRemoved + " duplicates");
+ Log.d("ReprintActivity", "✅ Advanced deduplication complete:");
+ Log.d("ReprintActivity", " 📥 Input: " + rawTransactions.size() + " transactions");
+ Log.d("ReprintActivity", " 📤 Output: " + deduplicatedList.size() + " unique transactions");
+ Log.d("ReprintActivity", " 🗑️ Removed: " + duplicatesRemoved + " duplicates");
return deduplicatedList;
}
@@ -860,7 +864,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
* ✅ ENHANCED SELECTION: Advanced algorithm to pick the best transaction
*/
private Transaction selectBestTransactionAdvanced(List duplicates, String referenceId) {
- Log.d("TransactionActivity", "🎯 Selecting best from " + duplicates.size() + " duplicates for: " + referenceId);
+ Log.d("ReprintActivity", "🎯 Selecting best from " + duplicates.size() + " duplicates for: " + referenceId);
Transaction bestTransaction = duplicates.get(0);
int bestPriority = getStatusPriority(bestTransaction.status);
@@ -871,7 +875,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
int currentPriority = getStatusPriority(tx.status);
Date currentDate = parseCreatedAtDate(tx.createdAt);
- Log.d("TransactionActivity", " 📊 Candidate: ID=" + tx.id +
+ Log.d("ReprintActivity", " 📊 Candidate: ID=" + tx.id +
", Status=" + tx.status + " (priority=" + currentPriority + ")" +
", Created=" + tx.createdAt);
@@ -903,11 +907,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
bestTransaction = tx;
bestPriority = currentPriority;
bestDate = currentDate;
- Log.d("TransactionActivity", " ⭐ NEW BEST selected: " + reason);
+ Log.d("ReprintActivity", " ⭐ NEW BEST selected: " + reason);
}
}
- Log.d("TransactionActivity", "🏆 FINAL SELECTION: ID=" + bestTransaction.id +
+ Log.d("ReprintActivity", "🏆 FINAL SELECTION: ID=" + bestTransaction.id +
", Status=" + bestTransaction.status + ", Created=" + bestTransaction.createdAt);
return bestTransaction;
@@ -925,7 +929,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
return date1.after(date2);
}
} catch (Exception e) {
- Log.w("TransactionActivity", "Date comparison error, falling back to ID comparison");
+ Log.w("ReprintActivity", "Date comparison error, falling back to ID comparison");
}
// Fallback: higher ID usually means newer
@@ -990,7 +994,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
// Tier 5: Unknown status
default:
- Log.w("TransactionActivity", "Unknown status encountered: " + status);
+ Log.w("ReprintActivity", "Unknown status encountered: " + status);
return 1;
}
}
@@ -1002,12 +1006,12 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
Intent intent = new Intent(this, ReceiptActivity.class);
// Add calling activity information for proper back navigation
- intent.putExtra("calling_activity", "TransactionActivity");
+ intent.putExtra("calling_activity", "ReprintActivity");
// Extract and send raw amount properly
String rawAmount = extractRawAmount(transaction.amount);
- Log.d("TransactionActivity", "Opening receipt for transaction: " + transaction.referenceId +
+ Log.d("ReprintActivity", "Opening receipt for transaction: " + transaction.referenceId +
", channel: " + transaction.channelCode + ", original amount: '" + transaction.amount + "'");
// Send transaction data to ReceiptActivity
@@ -1032,7 +1036,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
String acquirer = getRealAcquirerForQris(transaction.referenceId, transaction.channelCode);
intent.putExtra("acquirer", acquirer); // Jenis Kartu
- Log.d("TransactionActivity", "🎯 Determined acquirer: " + acquirer + " for channel: " + transaction.channelCode);
+ Log.d("ReprintActivity", "🎯 Determined acquirer: " + acquirer + " for channel: " + transaction.channelCode);
startActivity(intent);
}
@@ -1084,7 +1088,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
// For QRIS, we could implement real-time acquirer lookup here
// For now, return "qris" and let ReceiptActivity handle the detection
- Log.d("TransactionActivity", "🔍 QRIS transaction detected, deferring acquirer detection to ReceiptActivity");
+ Log.d("ReprintActivity", "🔍 QRIS transaction detected, deferring acquirer detection to ReceiptActivity");
return "qris";
}
@@ -1128,7 +1132,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
Long.parseLong(cleaned);
return cleaned;
} catch (NumberFormatException e) {
- Log.e("TransactionActivity", "Invalid amount: " + formattedAmount);
+ Log.e("ReprintActivity", "Invalid amount: " + formattedAmount);
return "0";
}
}
diff --git a/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java b/app/src/main/java/com/example/bdkipoc/cetakulang/ReprintAdapterActivity.java
similarity index 80%
rename from app/src/main/java/com/example/bdkipoc/TransactionAdapter.java
rename to app/src/main/java/com/example/bdkipoc/cetakulang/ReprintAdapterActivity.java
index ae80699..205af2d 100644
--- a/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java
+++ b/app/src/main/java/com/example/bdkipoc/cetakulang/ReprintAdapterActivity.java
@@ -1,5 +1,6 @@
-package com.example.bdkipoc;
+package com.example.bdkipoc.cetakulang;
+import com.example.bdkipoc.R;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -26,15 +27,17 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-public class TransactionAdapter extends RecyclerView.Adapter {
- private List transactionList;
+import com.example.bdkipoc.StyleHelper;
+
+public class ReprintAdapterActivity extends RecyclerView.Adapter {
+ private List transactionList;
private OnPrintClickListener printClickListener;
public interface OnPrintClickListener {
- void onPrintClick(TransactionActivity.Transaction transaction);
+ void onPrintClick(ReprintActivity.Transaction transaction);
}
- public TransactionAdapter(List transactionList) {
+ public ReprintAdapterActivity(List transactionList) {
this.transactionList = transactionList;
}
@@ -45,23 +48,23 @@ public class TransactionAdapter extends RecyclerView.Adapter newData, int startIndex) {
+ public void updateData(List newData, int startIndex) {
this.transactionList = newData;
notifyDataSetChanged();
- Log.d("TransactionAdapter", "📋 Data updated: " + newData.size() + " items");
+ Log.d("ReprintAdapterActivity", "📋 Data updated: " + newData.size() + " items");
}
@NonNull
@Override
public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false);
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_reprint, parent, false);
return new TransactionViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
- TransactionActivity.Transaction t = transactionList.get(position);
+ ReprintActivity.Transaction t = transactionList.get(position);
// ✅ STRIPE TABLE: Set alternating row colors
LinearLayout itemContainer = holder.itemView.findViewById(R.id.itemContainer);
@@ -73,10 +76,10 @@ public class TransactionAdapter extends RecyclerView.Adapter '" + formattedAmount + "'");
+ Log.d("ReprintAdapterActivity", "💰 Amount processed: '" + t.amount + "' -> '" + formattedAmount + "'");
} catch (NumberFormatException e) {
- Log.e("TransactionAdapter", "❌ Amount format error: " + t.amount, e);
+ Log.e("ReprintAdapterActivity", "❌ Amount format error: " + t.amount, e);
String fallback = t.amount.startsWith("Rp") ? t.amount : "Rp " + t.amount;
holder.amount.setText(fallback);
}
@@ -99,7 +102,7 @@ public class TransactionAdapter extends RecyclerView.Adapter " + formattedDate);
+ Log.d("ReprintAdapterActivity", "📅 Created at: " + t.createdAt + " -> " + formattedDate);
// Set click listeners
holder.itemView.setOnClickListener(v -> {
@@ -148,7 +151,7 @@ public class TransactionAdapter extends RecyclerView.Adapter {
try {
- Log.d("TransactionAdapter", "🔍 Comprehensive status check for reference: " + referenceId);
+ Log.d("ReprintAdapterActivity", "🔍 Comprehensive status check for reference: " + referenceId);
// STEP 1: Query webhook logs untuk semua order_id yang terkait
String queryUrl = "https://be-edc.msvc.app/api-logs?limit=200&sortOrder=DESC&sortColumn=created_at";
@@ -244,7 +247,7 @@ public class TransactionAdapter extends RecyclerView.Adapter 0) {
- Log.d("TransactionAdapter", "📊 Processing " + results.length() + " log entries");
+ Log.d("ReprintAdapterActivity", "📊 Processing " + results.length() + " log entries");
// STEP 2: Comprehensive search dengan multiple matching strategies
for (int i = 0; i < results.length(); i++) {
@@ -270,7 +273,7 @@ public class TransactionAdapter extends RecyclerView.Adapter " + logTransactionStatus);
+ Log.d("ReprintAdapterActivity", "✅ PAYMENT CONFIRMED: " + logOrderId + " -> " + logTransactionStatus);
break; // Found paid status, stop searching
} else if (logTransactionStatus.equals("pending") && finalStatus.equals("INIT")) {
finalStatus = "PENDING";
foundOrderId = logOrderId;
foundAcquirer = logAcquirer;
- Log.d("TransactionAdapter", "⏳ PENDING found: " + logOrderId);
+ Log.d("ReprintAdapterActivity", "⏳ PENDING found: " + logOrderId);
} else if (logTransactionStatus.equals("expire") || logTransactionStatus.equals("cancel")) {
if (finalStatus.equals("INIT")) { // Only update if no better status found
finalStatus = "FAILED";
foundOrderId = logOrderId;
foundAcquirer = logAcquirer;
- Log.d("TransactionAdapter", "❌ FAILED status: " + logOrderId + " -> " + logTransactionStatus);
+ Log.d("ReprintAdapterActivity", "❌ FAILED status: " + logOrderId + " -> " + logTransactionStatus);
}
}
}
}
}
- Log.d("TransactionAdapter", "🔍 FINAL RESULT for " + referenceId + ":");
- Log.d("TransactionAdapter", " Status: " + finalStatus);
- Log.d("TransactionAdapter", " Order ID: " + (foundOrderId != null ? foundOrderId : "N/A"));
- Log.d("TransactionAdapter", " Acquirer: " + (foundAcquirer != null ? foundAcquirer : "N/A"));
+ Log.d("ReprintAdapterActivity", "🔍 FINAL RESULT for " + referenceId + ":");
+ Log.d("ReprintAdapterActivity", " Status: " + finalStatus);
+ Log.d("ReprintAdapterActivity", " Order ID: " + (foundOrderId != null ? foundOrderId : "N/A"));
+ Log.d("ReprintAdapterActivity", " Acquirer: " + (foundAcquirer != null ? foundAcquirer : "N/A"));
}
// STEP 3: Update UI di main thread
@@ -348,10 +351,10 @@ public class TransactionAdapter extends RecyclerView.Adapter {
statusTextView.setText("ERROR");
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "ERROR");
@@ -368,7 +371,7 @@ public class TransactionAdapter extends RecyclerView.Adapter {
statusTextView.setText("INIT");
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "INIT");
@@ -383,7 +386,7 @@ public class TransactionAdapter extends RecyclerView.Adapter {
try {
- Log.d("TransactionAdapter", "🔄 Updating backend status for reference: " + referenceId);
+ Log.d("ReprintAdapterActivity", "🔄 Updating backend status for reference: " + referenceId);
JSONObject updatePayload = new JSONObject();
updatePayload.put("status", status);
@@ -414,16 +417,16 @@ public class TransactionAdapter extends RecyclerView.Adapter " + formatted);
+ Log.d("ReprintAdapterActivity", "📅 Date formatted: " + rawDate + " -> " + formatted);
return formatted;
}
} catch (Exception e) {
- Log.e("TransactionAdapter", "❌ Date formatting error for: " + rawDate, e);
+ Log.e("ReprintAdapterActivity", "❌ Date formatting error for: " + rawDate, e);
}
// Fallback: Manual parsing
@@ -485,7 +488,7 @@ public class TransactionAdapter extends RecyclerView.Adapter
+ * This request is guaranteed to be called after {@link #onStart(String)}.
+ *
+ * @param utteranceId The utterance ID of the utterance.
+ */
+ void onDone(String utteranceId);
+
+ /**
+ * Called when an error has occurred during processing. This can be called
+ * at any point in the synthesis process. Note that there might be calls
+ * to {@link #onStart(String)} for specified utteranceId but there will never
+ * be a call to both {@link #onDone(String)} and {@link #onError(String)} for
+ * the same utterance.
+ *
+ * @param utteranceId The utterance ID of the utterance.
+ * @deprecated Use {@link #onError(String, int)} instead
+ */
+
+ /**
+ * @deprecated Use {@link #onError(String, int)} instead
+ */
+ @Deprecated
+ void onError(String utteranceId);
+
+ /**
+ * Called when an utterance has been stopped while in progress or flushed from the
+ * synthesis queue. This can happen if a client calls {@link TextToSpeech#stop()}
+ * or uses {@link TextToSpeech#QUEUE_FLUSH} as an argument with the
+ * {@link TextToSpeech#speak} or {@link TextToSpeech#synthesizeToFile} methods.
+ *
+ * @param utteranceId The utterance ID of the utterance.
+ * @param interrupted If true, then the utterance was interrupted while being synthesized
+ * and its output is incomplete. If false, then the utterance was flushed
+ * before the synthesis started.
+ */
+ void onStop(String utteranceId, boolean interrupted);
+}
diff --git a/app/src/main/java/com/example/bdkipoc/HistoryActivity.java b/app/src/main/java/com/example/bdkipoc/histori/HistoryActivity.java
similarity index 100%
rename from app/src/main/java/com/example/bdkipoc/HistoryActivity.java
rename to app/src/main/java/com/example/bdkipoc/histori/HistoryActivity.java
diff --git a/app/src/main/java/com/example/bdkipoc/HistoryDetailActivity.java b/app/src/main/java/com/example/bdkipoc/histori/HistoryDetailActivity.java
similarity index 100%
rename from app/src/main/java/com/example/bdkipoc/HistoryDetailActivity.java
rename to app/src/main/java/com/example/bdkipoc/histori/HistoryDetailActivity.java
diff --git a/app/src/main/java/com/example/bdkipoc/QrisActivity.java b/app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java
similarity index 99%
rename from app/src/main/java/com/example/bdkipoc/QrisActivity.java
rename to app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java
index 62c6fee..0f37f41 100644
--- a/app/src/main/java/com/example/bdkipoc/QrisActivity.java
+++ b/app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java
@@ -70,7 +70,7 @@ public class QrisActivity extends AppCompatActivity {
private static final String BACKEND_BASE = "https://be-edc.msvc.app";
private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
- private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM="; // Replace with your actual key
+ private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc=";
private static final String WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
@Override
diff --git a/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java b/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java
new file mode 100644
index 0000000..2d46142
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/qris/QrisResultActivity.java
@@ -0,0 +1,1448 @@
+package com.example.bdkipoc;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.AsyncTask;
+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 org.json.JSONArray;
+import org.json.JSONException;
+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.text.NumberFormat;
+import java.util.Locale;
+import java.util.HashMap;
+import java.util.Map;
+
+public class QrisResultActivity extends AppCompatActivity {
+ // Main UI Components
+ private ImageView qrImageView;
+ private TextView amountTextView;
+ private TextView timerTextView;
+ private Button cancelButton;
+ private TextView qrisLogo;
+ private CardView mainCard;
+ private View headerBackground;
+ private View backNavigation;
+
+ // Hidden components for functionality
+ private TextView referenceTextView;
+ private TextView statusTextView;
+ private TextView qrStatusTextView;
+ private ProgressBar progressBar;
+ private Button downloadQrisButton;
+ private Button checkStatusButton;
+ private Button returnMainButton;
+
+ // QR Refresh Components
+ private Handler qrRefreshHandler;
+ private Runnable qrRefreshRunnable;
+ private int countdownSeconds = 60;
+ private boolean isQrRefreshActive = true;
+
+ // Success screen views
+ private View successScreen;
+ private ImageView successIcon;
+ private TextView successMessage;
+
+ private String orderId;
+ private String grossAmount;
+ private String referenceId;
+ private String transactionId;
+ private String transactionTime;
+ private String acquirer;
+ private String merchantId;
+ private String currentQrImageUrl;
+ private int originalAmount;
+
+ // ✅ QR String untuk validasi QRIS
+ private String currentQrString = "";
+ private String qrStringFromMidtrans = "";
+
+ // ✅ Store actual issuer/acquirer from Midtrans response
+ private String actualIssuerFromMidtrans = "";
+ private String actualAcquirerFromMidtrans = "";
+
+ // ✅ Track QR refresh transaction for payment monitoring
+ private String currentQrTransactionId = "";
+ private boolean isMonitoringQrRefreshTransaction = false;
+
+ private String backendBase = "https://be-edc.msvc.app";
+ private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans";
+
+ // Server key for signature generation
+ private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc=";
+ private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
+
+ // ✅ Mapping dari technical issuer ke display name
+ private static final Map ISSUER_DISPLAY_MAP = new HashMap() {{
+ 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");
+ }};
+
+ // Animation handler
+ private Handler animationHandler = new Handler(Looper.getMainLooper());
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // ✅ TETAP MENGGUNAKAN LAYOUT ASLI UNTUK QR DISPLAY
+ setContentView(R.layout.activity_qris_result);
+
+ // Initialize views
+ initializeViews();
+
+ // Get intent data
+ getIntentData();
+
+ // ✅ Initialize parent transaction tracking
+ currentQrTransactionId = transactionId; // Initially monitor parent transaction
+ isMonitoringQrRefreshTransaction = false;
+ Log.d("QrisResultFlow", "🆔 Initial monitoring - parent transaction ID: " + transactionId);
+ Log.d("QrisResultFlow", "🆔 Parent order ID: " + orderId);
+
+ // Setup UI
+ setupUI();
+
+ // Setup success screen
+ setupSuccessScreen();
+
+ // Start QR refresh timer
+ startQrRefreshTimer();
+
+ // Start polling for pending payment log
+ pollPendingPaymentLog(orderId);
+
+ // Set up click listeners
+ setupClickListeners();
+
+ // Start continuous payment monitoring
+ startContinuousPaymentMonitoring();
+ }
+
+ private void initializeViews() {
+ // Main visible components
+ qrImageView = findViewById(R.id.qrImageView);
+ amountTextView = findViewById(R.id.amountTextView);
+ timerTextView = findViewById(R.id.timerTextView);
+ cancelButton = findViewById(R.id.cancel_button);
+ qrisLogo = findViewById(R.id.qris_logo);
+ mainCard = findViewById(R.id.main_card);
+ headerBackground = findViewById(R.id.header_background);
+ backNavigation = findViewById(R.id.back_navigation);
+
+ // Hidden components for functionality
+ referenceTextView = findViewById(R.id.referenceTextView);
+ statusTextView = findViewById(R.id.statusTextView);
+ qrStatusTextView = findViewById(R.id.qrStatusTextView);
+ progressBar = findViewById(R.id.progressBar);
+ downloadQrisButton = findViewById(R.id.downloadQrisButton);
+ checkStatusButton = findViewById(R.id.checkStatusButton);
+ 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);
+
+ // Initialize handler for QR refresh
+ qrRefreshHandler = new Handler(Looper.getMainLooper());
+ }
+
+ private void setupSuccessScreen() {
+ // Initially hide success screen
+ if (successScreen != null) {
+ successScreen.setVisibility(View.GONE);
+ }
+ }
+
+ private void getIntentData() {
+ Intent intent = getIntent();
+ currentQrImageUrl = intent.getStringExtra("qrImageUrl");
+ originalAmount = intent.getIntExtra("amount", 0);
+ referenceId = intent.getStringExtra("referenceId");
+ orderId = intent.getStringExtra("orderId");
+ grossAmount = intent.getStringExtra("grossAmount");
+ transactionId = intent.getStringExtra("transactionId");
+ transactionTime = intent.getStringExtra("transactionTime");
+ acquirer = intent.getStringExtra("acquirer");
+ merchantId = intent.getStringExtra("merchantId");
+
+ // ✅ GET QR STRING from intent if available
+ qrStringFromMidtrans = intent.getStringExtra("qrString");
+ if (qrStringFromMidtrans != null) {
+ currentQrString = qrStringFromMidtrans;
+ }
+
+ // Enhanced logging for debugging
+ Log.d("QrisResultFlow", "=== QRIS RESULT ACTIVITY STARTED ===");
+ Log.d("QrisResultFlow", "QR Image URL: " + currentQrImageUrl);
+ Log.d("QrisResultFlow", "QR String: " + (currentQrString.length() > 50 ? currentQrString.substring(0, 50) + "..." : currentQrString));
+ Log.d("QrisResultFlow", "Amount (int): " + originalAmount);
+ Log.d("QrisResultFlow", "Gross Amount (string): " + grossAmount);
+ Log.d("QrisResultFlow", "Reference ID: " + referenceId);
+ Log.d("QrisResultFlow", "Order ID: " + orderId);
+ Log.d("QrisResultFlow", "Transaction ID: " + transactionId);
+ Log.d("QrisResultFlow", "======================================");
+ }
+
+ private void setupUI() {
+ // Validate required data
+ if (orderId == null || transactionId == null) {
+ Log.e("QrisResultFlow", "Critical error: orderId or transactionId is null!");
+ Toast.makeText(this, "Missing transaction details! Cannot proceed.", Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ // Display formatted amount in rupiah format
+ String formattedAmount = formatRupiahAmount(grossAmount != null ? grossAmount : String.valueOf(originalAmount));
+ amountTextView.setText(formattedAmount);
+
+ // Set reference data to hidden field
+ if (referenceTextView != null) {
+ referenceTextView.setText("Reference ID: " + referenceId);
+ }
+
+ // Load initial QR image
+ loadQrImage(currentQrImageUrl);
+
+ // Initialize timer display
+ timerTextView.setText("60");
+
+ // Set initial status in hidden field
+ if (statusTextView != null) {
+ statusTextView.setText("Waiting for payment...");
+ }
+
+ // Enable simulate payment functionality
+ if (checkStatusButton != null) {
+ checkStatusButton.setEnabled(false);
+ }
+
+ // ✅ VALIDATE QR STRING
+ validateQrString(currentQrString);
+ }
+
+ private String formatRupiahAmount(String amount) {
+ try {
+ // Remove any existing currency symbols and formatting
+ String cleanAmount = amount.replaceAll("[^0-9]", "");
+ long amountLong = Long.parseLong(cleanAmount);
+
+ // Format with dots as thousand separators
+ return "RP." + String.format("%,d", amountLong).replace(',', '.');
+ } catch (NumberFormatException e) {
+ Log.w("QrisResultFlow", "Error formatting rupiah amount: " + e.getMessage());
+ return "RP." + amount;
+ }
+ }
+
+ // ✅ NEW: Validate QR String format
+ private void validateQrString(String qrString) {
+ if (qrString == null || qrString.isEmpty()) {
+ Log.w("QrisResultFlow", "⚠️ QR String is empty - QR might be unparsable");
+ return;
+ }
+
+ Log.d("QrisResultFlow", "🔍 Validating QR String...");
+ Log.d("QrisResultFlow", "QR String length: " + qrString.length());
+ Log.d("QrisResultFlow", "QR String preview: " + (qrString.length() > 100 ? qrString.substring(0, 100) + "..." : qrString));
+
+ // ✅ BASIC QRIS FORMAT VALIDATION
+ if (qrString.startsWith("00020101") || qrString.startsWith("00020102")) {
+ Log.d("QrisResultFlow", "✅ QR String has valid QRIS header");
+ } else {
+ Log.w("QrisResultFlow", "⚠️ QR String might not be valid QRIS format - missing standard header");
+ }
+
+ // ✅ CHECK FOR REQUIRED FIELDS
+ if (qrString.contains("ID.CO.QRIS.WWW")) {
+ Log.d("QrisResultFlow", "✅ QR String contains QRIS Indonesia identifier");
+ } else {
+ Log.w("QrisResultFlow", "⚠️ QR String missing Indonesia QRIS identifier");
+ }
+
+ // ✅ CHECK FOR AMOUNT
+ if (qrString.contains("54")) { // Field 54 is transaction amount
+ Log.d("QrisResultFlow", "✅ QR String contains amount field");
+ } else {
+ Log.w("QrisResultFlow", "⚠️ QR String missing amount field");
+ }
+ }
+
+ private void startQrRefreshTimer() {
+ countdownSeconds = 60;
+ isQrRefreshActive = true;
+
+ qrRefreshRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (!isQrRefreshActive) {
+ return;
+ }
+
+ if (countdownSeconds > 0) {
+ // Update countdown display
+ timerTextView.setText(String.valueOf(countdownSeconds));
+ countdownSeconds--;
+
+ // Schedule next update in 1 second
+ qrRefreshHandler.postDelayed(this, 1000);
+ } else {
+ // ✅ Time to refresh QR code - CREATE NEW QR TRANSACTION
+ Log.d("QrisResultFlow", "🔄 QR Code refresh time reached - creating new QR transaction");
+ refreshQrCode();
+ }
+ }
+ };
+
+ qrRefreshHandler.post(qrRefreshRunnable);
+ Log.d("QrisResultFlow", "🕒 QR refresh timer started - 60 seconds countdown");
+ }
+
+ private void refreshQrCode() {
+ if (!isQrRefreshActive) {
+ return;
+ }
+
+ Log.d("QrisResultFlow", "🔄 Starting QR code refresh...");
+
+ // Show loading state
+ timerTextView.setText("...");
+
+ // Generate new QR code in background
+ new Thread(() -> {
+ try {
+ QrRefreshResult result = generateNewQrCode();
+
+ runOnUiThread(() -> {
+ if (result != null && result.qrUrl != null && !result.qrUrl.isEmpty()) {
+ // ✅ Successfully refreshed QR
+ currentQrImageUrl = result.qrUrl;
+ currentQrString = result.qrString; // ✅ UPDATE QR STRING
+ loadQrImage(result.qrUrl);
+
+ // ✅ VALIDATE NEW QR STRING
+ validateQrString(currentQrString);
+
+ // ✅ IMPORTANT: Now monitoring QR refresh transaction for payment
+ Log.d("QrisResultFlow", "🔄 QR refreshed - now monitoring new QR transaction");
+ Log.d("QrisResultFlow", "🔄 Parent transaction ID: " + transactionId + " (reference only)");
+ Log.d("QrisResultFlow", "🔄 Monitoring QR transaction ID: " + currentQrTransactionId);
+
+ // Restart timer
+ countdownSeconds = 60;
+ Log.d("QrisResultFlow", "✅ QR code refreshed successfully");
+ Toast.makeText(QrisResultActivity.this, "QR Code refreshed with valid format", Toast.LENGTH_SHORT).show();
+ } else {
+ // Failed to generate new QR
+ Log.e("QrisResultFlow", "❌ Failed to refresh QR code");
+ countdownSeconds = 30;
+ Toast.makeText(QrisResultActivity.this, "QR refresh failed, retrying...", Toast.LENGTH_SHORT).show();
+ }
+
+ // Continue timer
+ qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000);
+ });
+
+ } catch (Exception e) {
+ Log.e("QrisResultFlow", "❌ QR refresh error: " + e.getMessage(), e);
+
+ runOnUiThread(() -> {
+ countdownSeconds = 30;
+ qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000);
+ Toast.makeText(QrisResultActivity.this, "QR refresh error", Toast.LENGTH_SHORT).show();
+ });
+ }
+ }).start();
+ }
+
+ // ✅ NEW: QR Refresh Result class
+ private static class QrRefreshResult {
+ String qrUrl;
+ String qrString;
+ String transactionId;
+
+ QrRefreshResult(String qrUrl, String qrString, String transactionId) {
+ this.qrUrl = qrUrl;
+ this.qrString = qrString;
+ this.transactionId = transactionId;
+ }
+ }
+
+ // ✅ ENHANCED: Return both QR URL and QR String
+ private QrRefreshResult generateNewQrCode() {
+ try {
+ Log.d("QrisResultFlow", "🔧 Refreshing QR code for existing transaction");
+ Log.d("QrisResultFlow", "🔄 Parent Transaction ID: " + transactionId);
+ Log.d("QrisResultFlow", "🔄 Parent Order ID: " + orderId);
+
+ // ✅ GENERATE SHORT ORDER ID to avoid 50 character limit
+ String shortTimestamp = String.valueOf(System.currentTimeMillis()).substring(7);
+ String newOrderId = orderId.substring(0, Math.min(orderId.length(), 43)) + "-q" + shortTimestamp;
+
+ Log.d("QrisResultFlow", "🆕 New QR Order ID: " + newOrderId + " (Length: " + newOrderId.length() + ")");
+
+ // ✅ VALIDATE ORDER ID LENGTH
+ if (newOrderId.length() > 50) {
+ newOrderId = orderId.substring(0, 36) + "-q" + shortTimestamp.substring(0, Math.min(shortTimestamp.length(), 7));
+ Log.w("QrisResultFlow", "⚠️ Order ID too long, using fallback: " + newOrderId + " (Length: " + newOrderId.length() + ")");
+ }
+
+ // ✅ CREATE LINK TO PARENT TRANSACTION
+ JSONObject customField1 = new JSONObject();
+ customField1.put("parent_transaction_id", transactionId);
+ customField1.put("parent_order_id", orderId);
+ customField1.put("parent_reference_id", referenceId);
+ customField1.put("qr_refresh_time", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date()));
+ customField1.put("qr_refresh_count", System.currentTimeMillis());
+ customField1.put("is_qr_refresh", true);
+
+ // ✅ CREATE QRIS PAYLOAD WITH NEW ORDER ID
+ JSONObject payload = new JSONObject();
+ payload.put("payment_type", "qris");
+
+ JSONObject transactionDetails = new JSONObject();
+ transactionDetails.put("order_id", newOrderId);
+ transactionDetails.put("gross_amount", originalAmount);
+ 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", originalAmount);
+ item.put("quantity", 1);
+ item.put("name", "QRIS Payment QR Refresh - " + new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date()) +
+ " (Parent Ref: " + referenceId + ")");
+ 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", transactionId);
+ qrisDetails.put("refresh_timestamp", System.currentTimeMillis());
+ payload.put("qris", qrisDetails);
+
+ Log.d("QrisResultFlow", "📤 QR Refresh payload: " + payload.toString());
+
+ // ✅ MAKE API CALL TO MIDTRANS
+ 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");
+ conn.setRequestProperty("X-QR-Refresh", "true");
+ conn.setRequestProperty("X-Parent-Transaction", transactionId);
+ 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("QrisResultFlow", "📥 QR refresh response code: " + responseCode);
+
+ if (responseCode == 200 || responseCode == 201) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ StringBuilder response = new StringBuilder();
+ String responseLine;
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ // ✅ LOG FULL RESPONSE FOR DEBUGGING
+ Log.d("QrisResultFlow", "📋 Full QR Refresh Response: " + response.toString());
+
+ JSONObject jsonResponse = new JSONObject(response.toString());
+
+ if (jsonResponse.has("status_code")) {
+ String statusCode = jsonResponse.getString("status_code");
+ if (!statusCode.equals("201")) {
+ String statusMessage = jsonResponse.optString("status_message", "Unknown error");
+ Log.e("QrisResultFlow", "❌ QR refresh failed with status: " + statusCode + " - " + statusMessage);
+ return null;
+ }
+ }
+
+ // ✅ EXTRACT QR URL AND QR STRING
+ String newQrUrl = null;
+ String newQrString = null;
+ String newTransactionId = jsonResponse.optString("transaction_id", "");
+
+ // Get QR URL from actions
+ if (jsonResponse.has("actions")) {
+ JSONArray actionsArray = jsonResponse.getJSONArray("actions");
+ if (actionsArray.length() > 0) {
+ JSONObject actions = actionsArray.getJSONObject(0);
+ newQrUrl = actions.getString("url");
+ }
+ }
+
+ // ✅ GET QR STRING (CRITICAL FOR QRIS VALIDATION)
+ if (jsonResponse.has("qr_string")) {
+ newQrString = jsonResponse.getString("qr_string");
+ Log.d("QrisResultFlow", "✅ Found QR String in response: " + (newQrString.length() > 50 ? newQrString.substring(0, 50) + "..." : newQrString));
+ } else {
+ Log.w("QrisResultFlow", "⚠️ No QR String found in response - QR might be unparsable");
+ }
+
+ if (!newTransactionId.isEmpty()) {
+ currentQrTransactionId = newTransactionId;
+ isMonitoringQrRefreshTransaction = true;
+ Log.d("QrisResultFlow", "🔄 Now monitoring QR refresh transaction: " + newTransactionId);
+ }
+
+ Log.d("QrisResultFlow", "✅ QR refresh successful!");
+ Log.d("QrisResultFlow", "🆕 New QR URL: " + newQrUrl);
+ Log.d("QrisResultFlow", "🆕 New QR String Length: " + (newQrString != null ? newQrString.length() : "null"));
+ Log.d("QrisResultFlow", "🆕 New QR Transaction ID: " + newTransactionId);
+
+ return new QrRefreshResult(newQrUrl, newQrString, newTransactionId);
+
+ } else {
+ InputStream errorStream = conn.getErrorStream();
+ String errorResponse = "";
+
+ if (errorStream != null) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(errorStream, "utf-8"));
+ StringBuilder errorBuilder = new StringBuilder();
+ String responseLine;
+ while ((responseLine = br.readLine()) != null) {
+ errorBuilder.append(responseLine.trim());
+ }
+ errorResponse = errorBuilder.toString();
+ }
+
+ Log.e("QrisResultFlow", "❌ QR refresh HTTP error " + responseCode + ": " + errorResponse);
+ return null;
+ }
+
+ } catch (Exception e) {
+ Log.e("QrisResultFlow", "❌ QR refresh exception: " + e.getMessage(), e);
+ return null;
+ }
+ }
+
+ // ✅ ENHANCED: Load QR image with better error handling
+ private void loadQrImage(String qrImageUrl) {
+ if (qrImageUrl != null && !qrImageUrl.isEmpty()) {
+ Log.d("QrisResultFlow", "🖼️ Loading QR image from: " + qrImageUrl);
+
+ // ✅ VALIDATE URL FORMAT
+ if (!qrImageUrl.startsWith("http")) {
+ Log.e("QrisResultFlow", "❌ Invalid QR URL format: " + qrImageUrl);
+ qrImageView.setVisibility(View.GONE);
+ Toast.makeText(this, "Invalid QR code URL format", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ new EnhancedDownloadImageTask(qrImageView).execute(qrImageUrl);
+ } else {
+ Log.w("QrisResultFlow", "⚠️ QR image URL is not available");
+ qrImageView.setVisibility(View.GONE);
+ Toast.makeText(this, "QR code URL not available", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void stopQrRefresh() {
+ isQrRefreshActive = false;
+ if (qrRefreshHandler != null && qrRefreshRunnable != null) {
+ qrRefreshHandler.removeCallbacks(qrRefreshRunnable);
+ }
+
+ Log.d("QrisResultFlow", "🛑 QR refresh timer stopped");
+ }
+
+ private void setupClickListeners() {
+ // Cancel button
+ cancelButton.setOnClickListener(v -> {
+ addClickAnimation(v);
+ stopQrRefresh();
+ finish();
+ });
+
+ // Back navigation
+ if (backNavigation != null) {
+ backNavigation.setOnClickListener(v -> {
+ addClickAnimation(v);
+ stopQrRefresh();
+ finish();
+ });
+ }
+
+ // Hidden check status button for testing
+ if (checkStatusButton != null) {
+ checkStatusButton.setOnClickListener(v -> {
+ Log.d("QrisResultFlow", "Check status button clicked");
+ stopQrRefresh();
+ simulateWebhook();
+ });
+ }
+
+ // Hidden return main button
+ if (returnMainButton != null) {
+ returnMainButton.setOnClickListener(v -> {
+ stopQrRefresh();
+ Intent intent = new Intent(QrisResultActivity.this, com.example.bdkipoc.MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finishAffinity();
+ });
+ }
+
+ // Enable testing via double tap on QR logo
+ qrisLogo.setOnClickListener(new View.OnClickListener() {
+ private int clickCount = 0;
+ private Handler handler = new Handler();
+ private final int DOUBLE_TAP_TIMEOUT = 300;
+
+ @Override
+ public void onClick(View v) {
+ clickCount++;
+ if (clickCount == 1) {
+ handler.postDelayed(() -> clickCount = 0, DOUBLE_TAP_TIMEOUT);
+ } else if (clickCount == 2) {
+ // Double tap detected - simulate payment
+ clickCount = 0;
+ Log.d("QrisResultFlow", "Double tap detected - simulating payment");
+ stopQrRefresh();
+ simulateWebhook();
+ }
+ }
+ });
+ }
+
+ // Animation methods
+ private void addClickAnimation(View view) {
+ ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f, 1f);
+ ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f, 1f);
+
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(scaleX, scaleY);
+ animatorSet.setDuration(150);
+ animatorSet.start();
+ }
+
+ // ✅ ENHANCED: Download Image Task with better validation
+ private static class EnhancedDownloadImageTask extends AsyncTask {
+ private ImageView bmImage;
+ private String errorMessage;
+
+ EnhancedDownloadImageTask(ImageView bmImage) {
+ this.bmImage = bmImage;
+ }
+
+ @Override
+ protected Bitmap doInBackground(String... urls) {
+ String urlDisplay = urls[0];
+ Bitmap bitmap = null;
+
+ try {
+ // ✅ VALIDATE URL
+ if (urlDisplay == null || urlDisplay.isEmpty()) {
+ Log.e("QrisResultFlow", "❌ Empty QR URL provided");
+ errorMessage = "QR URL is empty";
+ return null;
+ }
+
+ if (!urlDisplay.startsWith("http")) {
+ Log.e("QrisResultFlow", "❌ Invalid QR URL format: " + urlDisplay);
+ errorMessage = "Invalid QR URL format";
+ return null;
+ }
+
+ Log.d("QrisResultFlow", "📥 Downloading image from: " + urlDisplay);
+
+ URL url = new URI(urlDisplay).toURL();
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setDoInput(true);
+ connection.setConnectTimeout(15000); // Increase timeout
+ connection.setReadTimeout(15000);
+ connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
+ connection.setRequestProperty("Accept", "image/*");
+
+ // ✅ ADD AUTHORIZATION if needed for Midtrans QR
+ if (urlDisplay.contains("midtrans.com")) {
+ connection.setRequestProperty("Authorization", MIDTRANS_AUTH);
+ }
+
+ connection.connect();
+
+ int responseCode = connection.getResponseCode();
+ Log.d("QrisResultFlow", "📥 Image download response code: " + responseCode);
+
+ if (responseCode == 200) {
+ InputStream input = connection.getInputStream();
+ bitmap = BitmapFactory.decodeStream(input);
+
+ if (bitmap != null) {
+ Log.d("QrisResultFlow", "✅ Image downloaded successfully. Size: " +
+ bitmap.getWidth() + "x" + bitmap.getHeight());
+ } else {
+ Log.e("QrisResultFlow", "❌ Failed to decode bitmap from stream");
+ errorMessage = "Failed to decode QR code image";
+ }
+ } else {
+ Log.e("QrisResultFlow", "❌ Failed to download image. HTTP code: " + responseCode);
+
+ // ✅ READ ERROR RESPONSE
+ InputStream errorStream = connection.getErrorStream();
+ if (errorStream != null) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(errorStream));
+ StringBuilder errorResponse = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ errorResponse.append(line);
+ }
+ Log.e("QrisResultFlow", "❌ Error response: " + errorResponse.toString());
+ }
+
+ errorMessage = "Failed to download QR code (HTTP " + responseCode + ")";
+ }
+
+ } catch (Exception e) {
+ Log.e("QrisResultFlow", "❌ Exception downloading image: " + e.getMessage(), e);
+ errorMessage = "Error downloading QR code: " + e.getMessage();
+ }
+
+ return bitmap;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ if (result != null) {
+ bmImage.setImageBitmap(result);
+ Log.d("QrisResultFlow", "✅ QR code image displayed successfully");
+ } else {
+ Log.e("QrisResultFlow", "❌ Failed to display QR code image");
+ bmImage.setImageResource(android.R.drawable.ic_menu_report_image);
+ if (errorMessage != null && bmImage.getContext() != null) {
+ Toast.makeText(bmImage.getContext(), "QR Error: " + errorMessage, Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+ }
+
+ // ✅ HELPER: Convert technical issuer name ke display name
+ private String getDisplayName(String technicalName) {
+ if (technicalName == null || technicalName.isEmpty()) {
+ return "QRIS";
+ }
+
+ String lowerTechnicalName = technicalName.toLowerCase().trim();
+ String displayName = ISSUER_DISPLAY_MAP.get(lowerTechnicalName);
+
+ if (displayName != null) {
+ Log.d("QrisResultFlow", "🏷️ Mapped '" + technicalName + "' -> '" + displayName + "'");
+ return displayName;
+ }
+
+ // Fallback: capitalize first letter of each word
+ String[] words = technicalName.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(" ");
+ }
+ }
+ String fallbackName = result.toString().trim();
+ Log.d("QrisResultFlow", "🏷️ No mapping found for '" + technicalName + "', using fallback: '" + fallbackName + "'");
+ return fallbackName;
+ }
+
+ // ✅ ENHANCED: Use actual issuer from Midtrans response
+ private void syncTransactionStatusToBackend(String finalStatus) {
+ Log.d("QrisResultFlow", "🔄 Syncing status '" + finalStatus + "' to backend for reference: " + referenceId);
+
+ new Thread(() -> {
+ try {
+ // ✅ GET FINAL ISSUER AND ACQUIRER VALUES
+ String finalIssuer = actualIssuerFromMidtrans;
+ String finalAcquirer = actualAcquirerFromMidtrans;
+
+ // ✅ FALLBACK IF NOT SET
+ if (finalIssuer.isEmpty()) {
+ finalIssuer = acquirer != null ? acquirer : "qris";
+ }
+ if (finalAcquirer.isEmpty()) {
+ finalAcquirer = acquirer != null ? acquirer : "gopay";
+ }
+
+ // ✅ USE MONITORING TRANSACTION FOR WEBHOOK (could be parent or QR refresh)
+ String webhookTransactionId = !currentQrTransactionId.isEmpty() ? currentQrTransactionId : transactionId;
+ String transactionType = isMonitoringQrRefreshTransaction ? "QR refresh transaction" : "parent transaction";
+
+ Log.d("QrisResultFlow", "🏷️ Final webhook values:");
+ Log.d("QrisResultFlow", " Transaction ID: '" + webhookTransactionId + "' (" + transactionType + ")");
+ Log.d("QrisResultFlow", " Order ID: '" + orderId + "'");
+ Log.d("QrisResultFlow", " Issuer: '" + finalIssuer + "'");
+ Log.d("QrisResultFlow", " Acquirer: '" + finalAcquirer + "'");
+
+ // ✅ FORMAT WEBHOOK MIDTRANS STANDARD
+ JSONObject payload = new JSONObject();
+ payload.put("status_code", "200");
+ payload.put("status_message", "Success, transaction is found");
+ payload.put("transaction_id", webhookTransactionId);
+ payload.put("order_id", orderId);
+ payload.put("merchant_id", merchantId != null ? merchantId : "G616299250");
+ payload.put("gross_amount", grossAmount != null ? grossAmount : String.valueOf(originalAmount));
+ payload.put("currency", "IDR");
+ payload.put("payment_type", "qris");
+ payload.put("transaction_time", transactionTime != null ? transactionTime : getCurrentDateTime());
+ payload.put("transaction_status", finalStatus.equals("PAID") ? "settlement" : finalStatus.toLowerCase());
+ payload.put("fraud_status", "accept");
+ payload.put("acquirer", finalAcquirer);
+ payload.put("issuer", finalIssuer);
+ payload.put("settlement_time", getCurrentISOTime());
+ payload.put("reference_id", referenceId);
+ payload.put("shopeepay_reference_number", "");
+
+ // ✅ ADD QR STRING if available
+ if (!currentQrString.isEmpty()) {
+ payload.put("qr_string", currentQrString);
+ Log.d("QrisResultFlow", "📋 Added QR String to webhook payload");
+ }
+
+ // ✅ SIGNATURE untuk validasi
+ String serverKey = getServerKey();
+ String signature = generateSignature(orderId, "200",
+ grossAmount != null ? grossAmount : String.valueOf(originalAmount), serverKey);
+ payload.put("signature_key", signature);
+
+ Log.d("QrisResultFlow", "📤 Webhook payload: " + payload.toString());
+
+ // ✅ KIRIM KE WEBHOOK ENDPOINT
+ String webhookUrl = backendBase + "/webhooks/midtrans";
+ URL url = new URI(webhookUrl).toURL();
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setRequestProperty("Accept", "application/json");
+ conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(15000);
+ conn.setReadTimeout(15000);
+
+ try (OutputStream os = conn.getOutputStream()) {
+ byte[] input = payload.toString().getBytes("utf-8");
+ os.write(input, 0, input.length);
+ }
+
+ int responseCode = conn.getResponseCode();
+ Log.d("QrisResultFlow", "📥 Webhook response: " + responseCode);
+
+ if (responseCode == 200 || responseCode == 201) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ response.append(line);
+ }
+ Log.d("QrisResultFlow", "✅ Webhook successful: " + response.toString());
+ } else {
+ InputStream errorStream = conn.getErrorStream();
+ if (errorStream != null) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(errorStream));
+ StringBuilder errorResponse = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ errorResponse.append(line);
+ }
+ Log.e("QrisResultFlow", "❌ Webhook failed: " + responseCode + " - " + errorResponse.toString());
+ }
+ }
+
+ } catch (Exception e) {
+ Log.e("QrisResultFlow", "❌ Webhook error: " + e.getMessage(), e);
+ }
+ }).start();
+ }
+
+ private String getCurrentISOTime() {
+ return new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
+ .format(new java.util.Date());
+ }
+
+ // ✅ MODIFIED: Show full screen success instead of simple text display
+ private void showPaymentSuccess() {
+ Log.d("QrisResultFlow", "Showing payment success screen");
+
+ stopQrRefresh();
+
+ // ✅ SYNC STATUS KE BACKEND
+ syncTransactionStatusToBackend("PAID");
+
+ // Show full screen success
+ showSuccessScreen();
+
+ // Navigate to receipt after delay
+ animationHandler.postDelayed(() -> {
+ launchReceiptActivity();
+ }, 2500);
+
+ Toast.makeText(this, "Payment simulation completed successfully!", Toast.LENGTH_LONG).show();
+ }
+
+ // ✅ NEW: Show full screen success overlay with animations
+ private void showSuccessScreen() {
+ if (successScreen != null) {
+ // Hide all main UI components first
+ hideMainUIComponents();
+
+ // Set success message
+ if (successMessage != null) {
+ successMessage.setText("Pembayaran Berhasil");
+ }
+
+ // Show success screen with fade in animation
+ successScreen.setVisibility(View.VISIBLE);
+ successScreen.setAlpha(0f);
+
+ // Fade in the background
+ ObjectAnimator backgroundFadeIn = ObjectAnimator.ofFloat(successScreen, "alpha", 0f, 1f);
+ backgroundFadeIn.setDuration(500);
+ backgroundFadeIn.start();
+
+ // Add scale and bounce animation to success icon
+ if (successIcon != null) {
+ // Start with invisible icon
+ successIcon.setScaleX(0f);
+ successIcon.setScaleY(0f);
+ successIcon.setAlpha(0f);
+
+ // Scale animation with bounce effect
+ ObjectAnimator scaleX = ObjectAnimator.ofFloat(successIcon, "scaleX", 0f, 1.2f, 1f);
+ ObjectAnimator scaleY = ObjectAnimator.ofFloat(successIcon, "scaleY", 0f, 1.2f, 1f);
+ ObjectAnimator iconFadeIn = ObjectAnimator.ofFloat(successIcon, "alpha", 0f, 1f);
+
+ AnimatorSet iconAnimation = new AnimatorSet();
+ iconAnimation.playTogether(scaleX, scaleY, iconFadeIn);
+ iconAnimation.setDuration(800);
+ iconAnimation.setStartDelay(300);
+ iconAnimation.setInterpolator(new android.view.animation.OvershootInterpolator(1.2f));
+ iconAnimation.start();
+ }
+
+ // Add slide up animation to success message
+ if (successMessage != null) {
+ successMessage.setAlpha(0f);
+ successMessage.setTranslationY(50f);
+
+ ObjectAnimator messageSlideUp = ObjectAnimator.ofFloat(successMessage, "translationY", 50f, 0f);
+ ObjectAnimator messageFadeIn = ObjectAnimator.ofFloat(successMessage, "alpha", 0f, 1f);
+
+ AnimatorSet messageAnimation = new AnimatorSet();
+ messageAnimation.playTogether(messageSlideUp, messageFadeIn);
+ messageAnimation.setDuration(600);
+ messageAnimation.setStartDelay(600);
+ messageAnimation.setInterpolator(new android.view.animation.DecelerateInterpolator());
+ messageAnimation.start();
+ }
+ }
+ }
+
+ // ✅ NEW: Hide main UI components for clean success screen
+ private void hideMainUIComponents() {
+ // Hide main content
+ if (mainCard != null) {
+ mainCard.setVisibility(View.GONE);
+ }
+
+ // Hide header elements
+ if (headerBackground != null) {
+ headerBackground.setVisibility(View.GONE);
+ }
+ if (backNavigation != null) {
+ backNavigation.setVisibility(View.GONE);
+ }
+
+ // Hide cancel button
+ if (cancelButton != null) {
+ cancelButton.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ stopQrRefresh();
+ if (animationHandler != null) {
+ animationHandler.removeCallbacksAndMessages(null);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ // Prevent back press when success screen is showing
+ if (successScreen != null && successScreen.getVisibility() == View.VISIBLE) {
+ return;
+ }
+
+ stopQrRefresh();
+ finish();
+ super.onBackPressed();
+ }
+
+ private void pollPendingPaymentLog(final String orderId) {
+ Log.d("QrisResultFlow", "Starting polling for orderId: " + orderId);
+
+ new Thread(() -> {
+ int maxAttempts = 12;
+ int intervalMs = 2000;
+ int attempt = 0;
+ boolean found = false;
+
+ while (attempt < maxAttempts && !found) {
+ try {
+ String currentOrderId = this.orderId;
+ String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + currentOrderId + "\"}";
+ Log.d("QrisResultFlow", "Polling attempt " + (attempt + 1) + "/" + maxAttempts + " for parent order: " + currentOrderId);
+
+ URL url = new URL(urlStr);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.setRequestProperty("Accept", "application/json");
+ conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
+ conn.setConnectTimeout(5000);
+ conn.setReadTimeout(5000);
+
+ int responseCode = conn.getResponseCode();
+
+ if (responseCode == 200) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ response.append(line);
+ }
+
+ JSONObject json = new JSONObject(response.toString());
+ JSONArray results = json.optJSONArray("results");
+
+ if (results != null && results.length() > 0) {
+ Log.d("QrisResultFlow", "Found " + results.length() + " log entries");
+
+ for (int i = 0; i < results.length(); i++) {
+ JSONObject log = results.getJSONObject(i);
+ JSONObject reqBody = log.optJSONObject("request_body");
+
+ if (reqBody != null) {
+ String transactionStatus = reqBody.optString("transaction_status");
+ String logOrderId = reqBody.optString("order_id");
+
+ Log.d("QrisResultFlow", "Log entry " + i + ": order_id=" + logOrderId +
+ ", transaction_status=" + transactionStatus);
+
+ if (currentOrderId.equals(logOrderId) &&
+ (transactionStatus.equals("pending") ||
+ transactionStatus.equals("settlement") ||
+ transactionStatus.equals("capture") ||
+ transactionStatus.equals("success"))) {
+ found = true;
+
+ if (transactionStatus.equals("settlement") ||
+ transactionStatus.equals("capture") ||
+ transactionStatus.equals("success")) {
+
+ Log.d("QrisResultFlow", "🎉 Payment already completed with status: " + transactionStatus);
+
+ new Handler(Looper.getMainLooper()).post(() -> {
+ stopQrRefresh();
+ showPaymentSuccess();
+ Toast.makeText(QrisResultActivity.this, "Payment completed!", Toast.LENGTH_LONG).show();
+ });
+ return;
+ }
+
+ Log.d("QrisResultFlow", "Found matching payment log with status: " + transactionStatus);
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ Log.w("QrisResultFlow", "Polling failed with HTTP code: " + responseCode);
+ }
+ } catch (Exception e) {
+ Log.e("QrisResultFlow", "Polling error on attempt " + (attempt + 1) + ": " + e.getMessage());
+ }
+
+ if (!found) {
+ attempt++;
+ if (attempt < maxAttempts) {
+ try {
+ Thread.sleep(intervalMs);
+ } catch (InterruptedException ignored) {
+ break;
+ }
+ }
+ }
+ }
+
+ final boolean logFound = found;
+ new Handler(Looper.getMainLooper()).post(() -> {
+ if (logFound && checkStatusButton != null) {
+ checkStatusButton.setEnabled(true);
+ Toast.makeText(QrisResultActivity.this, "Payment log found!", Toast.LENGTH_SHORT).show();
+ } else if (checkStatusButton != null) {
+ Toast.makeText(QrisResultActivity.this, "Payment log not found. Manual simulation available.", Toast.LENGTH_LONG).show();
+ checkStatusButton.setEnabled(true);
+ }
+ });
+ }).start();
+ }
+
+ private String getServerKey() {
+ try {
+ String base64 = MIDTRANS_AUTH.replace("Basic ", "");
+ byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT);
+ String decodedString = new String(decoded);
+ return decodedString.replace(":", "");
+ } catch (Exception e) {
+ Log.e("QrisResultFlow", "Error decoding server key: " + e.getMessage());
+ return "";
+ }
+ }
+
+ private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
+ String input = orderId + statusCode + grossAmount + serverKey;
+ try {
+ java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-512");
+ byte[] messageDigest = md.digest(input.getBytes());
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : messageDigest) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) hexString.append('0');
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ } catch (java.security.NoSuchAlgorithmException e) {
+ Log.e("QrisResultFlow", "Error generating signature: " + e.getMessage());
+ return "dummy_signature";
+ }
+ }
+
+ private void startContinuousPaymentMonitoring() {
+ Log.d("QrisResultFlow", "🔍 Starting continuous payment monitoring");
+
+ Handler paymentMonitorHandler = new Handler(Looper.getMainLooper());
+
+ Runnable paymentMonitorRunnable = new Runnable() {
+ @Override
+ public void run() {
+ checkCurrentPaymentStatus();
+
+ if (!isFinishing() && isQrRefreshActive) {
+ paymentMonitorHandler.postDelayed(this, 5000);
+ }
+ }
+ };
+
+ paymentMonitorHandler.post(paymentMonitorRunnable);
+ }
+
+ private void checkCurrentPaymentStatus() {
+ new Thread(() -> {
+ try {
+ // ✅ CRITICAL: Monitor current QR transaction (could be parent or QR refresh)
+ String monitoringTransactionId = !currentQrTransactionId.isEmpty() ? currentQrTransactionId : transactionId;
+ String statusUrl = "https://api.sandbox.midtrans.com/v2/" + monitoringTransactionId + "/status";
+
+ String transactionType = isMonitoringQrRefreshTransaction ? "QR refresh transaction" : "parent transaction";
+ Log.d("QrisResultFlow", "🔍 Checking payment status for " + transactionType + ": " + 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(10000);
+ conn.setReadTimeout(10000);
+
+ int responseCode = conn.getResponseCode();
+
+ if (responseCode == 200) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ response.append(line);
+ }
+
+ JSONObject statusResponse = new JSONObject(response.toString());
+ String transactionStatus = statusResponse.optString("transaction_status", "");
+ String paymentType = statusResponse.optString("payment_type", "");
+ String grossAmount = statusResponse.optString("gross_amount", "");
+
+ // ✅ PROPERLY EXTRACT AND STORE ACTUAL ISSUER/ACQUIRER
+ String actualIssuer = statusResponse.optString("issuer", "");
+ String actualAcquirer = statusResponse.optString("acquirer", "");
+
+ // ✅ UPDATE QR STRING if available in status response
+ String qrStringFromStatus = statusResponse.optString("qr_string", "");
+ if (!qrStringFromStatus.isEmpty()) {
+ currentQrString = qrStringFromStatus;
+ Log.d("QrisResultFlow", "🔄 Updated QR String from status check");
+ }
+
+ Log.d("QrisResultFlow", "💳 Payment status check result (" + transactionType + "):");
+ Log.d("QrisResultFlow", " Status: " + transactionStatus);
+ Log.d("QrisResultFlow", " Payment Type: " + paymentType);
+ Log.d("QrisResultFlow", " Amount: " + grossAmount);
+ Log.d("QrisResultFlow", " Actual Issuer: " + actualIssuer);
+ Log.d("QrisResultFlow", " Actual Acquirer: " + actualAcquirer);
+ Log.d("QrisResultFlow", " QR String Available: " + !qrStringFromStatus.isEmpty());
+
+ // ✅ CRITICAL FIX: Store the actual values from Midtrans
+ if (!actualIssuer.isEmpty() && !actualIssuer.equalsIgnoreCase("qris")) {
+ actualIssuerFromMidtrans = actualIssuer;
+ Log.d("QrisResultFlow", "✅ Updated issuer from Midtrans: " + actualIssuer);
+ }
+
+ if (!actualAcquirer.isEmpty() && !actualAcquirer.equalsIgnoreCase("qris")) {
+ actualAcquirerFromMidtrans = actualAcquirer;
+ Log.d("QrisResultFlow", "✅ Updated acquirer from Midtrans: " + actualAcquirer);
+ }
+
+ // Update backward compatibility variable
+ if (!actualIssuer.isEmpty()) {
+ acquirer = actualIssuer;
+ }
+
+ if (transactionStatus.equals("settlement") ||
+ transactionStatus.equals("capture") ||
+ transactionStatus.equals("success")) {
+
+ Log.d("QrisResultFlow", "🎉 Payment detected as PAID! Status: " + transactionStatus + " (" + transactionType + ")");
+
+ runOnUiThread(() -> {
+ stopQrRefresh();
+ syncTransactionStatusToBackend("PAID");
+ showPaymentSuccess();
+ Toast.makeText(QrisResultActivity.this, "Payment Successful! 🎉", Toast.LENGTH_LONG).show();
+ });
+
+ } else if (transactionStatus.equals("pending")) {
+ Log.d("QrisResultFlow", "⏳ Payment still pending (" + transactionType + ")");
+ } else if (transactionStatus.equals("expire") || transactionStatus.equals("cancel")) {
+ Log.w("QrisResultFlow", "⚠️ Payment expired or cancelled: " + transactionStatus + " (" + transactionType + ")");
+ syncTransactionStatusToBackend("FAILED");
+ } else {
+ Log.d("QrisResultFlow", "📊 Payment status: " + transactionStatus + " (" + transactionType + ")");
+ }
+
+ } else {
+ Log.w("QrisResultFlow", "⚠️ Payment status check failed: HTTP " + responseCode + " (" + transactionType + ")");
+ }
+
+ } catch (Exception e) {
+ Log.e("QrisResultFlow", "❌ Payment status check error: " + e.getMessage(), e);
+ }
+ }).start();
+ }
+
+ private void simulateWebhook() {
+ Log.d("QrisResultFlow", "🚀 Starting webhook simulation");
+
+ stopQrRefresh();
+
+ new Thread(() -> {
+ try {
+ String serverKey = getServerKey();
+
+ // ✅ USE MONITORING TRANSACTION FOR SIMULATION
+ String currentOrderId = this.orderId;
+ String currentTransactionId = !this.currentQrTransactionId.isEmpty() ? this.currentQrTransactionId : this.transactionId;
+ String currentGrossAmount = this.grossAmount;
+ String transactionType = isMonitoringQrRefreshTransaction ? "QR refresh transaction" : "parent transaction";
+
+ Log.d("QrisResultFlow", "🚀 Simulating webhook for " + transactionType + ": " + currentTransactionId);
+
+ String signatureKey = generateSignature(currentOrderId, "200", currentGrossAmount, serverKey);
+
+ // ✅ GET ACTUAL ISSUER AND ACQUIRER
+ String finalIssuer = actualIssuerFromMidtrans.isEmpty() ?
+ (acquirer != null ? acquirer : "qris") : actualIssuerFromMidtrans;
+ String finalAcquirer = actualAcquirerFromMidtrans.isEmpty() ?
+ (acquirer != null ? acquirer : "gopay") : actualAcquirerFromMidtrans;
+
+ JSONObject payload = new JSONObject();
+ payload.put("transaction_type", "on-us");
+ payload.put("transaction_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
+ payload.put("transaction_status", "settlement");
+ payload.put("transaction_id", currentTransactionId);
+ payload.put("status_message", "midtrans payment notification");
+ payload.put("status_code", "200");
+ payload.put("signature_key", signatureKey);
+ payload.put("settlement_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
+ payload.put("payment_type", "qris");
+ payload.put("order_id", currentOrderId);
+ payload.put("merchant_id", merchantId != null ? merchantId : "G616299250");
+ payload.put("issuer", finalIssuer);
+ payload.put("gross_amount", currentGrossAmount);
+ payload.put("fraud_status", "accept");
+ payload.put("currency", "IDR");
+ payload.put("acquirer", finalAcquirer);
+ payload.put("shopeepay_reference_number", "");
+ payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID");
+
+ // ✅ ADD QR STRING to webhook if available
+ if (!currentQrString.isEmpty()) {
+ payload.put("qr_string", currentQrString);
+ Log.d("QrisResultFlow", "📋 Added QR String to webhook simulation");
+ }
+
+ URL url = new URL(webhookUrl);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(15000);
+ conn.setReadTimeout(15000);
+
+ OutputStream os = conn.getOutputStream();
+ os.write(payload.toString().getBytes());
+ os.flush();
+ os.close();
+
+ int responseCode = conn.getResponseCode();
+ Log.d("QrisResultFlow", "📥 Webhook response code: " + responseCode);
+
+ BufferedReader br = new BufferedReader(new InputStreamReader(
+ responseCode < 400 ? conn.getInputStream() : conn.getErrorStream()));
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = br.readLine()) != null) {
+ response.append(line);
+ }
+
+ Log.d("QrisResultFlow", "📥 Webhook response: " +
+ (response.length() > 200 ? response.substring(0, 200) + "..." : response.toString()));
+
+ Thread.sleep(2000);
+
+ } catch (Exception e) {
+ Log.e("QrisResultFlow", "❌ Webhook simulation error: " + e.getMessage(), e);
+ }
+
+ new Handler(Looper.getMainLooper()).post(() -> {
+ showPaymentSuccess();
+ });
+ }).start();
+ }
+
+ private void launchReceiptActivity() {
+ Intent intent = new Intent(this, ReceiptActivity.class);
+
+ intent.putExtra("calling_activity", "QrisResultActivity");
+
+ // ✅ GET FINAL ISSUER FOR RECEIPT
+ String finalIssuer = actualIssuerFromMidtrans.isEmpty() ?
+ (acquirer != null ? acquirer : "qris") : actualIssuerFromMidtrans;
+
+ String displayCardType = getDisplayName(finalIssuer);
+
+ Log.d("QrisResultFlow", "Launching receipt with data:");
+ Log.d("QrisResultFlow", " Reference ID: " + referenceId);
+ Log.d("QrisResultFlow", " Transaction ID: " + transactionId);
+ Log.d("QrisResultFlow", " Amount: " + originalAmount);
+ Log.d("QrisResultFlow", " Actual Issuer: " + finalIssuer);
+ Log.d("QrisResultFlow", " Display Card Type: " + displayCardType);
+ Log.d("QrisResultFlow", " QR String Available: " + !currentQrString.isEmpty());
+
+ intent.putExtra("transaction_id", transactionId);
+ intent.putExtra("reference_id", referenceId);
+ intent.putExtra("order_id", orderId);
+ intent.putExtra("transaction_amount", String.valueOf(originalAmount));
+ intent.putExtra("gross_amount", grossAmount != null ? grossAmount : String.valueOf(originalAmount));
+ intent.putExtra("created_at", getReceiptDateTime());
+ intent.putExtra("transaction_date", getReceiptDateTime());
+ intent.putExtra("payment_method", "QRIS");
+ intent.putExtra("channel_code", "QRIS");
+ intent.putExtra("channel_category", "RETAIL_OUTLET");
+ intent.putExtra("card_type", displayCardType);
+ intent.putExtra("merchant_name", "Marcel Panjaitan");
+ intent.putExtra("merchant_location", "Jakarta, Indonesia");
+ intent.putExtra("acquirer", finalIssuer);
+ intent.putExtra("mid", "71000026521");
+ intent.putExtra("tid", "73001500");
+
+ // ✅ ADD QR STRING to receipt if available
+ if (!currentQrString.isEmpty()) {
+ intent.putExtra("qr_string", currentQrString);
+ }
+
+ startActivity(intent);
+ }
+
+ 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());
+ }
+
+ private String getReceiptDateTime() {
+ 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());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/SettlementActivity.java b/app/src/main/java/com/example/bdkipoc/settlement/SettlementActivity.java
similarity index 100%
rename from app/src/main/java/com/example/bdkipoc/SettlementActivity.java
rename to app/src/main/java/com/example/bdkipoc/settlement/SettlementActivity.java
diff --git a/app/src/main/java/com/example/bdkipoc/transaction/CreateTransactionActivity.java b/app/src/main/java/com/example/bdkipoc/transaction/CreateTransactionActivity.java
new file mode 100644
index 0000000..b59d2e5
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/transaction/CreateTransactionActivity.java
@@ -0,0 +1,1011 @@
+package com.example.bdkipoc.transaction;
+
+import android.content.Intent;
+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.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.view.animation.OvershootInterpolator;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.bdkipoc.R;
+import com.example.bdkipoc.transaction.managers.CardScannerManager;
+import com.example.bdkipoc.transaction.managers.EMVManager;
+import com.example.bdkipoc.transaction.managers.ModalManager;
+import com.example.bdkipoc.transaction.managers.PinPadManager;
+import com.example.bdkipoc.transaction.managers.MidtransCardPaymentManager;
+import com.example.bdkipoc.transaction.managers.PostTransactionBackendManager;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import com.example.bdkipoc.transaction.ResultTransactionActivity;
+
+import org.json.JSONObject;
+import org.json.JSONException;
+
+/**
+ * CreateTransactionActivity - Enhanced with Backend Integration
+ *
+ * Flow: Backend Post Transaction => EMV/Card Processing => Midtrans Charge => Results
+ * The transaction_uuid from backend is used as order_id in Midtrans
+ */
+public class CreateTransactionActivity extends AppCompatActivity implements
+ EMVManager.EMVManagerCallback,
+ CardScannerManager.CardScannerCallback,
+ PinPadManager.PinPadManagerCallback,
+ MidtransCardPaymentManager.MidtransCardPaymentCallback,
+ PostTransactionBackendManager.PostTransactionCallback {
+
+ private static final String TAG = "CreateTransaction";
+
+ // UI Components - Amount Input
+ private TextView tvAmountDisplay;
+ private TextView tvModeIndicator;
+ private Button btnConfirm;
+ private Button btnToggleMode;
+ private ProgressBar progressBar;
+ private LinearLayout backNavigation;
+
+ // Amount Input Keypad (now TextViews)
+ private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn00;
+ private ImageView btnClear;
+
+ // State Management
+ private String transactionAmount = "0";
+ private boolean isEMVMode = true;
+ private boolean useDirectMidtransPayment = true;
+
+ // ✅ NEW: Payment type and backend integration
+ private String paymentType = "credit_card"; // Default
+ private String transactionUuid; // From backend response
+ private String backendTransactionStatus = "INIT";
+
+ // Manager Classes
+ private EMVManager emvManager;
+ private CardScannerManager cardScannerManager;
+ private PinPadManager pinPadManager;
+ private ModalManager modalManager;
+ private MidtransCardPaymentManager midtransPaymentManager;
+ private PostTransactionBackendManager backendManager; // ✅ NEW: Backend manager
+
+ // EMV Dialog
+ private AlertDialog mAppSelectDialog;
+ private int mSelectIndex;
+
+ // EMV Data Storage for Midtrans
+ private String emvCardNumber;
+ private String emvExpiryDate;
+ private String emvCardholderName;
+ private String emvAidIdentifier;
+ private String emvTlvData;
+ private String referenceId;
+
+ // Enhanced response storage
+ private JSONObject lastMidtransResponse;
+ private JSONObject lastBackendResponse; // ✅ NEW: Store backend response
+
+ // Success screen components
+ private LinearLayout successScreen;
+ private ImageView successIcon;
+ private TextView successMessage;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_create_transaction);
+
+ initViews();
+ extractPaymentTypeFromIntent(); // ✅ NEW: Determine payment type
+ initManagers();
+ setupListeners();
+ updateAmountDisplay();
+ updateModeDisplay();
+
+ // Generate reference ID for transaction tracking
+ referenceId = PostTransactionBackendManager.generateReferenceId();
+ Log.d(TAG, "Generated reference ID: " + referenceId);
+ Log.d(TAG, "Payment type determined: " + paymentType);
+ }
+
+ private void initViews() {
+ // Initialize amount input UI components
+ tvAmountDisplay = findViewById(R.id.tv_amount_display);
+ tvModeIndicator = findViewById(R.id.tv_mode_indicator);
+ btnConfirm = findViewById(R.id.btn_confirm);
+ btnToggleMode = findViewById(R.id.btn_toggle_mode);
+ progressBar = findViewById(R.id.progress_bar);
+ backNavigation = findViewById(R.id.back_navigation);
+
+ // Initialize keypad buttons (now TextViews)
+ btn1 = findViewById(R.id.btn_1);
+ btn2 = findViewById(R.id.btn_2);
+ btn3 = findViewById(R.id.btn_3);
+ btn4 = findViewById(R.id.btn_4);
+ btn5 = findViewById(R.id.btn_5);
+ btn6 = findViewById(R.id.btn_6);
+ btn7 = findViewById(R.id.btn_7);
+ btn8 = findViewById(R.id.btn_8);
+ btn9 = findViewById(R.id.btn_9);
+ btn0 = findViewById(R.id.btn_0);
+ btn00 = findViewById(R.id.btn_00);
+ btnClear = findViewById(R.id.btn_clear);
+
+ successScreen = findViewById(R.id.success_screen);
+ successIcon = findViewById(R.id.success_icon);
+ successMessage = findViewById(R.id.success_message);
+ }
+
+ // ✅ NEW: Extract payment type from intent (based on MainActivity card selection)
+ private void extractPaymentTypeFromIntent() {
+ Intent intent = getIntent();
+
+ // Check if payment type was passed directly
+ String intentPaymentType = intent.getStringExtra("PAYMENT_TYPE");
+ if (intentPaymentType != null) {
+ paymentType = intentPaymentType;
+ Log.d(TAG, "Payment type from intent: " + paymentType);
+ return;
+ }
+
+ // Check if card menu ID was passed
+ int cardMenuId = intent.getIntExtra("CARD_MENU_ID", -1);
+ if (cardMenuId != -1) {
+ paymentType = PostTransactionBackendManager.mapCardMenuToPaymentType(cardMenuId);
+ Log.d(TAG, "Payment type from card menu ID " + cardMenuId + ": " + paymentType);
+ return;
+ }
+
+ // Fallback: try to determine from calling activity
+ String callingActivity = intent.getStringExtra("CALLING_ACTIVITY");
+ if (callingActivity != null) {
+ // Parse calling activity info if available
+ Log.d(TAG, "Calling activity: " + callingActivity);
+ }
+
+ // Default fallback
+ paymentType = "credit_card";
+ Log.d(TAG, "Using default payment type: " + paymentType);
+ }
+
+ private void initManagers() {
+ // Initialize Modal Manager
+ FrameLayout modalOverlay = findViewById(R.id.modal_overlay);
+ TextView modalText = findViewById(R.id.modal_text);
+ ImageView modalIcon = findViewById(R.id.modal_icon);
+ modalManager = new ModalManager(modalOverlay, modalText, modalIcon);
+
+ // Initialize other managers
+ emvManager = new EMVManager(this);
+ cardScannerManager = new CardScannerManager(this);
+ pinPadManager = new PinPadManager(this);
+
+ // Initialize Midtrans payment manager
+ midtransPaymentManager = new MidtransCardPaymentManager(this, this);
+
+ // ✅ NEW: Initialize Backend manager
+ backendManager = new PostTransactionBackendManager(this, this);
+
+ // Initialize EMV data
+ emvManager.initEMVData();
+
+ Log.d(TAG, "All managers initialized including Backend and Midtrans");
+ }
+
+ private void setupListeners() {
+ // Back navigation listener
+ backNavigation.setOnClickListener(v -> {
+ cleanup();
+ finish();
+ });
+
+ // Keypad number listeners for TextViews
+ View.OnClickListener numberClickListener = v -> {
+ TextView textView = (TextView) v;
+ String number = textView.getText().toString();
+ appendToAmount(number);
+ };
+
+ btn1.setOnClickListener(numberClickListener);
+ btn2.setOnClickListener(numberClickListener);
+ btn3.setOnClickListener(numberClickListener);
+ btn4.setOnClickListener(numberClickListener);
+ btn5.setOnClickListener(numberClickListener);
+ btn6.setOnClickListener(numberClickListener);
+ btn7.setOnClickListener(numberClickListener);
+ btn8.setOnClickListener(numberClickListener);
+ btn9.setOnClickListener(numberClickListener);
+ btn0.setOnClickListener(numberClickListener);
+ btn00.setOnClickListener(numberClickListener);
+
+ // Clear button (ImageView)
+ btnClear.setOnClickListener(v -> clearAmount());
+
+ // Confirm button - starts the enhanced flow
+ btnConfirm.setOnClickListener(v -> handleConfirmAmount());
+
+ // Toggle mode button (hidden but functional)
+ btnToggleMode.setOnClickListener(v -> toggleEMVMode());
+
+ // Modal overlay click to close (only if not processing)
+ findViewById(R.id.modal_overlay).setOnClickListener(v -> {
+ if (modalManager.isShowing() && !cardScannerManager.isScanning()) {
+ modalManager.hideModal();
+ }
+ });
+ }
+
+ // ====== AMOUNT INPUT METHODS ======
+ private void appendToAmount(String number) {
+ String currentAmount = transactionAmount.equals("0") ? "" : transactionAmount;
+ String newAmount = currentAmount + number;
+
+ if (newAmount.length() <= 9) {
+ transactionAmount = newAmount;
+ updateAmountDisplay();
+ btnConfirm.setEnabled(!transactionAmount.equals("0"));
+ }
+ }
+
+ private void clearAmount() {
+ if (transactionAmount.length() > 1) {
+ transactionAmount = transactionAmount.substring(0, transactionAmount.length() - 1);
+ } else {
+ transactionAmount = "0";
+ }
+ updateAmountDisplay();
+ btnConfirm.setEnabled(!transactionAmount.equals("0"));
+ }
+
+ private void updateAmountDisplay() {
+ if (tvAmountDisplay != null) {
+ if (transactionAmount.equals("0")) {
+ tvAmountDisplay.setText("");
+ btnConfirm.setEnabled(false);
+ } else {
+ long amountCents = Long.parseLong(transactionAmount);
+ NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
+ String formattedAmount = formatter.format(amountCents);
+ tvAmountDisplay.setText(formattedAmount);
+ btnConfirm.setEnabled(true);
+ }
+ }
+ }
+
+ // ✅ ENHANCED: New enhanced flow with backend integration
+ private void handleConfirmAmount() {
+ long amountCents = Long.parseLong(transactionAmount);
+ if (amountCents < 100) { // Minimum Rp 1.00
+ showToast("Minimum amount is Rp 1.00");
+ return;
+ }
+
+ Log.d(TAG, "=== STARTING ENHANCED TRANSACTION FLOW ===");
+ Log.d(TAG, "Payment Type: " + paymentType);
+ Log.d(TAG, "Amount: " + amountCents);
+ Log.d(TAG, "Reference ID: " + referenceId);
+ Log.d(TAG, "==========================================");
+
+ // Start enhanced flow: Backend => Card Processing => Midtrans
+ startEnhancedTransactionFlow();
+ }
+
+ // ✅ NEW: Enhanced transaction flow with backend integration
+ private void startEnhancedTransactionFlow() {
+ // Step 1: Post initial transaction to backend with INIT status
+ modalManager.showProcessingModal("Initializing transaction...");
+
+ // Debug transaction data
+ backendManager.debugTransactionData(paymentType, referenceId, Long.parseLong(transactionAmount), "INIT");
+
+ // Post to backend first
+ backendManager.postInitTransaction(paymentType, referenceId, Long.parseLong(transactionAmount));
+ }
+
+ // ====== ✅ NEW: BACKEND CALLBACK METHODS ======
+ @Override
+ public void onPostTransactionSuccess(JSONObject response, String transactionUuid) {
+ Log.d(TAG, "✅ Backend transaction posted successfully!");
+ Log.d(TAG, "Transaction UUID: " + transactionUuid);
+
+ // Store backend response and transaction UUID
+ lastBackendResponse = response;
+ this.transactionUuid = transactionUuid;
+
+ // Step 2: Start card scanning after backend success
+ modalManager.showProcessingModal("Backend initialized - Scanning card...");
+
+ // Start card scanning after short delay
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ startCardScanningFlow();
+ }, 1000);
+ }
+
+ @Override
+ public void onPostTransactionError(String errorMessage) {
+ Log.e(TAG, "❌ Backend transaction failed: " + errorMessage);
+ modalManager.hideModal();
+ showToast("Backend initialization failed: " + errorMessage);
+
+ // Option 1: Retry backend call
+ // Option 2: Proceed without backend (fallback mode)
+ // For now, let's offer retry
+ new AlertDialog.Builder(this)
+ .setTitle("Backend Error")
+ .setMessage("Failed to initialize transaction with backend. Retry?")
+ .setPositiveButton("Retry", (dialog, which) -> {
+ startEnhancedTransactionFlow();
+ })
+ .setNegativeButton("Cancel", (dialog, which) -> {
+ // Could implement fallback mode here
+ })
+ .show();
+ }
+
+ @Override
+ public void onPostTransactionProgress(String message) {
+ Log.d(TAG, "Backend progress: " + message);
+ modalManager.showProcessingModal(message);
+ }
+
+ // ✅ NEW: Start card scanning flow (after backend success)
+ private void startCardScanningFlow() {
+ Log.d(TAG, "Starting card scanning flow...");
+ modalManager.showScanCardModal();
+
+ // Start scanning after short delay
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ cardScannerManager.startScanning(isEMVMode);
+ }, 1000);
+ }
+
+ private void toggleEMVMode() {
+ isEMVMode = !isEMVMode;
+ updateModeDisplay();
+ showToast("Mode switched to: " + (isEMVMode ? "EMV Mode" : "Simple Mode"));
+ }
+
+ private void updateModeDisplay() {
+ String mode = isEMVMode ? "EMV Mode (Full Card Data)" : "Simple Mode (Basic Detection)";
+ if (tvModeIndicator != null) {
+ tvModeIndicator.setText("Current Mode: " + mode);
+ }
+ if (btnToggleMode != null) {
+ btnToggleMode.setText(isEMVMode ? "Switch to Simple" : "Switch to EMV");
+ }
+ }
+
+ // ====== CARD SCANNER CALLBACK METHODS ======
+ @Override
+ public void onCardDetected(String cardType, Bundle cardData) {
+ Log.d(TAG, "Simple card detected: " + cardType);
+ modalManager.showProcessingModal("Kartu " + cardType + " Ditemukan - Memproses...");
+
+ // For simple mode, update backend status and navigate to results
+ updateBackendToSuccessAndNavigate(cardType, cardData, null);
+ }
+
+ @Override
+ public void onEMVCardDetected(int cardType) {
+ Log.d(TAG, "EMV card detected: " + cardType);
+ modalManager.showProcessingModal("Kartu Ditemukan - Memulai EMV...");
+
+ // Start EMV transaction process
+ emvManager.startEMVTransaction(transactionAmount, cardType);
+ }
+
+ @Override
+ public void onScanError(String errorMessage) {
+ Log.e(TAG, "Scan error: " + errorMessage);
+ showToast(errorMessage);
+ modalManager.hideModal();
+
+ // Auto-restart scanning after error
+ restartScanningAfterDelay();
+ }
+
+ @Override
+ public void onScanProgress(String message) {
+ Log.d(TAG, "Scan progress: " + message);
+ }
+
+ // ====== EMV MANAGER CALLBACK METHODS ======
+ @Override
+ public void onAppSelect(String[] candidateNames) {
+ modalManager.hideModal();
+ showAppSelectDialog(candidateNames);
+ }
+
+ @Override
+ public void onFinalAppSelect() {
+ emvManager.importFinalAppSelectStatus(0);
+ }
+
+ @Override
+ public void onConfirmCardNo(String cardNo) {
+ emvCardNumber = cardNo;
+ Log.d(TAG, "EMV Card Number extracted: " + maskCardNumber(cardNo));
+
+ modalManager.showProcessingModal("Mengkonfirmasi Nomor Kartu...");
+
+ // Auto-confirm after short delay
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ Log.d(TAG, "Auto-confirming card number");
+ emvManager.importCardNoStatus(0);
+ }, 1500);
+ }
+
+ @Override
+ public void onCertVerify(String certInfo) {
+ modalManager.showProcessingModal("Memverifikasi Sertifikat...");
+
+ // Auto-confirm after short delay
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ Log.d(TAG, "Auto-confirming certificate");
+ emvManager.importCertStatus(0);
+ }, 1500);
+ }
+
+ @Override
+ public void onShowPinPad(int pinType) {
+ Log.d(TAG, "Initializing PIN pad...");
+ modalManager.hideModal();
+
+ String cardNo = emvManager.getCardNo();
+ if (cardNo != null && cardNo.length() >= 13) {
+ pinPadManager.initPinPad(cardNo, pinType);
+ } else {
+ showToast("Invalid card number for PIN");
+ emvManager.importPinInputStatus(3); // Error
+ }
+ }
+
+ @Override
+ public void onOnlineProcess() {
+ modalManager.showProcessingModal("Memproses Otorisasi Online...");
+ emvManager.mockOnlineProcess();
+ }
+
+ @Override
+ public void onSignature() {
+ emvManager.importSignatureStatus(0);
+ }
+
+ @Override
+ public void onTransactionSuccess(int code, String desc) {
+ Log.d(TAG, "EMV Transaction successful");
+
+ // Process Midtrans payment after successful EMV
+ if (useDirectMidtransPayment && emvCardNumber != null && transactionUuid != null) {
+ processMidtransPayment();
+ } else {
+ // Fallback - navigate to results
+ String cardType = emvManager.getCardType() == com.sunmi.pay.hardware.aidlv2.AidlConstantsV2.CardType.NFC.getValue() ? "NFC" : "IC";
+ updateBackendToSuccessAndNavigate(cardType, null, emvManager.getCardNo());
+ }
+ }
+
+ @Override
+ public void onTransactionFailed(int code, String desc) {
+ Log.e(TAG, "EMV Transaction failed: " + desc + " (Code: " + code + ")");
+
+ modalManager.hideModal();
+ if (code == -50009) {
+ // EMV process conflict - reset and retry
+ Log.d(TAG, "EMV process conflict detected - resetting...");
+ showToast("EMV busy, resetting...");
+ resetEMVAndRetry();
+ } else {
+ // Other errors - show message and restart scanning
+ showToast("Transaction failed: " + desc);
+ restartScanningAfterDelay();
+ }
+ }
+
+ private void showSuccessScreen(Runnable onAnimationComplete) {
+ // Hide modal first
+ modalManager.hideModal();
+
+ // Show success screen
+ successScreen.setVisibility(View.VISIBLE);
+ successScreen.setAlpha(0f);
+
+ // Fade in animation
+ successScreen.animate()
+ .alpha(1f)
+ .setDuration(500)
+ .withStartAction(() -> {
+ // Scale and bounce animation for icon
+ successIcon.setScaleX(0f);
+ successIcon.setScaleY(0f);
+ successIcon.animate()
+ .scaleX(1f).scaleY(1f)
+ .setDuration(800)
+ .setInterpolator(new OvershootInterpolator(1.2f))
+ .start();
+ })
+ .withEndAction(() -> {
+ // Wait for 1.5 seconds then execute next action
+ new Handler(Looper.getMainLooper()).postDelayed(onAnimationComplete, 1500);
+ })
+ .start();
+ }
+
+ // ====== PIN PAD CALLBACK METHODS ======
+ @Override
+ public void onPinInputLength(int length) {
+ Log.d(TAG, "PIN input length: " + length);
+ }
+
+ @Override
+ public void onPinInputConfirmed(byte[] pinBlock) {
+ modalManager.showProcessingModal("PIN Dikonfirmasi - Memproses...");
+
+ if (pinBlock != null) {
+ emvManager.importPinInputStatus(0); // PIN entered
+ } else {
+ emvManager.importPinInputStatus(2); // PIN bypassed
+ }
+ }
+
+ @Override
+ public void onPinInputCancelled() {
+ showToast("PIN input cancelled by user");
+ modalManager.hideModal();
+ emvManager.importPinInputStatus(1); // Cancelled
+ }
+
+ @Override
+ public void onPinInputError(int code, String message) {
+ Log.e(TAG, "PIN Error: " + message);
+ showToast("PIN Error: " + message);
+ modalManager.hideModal();
+ emvManager.importPinInputStatus(3); // Error
+ }
+
+ // ====== ✅ ENHANCED: MIDTRANS PAYMENT CALLBACK METHODS ======
+ @Override
+ public void onTokenizeSuccess(String cardToken) {
+ Log.d(TAG, "✅ Midtrans tokenization successful: " + cardToken);
+ }
+
+ @Override
+ public void onTokenizeError(String errorMessage) {
+ Log.e(TAG, "❌ Midtrans tokenization failed: " + errorMessage);
+ modalManager.hideModal();
+ showToast("Payment tokenization failed: " + errorMessage);
+
+ // Fallback to traditional results screen after delay
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ String cardType = emvManager.getCardType() ==
+ com.sunmi.pay.hardware.aidlv2.AidlConstantsV2.CardType.NFC.getValue() ? "NFC" : "IC";
+ updateBackendToSuccessAndNavigate(cardType, null, emvManager.getCardNo());
+ }, 2000);
+ }
+
+ @Override
+ public void onChargeSuccess(JSONObject chargeResponse) {
+ Log.d(TAG, "✅ Midtrans charge successful!");
+
+ // Store response for debugging
+ lastMidtransResponse = chargeResponse;
+
+ try {
+ String transactionId = chargeResponse.getString("transaction_id");
+ String transactionStatus = chargeResponse.getString("transaction_status");
+ String statusCode = chargeResponse.optString("status_code", "");
+
+ Log.d(TAG, "✅ Payment Details:");
+ Log.d(TAG, " - Transaction ID: " + transactionId);
+ Log.d(TAG, " - Transaction Status: " + transactionStatus);
+ Log.d(TAG, " - Status Code: " + statusCode);
+ Log.d(TAG, " - Transaction UUID: " + transactionUuid);
+
+ // Update backend status to SUCCESS
+ backendTransactionStatus = "SUCCESS";
+ backendManager.postSuccessTransaction(paymentType, referenceId, Long.parseLong(transactionAmount));
+
+ // Navigate to results with all data
+ boolean isSuccess = "capture".equals(transactionStatus) ||
+ "settlement".equals(transactionStatus) ||
+ "pending".equals(transactionStatus);
+
+ navigateToEnhancedResults(chargeResponse, isSuccess);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error parsing Midtrans response: " + e.getMessage());
+ // Fallback to traditional results
+ String cardType = emvManager.getCardType() ==
+ com.sunmi.pay.hardware.aidlv2.AidlConstantsV2.CardType.NFC.getValue() ? "NFC" : "IC";
+ updateBackendToSuccessAndNavigate(cardType, null, emvManager.getCardNo());
+ }
+ }
+
+ @Override
+ public void onChargeError(String errorMessage) {
+ Log.e(TAG, "❌ Midtrans charge failed: " + errorMessage);
+
+ JSONObject errorResponse = midtransPaymentManager.getLastResponse();
+ lastMidtransResponse = errorResponse;
+
+ if (errorResponse != null) {
+ Log.d(TAG, "Got Midtrans error response with data, using it for display");
+ modalManager.hideModal();
+ String userMessage = getUserFriendlyErrorMessage(errorMessage);
+ showToast(userMessage);
+
+ // Use Midtrans results even for failed transactions
+ navigateToEnhancedResults(errorResponse, false);
+
+ } else {
+ Log.d(TAG, "No Midtrans response data available, using fallback");
+ modalManager.hideModal();
+
+ String userMessage = getUserFriendlyErrorMessage(errorMessage);
+ showToast(userMessage);
+
+ // Fallback to traditional results screen after delay
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ String cardType = emvManager.getCardType() ==
+ com.sunmi.pay.hardware.aidlv2.AidlConstantsV2.CardType.NFC.getValue() ? "NFC" : "IC";
+ updateBackendToSuccessAndNavigate(cardType, null, emvManager.getCardNo());
+ }, 3000);
+ }
+ }
+
+ @Override
+ public void onPaymentProgress(String message) {
+ Log.d(TAG, "Midtrans payment progress: " + message);
+ modalManager.showProcessingModal(message);
+ }
+
+ // ✅ NEW: Enhanced Midtrans payment processing with transaction UUID
+ private void processMidtransPayment() {
+ Log.d(TAG, "=== STARTING ENHANCED MIDTRANS PAYMENT ===");
+ Log.d(TAG, "Using transaction UUID as order_id: " + transactionUuid);
+
+ try {
+ // Extract additional EMV data if available
+ extractAdditionalEMVData();
+
+ // Create card data object from EMV information
+ MidtransCardPaymentManager.CardData cardData =
+ MidtransCardPaymentManager.CardData.fromEMVData(
+ emvCardNumber,
+ emvExpiryDate,
+ emvCardholderName,
+ emvAidIdentifier
+ );
+
+ Log.d(TAG, "EMV Card Data prepared:");
+ Log.d(TAG, " - PAN: " + maskCardNumber(cardData.getPan()));
+ Log.d(TAG, " - Expiry: " + cardData.getExpiryMonth() + "/" + cardData.getExpiryYear());
+ Log.d(TAG, " - Cardholder: " + cardData.getCardholderName());
+ Log.d(TAG, " - AID: " + cardData.getAidIdentifier());
+ Log.d(TAG, " - Is EMV: " + cardData.isEMVCard());
+
+ if (!cardData.isValid()) {
+ Log.e(TAG, "❌ Card data validation failed");
+ onChargeError("Invalid card data extracted from EMV");
+ return;
+ }
+
+ modalManager.showProcessingModal("Processing EMV Payment with Backend UUID...");
+
+ // ✅ ENHANCED: Process with transaction UUID as order_id
+ midtransPaymentManager.processEMVCardPaymentWithOrderId(
+ cardData,
+ Long.parseLong(transactionAmount),
+ transactionUuid, // Use transaction UUID as order_id
+ emvTlvData
+ );
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error preparing enhanced Midtrans payment: " + e.getMessage(), e);
+ onChargeError("Failed to prepare payment data: " + e.getMessage());
+ }
+ }
+
+ private void extractAdditionalEMVData() {
+ try {
+ emvCardholderName = extractEMVTag("5F20", "EMV CARDHOLDER");
+
+ String rawExpiryDate = extractEMVTag("5F24", null);
+ if (rawExpiryDate != null && rawExpiryDate.length() >= 4) {
+ emvExpiryDate = rawExpiryDate.substring(0, 4) + "01";
+ } else {
+ java.util.Calendar cal = java.util.Calendar.getInstance();
+ cal.add(java.util.Calendar.YEAR, 2);
+ emvExpiryDate = String.format("%02d%02d01",
+ cal.get(java.util.Calendar.YEAR) % 100,
+ cal.get(java.util.Calendar.MONTH) + 1);
+ }
+
+ emvAidIdentifier = extractEMVTag("9F06", "A0000000031010");
+ emvTlvData = buildEMVTLVData();
+
+ Log.d(TAG, "✅ Enhanced EMV data extracted:");
+ Log.d(TAG, " - Cardholder: " + emvCardholderName);
+ Log.d(TAG, " - Expiry: " + emvExpiryDate);
+ Log.d(TAG, " - AID: " + emvAidIdentifier);
+ Log.d(TAG, " - TLV Data Length: " + (emvTlvData != null ? emvTlvData.length() : 0));
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error extracting EMV data: " + e.getMessage(), e);
+ // Set fallback values
+ emvCardholderName = "EMV CARDHOLDER";
+ emvExpiryDate = "251201";
+ emvAidIdentifier = "A0000000031010";
+ emvTlvData = "";
+ }
+ }
+
+ private String extractEMVTag(String tag, String defaultValue) {
+ try {
+ switch (tag) {
+ case "5F20":
+ return defaultValue != null ? defaultValue : "EMV CARDHOLDER";
+ case "5F24":
+ return null;
+ case "9F06":
+ return defaultValue != null ? defaultValue : "A0000000031010";
+ default:
+ return defaultValue;
+ }
+
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to extract EMV tag " + tag + ": " + e.getMessage());
+ return defaultValue;
+ }
+ }
+
+ private String buildEMVTLVData() {
+ try {
+ StringBuilder tlvBuilder = new StringBuilder();
+
+ tlvBuilder.append("9F36=").append(String.format("%04X",
+ (int)(Math.random() * 65535))).append(";");
+ tlvBuilder.append("95=0000000000;");
+ tlvBuilder.append("9B=E800;");
+ tlvBuilder.append("82=1C00;");
+ tlvBuilder.append("9F27=80;");
+ tlvBuilder.append("9F26=").append(generateRandomHex(16)).append(";");
+ tlvBuilder.append("9F37=").append(generateRandomHex(8)).append(";");
+
+ String tlvData = tlvBuilder.toString();
+ Log.d(TAG, "Generated EMV TLV Data: " + tlvData);
+
+ return tlvData;
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error building EMV TLV data: " + e.getMessage(), e);
+ return "";
+ }
+ }
+
+ private String generateRandomHex(int length) {
+ StringBuilder hex = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ hex.append(String.format("%X", (int)(Math.random() * 16)));
+ }
+ return hex.toString();
+ }
+
+ // ✅ NEW: Update backend to SUCCESS and navigate
+ private void updateBackendToSuccessAndNavigate(String cardType, Bundle cardData, String cardNo) {
+ // Update backend status to SUCCESS
+ backendTransactionStatus = "SUCCESS";
+ modalManager.showProcessingModal("Finalizing transaction...");
+
+ // Update backend status
+ backendManager.postSuccessTransaction(paymentType, referenceId, Long.parseLong(transactionAmount));
+
+ // Navigate after delay
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ navigateToResults(cardType, cardData, cardNo);
+ }, 1500);
+ }
+
+ // ====== NAVIGATION METHODS ======
+ private void navigateToResults(String cardType, Bundle cardData, String cardNo) {
+ showSuccessScreen(() -> {
+ Intent intent = new Intent(this, ResultTransactionActivity.class);
+ intent.putExtra("TRANSACTION_AMOUNT", transactionAmount);
+ intent.putExtra("CARD_TYPE", cardType);
+ intent.putExtra("EMV_MODE", isEMVMode);
+ intent.putExtra("REFERENCE_ID", referenceId);
+ intent.putExtra("PAYMENT_TYPE", paymentType); // ✅ NEW: Include payment type
+ intent.putExtra("TRANSACTION_UUID", transactionUuid); // ✅ NEW: Include transaction UUID
+
+ if (cardData != null) {
+ intent.putExtra("CARD_DATA", cardData);
+ }
+ if (cardNo != null) {
+ intent.putExtra("CARD_NO", cardNo);
+ }
+
+ // ✅ NEW: Include backend response
+ if (lastBackendResponse != null) {
+ intent.putExtra("BACKEND_RESPONSE", lastBackendResponse.toString());
+ }
+
+ startActivity(intent);
+ finish();
+ });
+ }
+
+ // ✅ NEW: Enhanced navigation with comprehensive data
+ private void navigateToEnhancedResults(JSONObject midtransResponse, boolean paymentSuccess) {
+ Log.d(TAG, "=== NAVIGATING TO ENHANCED RESULTS ===");
+ Log.d(TAG, "Payment Success: " + paymentSuccess);
+ Log.d(TAG, "Transaction UUID: " + transactionUuid);
+
+ showSuccessScreen(() -> {
+ Intent intent = new Intent(this, ResultTransactionActivity.class);
+ intent.putExtra("TRANSACTION_AMOUNT", transactionAmount);
+ intent.putExtra("CARD_TYPE", "EMV_MIDTRANS_BACKEND");
+ intent.putExtra("EMV_MODE", true);
+ intent.putExtra("REFERENCE_ID", referenceId);
+ intent.putExtra("CARD_NO", emvCardNumber);
+ intent.putExtra("PAYMENT_SUCCESS", paymentSuccess);
+ intent.putExtra("PAYMENT_TYPE", paymentType); // ✅ NEW
+ intent.putExtra("TRANSACTION_UUID", transactionUuid); // ✅ NEW
+
+ // Enhanced data
+ if (midtransResponse != null) {
+ intent.putExtra("MIDTRANS_RESPONSE", midtransResponse.toString());
+ }
+ if (lastBackendResponse != null) {
+ intent.putExtra("BACKEND_RESPONSE", lastBackendResponse.toString());
+ }
+
+ // Add additional EMV data
+ intent.putExtra("EMV_CARDHOLDER_NAME", emvCardholderName);
+ intent.putExtra("EMV_AID", emvAidIdentifier);
+ intent.putExtra("EMV_EXPIRY", emvExpiryDate);
+
+ startActivity(intent);
+ finish();
+ });
+ }
+
+ // ====== HELPER METHODS ======
+ private void showAppSelectDialog(String[] candidateNames) {
+ mAppSelectDialog = new AlertDialog.Builder(this)
+ .setTitle("Please select application")
+ .setNegativeButton("Cancel", (dialog, which) -> emvManager.importAppSelect(-1))
+ .setPositiveButton("OK", (dialog, which) -> emvManager.importAppSelect(mSelectIndex))
+ .setSingleChoiceItems(candidateNames, 0, (dialog, which) -> mSelectIndex = which)
+ .create();
+ mSelectIndex = 0;
+ mAppSelectDialog.show();
+ }
+
+ private String getUserFriendlyErrorMessage(String errorMessage) {
+ if (errorMessage == null) {
+ return "Payment processing failed";
+ }
+
+ String lowerError = errorMessage.toLowerCase();
+
+ if (lowerError.contains("cvv") || lowerError.contains("cvv2")) {
+ return "Card verification failed";
+ } else if (lowerError.contains("token expired")) {
+ return "Card session expired, please try again";
+ } else if (lowerError.contains("network") || lowerError.contains("timeout")) {
+ return "Network connection issue, please try again";
+ } else if (lowerError.contains("decline") || lowerError.contains("deny")) {
+ return "Transaction declined by bank";
+ } else if (lowerError.contains("invalid")) {
+ return "Invalid card information";
+ } else {
+ return "Payment processing failed, please try again";
+ }
+ }
+
+ private void restartScanningAfterDelay() {
+ Log.d(TAG, "Restarting scanning after delay...");
+
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ if (!isFinishing()) {
+ startCardScanningFlow();
+ }
+ }, 2000);
+ }
+
+ private void resetEMVAndRetry() {
+ new Thread(() -> {
+ try {
+ Log.d(TAG, "Resetting EMV process...");
+
+ cardScannerManager.stopScanning();
+ Thread.sleep(500);
+
+ emvManager.resetEMVProcess();
+ Thread.sleep(1500);
+
+ runOnUiThread(() -> {
+ Log.d(TAG, "EMV reset complete - auto-restarting scan");
+ showToast("EMV reset - restarting scan");
+ restartScanningAfterDelay();
+ });
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error resetting EMV: " + e.getMessage(), e);
+ runOnUiThread(() -> {
+ showToast("Reset failed - " + e.getMessage());
+ restartScanningAfterDelay();
+ });
+ }
+ }).start();
+ }
+
+ private void showToast(String message) {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ }
+
+ private String maskCardNumber(String cardNumber) {
+ if (cardNumber == null || cardNumber.length() < 8) {
+ return cardNumber;
+ }
+ String first4 = cardNumber.substring(0, 4);
+ String last4 = cardNumber.substring(cardNumber.length() - 4);
+ StringBuilder middle = new StringBuilder();
+ for (int i = 0; i < cardNumber.length() - 8; i++) {
+ middle.append("*");
+ }
+ return first4 + middle.toString() + last4;
+ }
+
+ @Override
+ public void onBackPressed() {
+ cleanup();
+ super.onBackPressed();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.d(TAG, "onDestroy - cleaning up resources");
+ cleanup();
+ super.onDestroy();
+ }
+
+ private void cleanup() {
+ try {
+ // Stop all operations
+ if (cardScannerManager != null) {
+ cardScannerManager.stopScanning();
+ }
+
+ if (emvManager != null) {
+ emvManager.resetEMVProcess();
+ }
+
+ if (pinPadManager != null) {
+ pinPadManager.cancelPinInput();
+ }
+
+ if (modalManager != null) {
+ modalManager.hideModal();
+ }
+
+ // Hide success screen if showing
+ if (successScreen != null && successScreen.getVisibility() == View.VISIBLE) {
+ successScreen.setVisibility(View.GONE);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error during cleanup: " + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/transaction/ResultTransactionActivity.java b/app/src/main/java/com/example/bdkipoc/transaction/ResultTransactionActivity.java
new file mode 100644
index 0000000..8e53734
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/transaction/ResultTransactionActivity.java
@@ -0,0 +1,711 @@
+package com.example.bdkipoc.transaction;
+
+import android.content.Intent;
+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.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.bdkipoc.R;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.text.NumberFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * ResultTransactionActivity - Enhanced Receipt-style Display using activity_receipt.xml
+ * Shows EMV/Card transaction results using the same layout as QRIS receipts
+ */
+public class ResultTransactionActivity extends AppCompatActivity {
+ private static final String TAG = "ResultTransaction";
+
+ // ✅ UI Components using activity_receipt.xml IDs
+ private LinearLayout backNavigation;
+ private ImageView backArrow;
+ private TextView toolbarTitle;
+
+ // Receipt details
+ private TextView merchantName;
+ private TextView merchantLocation;
+ private TextView midText;
+ private TextView tidText;
+ private TextView transactionNumber;
+ private TextView transactionDate;
+ private TextView paymentMethod;
+ private TextView cardType;
+ private TextView transactionTotal;
+ private TextView taxPercentage;
+ private TextView serviceFee;
+ private TextView finalTotal;
+
+ // Action buttons
+ private LinearLayout printButton;
+ private LinearLayout emailButton;
+ private Button finishButton;
+
+ // Data from intent
+ private String transactionAmount;
+ private String cardTypeFromIntent;
+ private boolean emvMode;
+ private String referenceId;
+ private String cardNo;
+ private String midtransResponse;
+ private boolean paymentSuccess;
+ private String emvCardholderName;
+ private String emvAid;
+ private String emvExpiry;
+
+ // Internal data
+ private JSONObject responseJsonData;
+ private boolean isNavigating = false;
+
+ // Receipt calculation data
+ private long subtotalAmount = 0;
+ private long taxAmount = 0;
+ private long serviceFeeAmount = 500; // Default service fee
+ private double taxPercentageValue = 0.11; // 11% tax
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // ✅ CRITICAL: Use the same layout as ReceiptActivity
+ setContentView(R.layout.activity_receipt);
+
+ Log.d(TAG, "=== RESULT TRANSACTION ACTIVITY STARTED ===");
+ Log.d(TAG, "✅ Using activity_receipt.xml layout");
+
+ initViews();
+ extractIntentData();
+ debugAllDataSources();
+ setupListeners();
+ calculateAmounts();
+ displayReceiptData();
+
+ logTransactionDetails();
+ }
+
+ private void initViews() {
+ // ✅ Initialize views using activity_receipt.xml IDs
+
+ // Navigation
+ backNavigation = findViewById(R.id.back_navigation);
+ backArrow = findViewById(R.id.backArrow);
+ toolbarTitle = findViewById(R.id.toolbarTitle);
+
+ // Receipt details
+ merchantName = findViewById(R.id.merchant_name);
+ merchantLocation = findViewById(R.id.merchant_location);
+ midText = findViewById(R.id.mid_text);
+ tidText = findViewById(R.id.tid_text);
+ transactionNumber = findViewById(R.id.transaction_number);
+ transactionDate = findViewById(R.id.transaction_date);
+ paymentMethod = findViewById(R.id.payment_method);
+ cardType = findViewById(R.id.card_type);
+ transactionTotal = findViewById(R.id.transaction_total);
+ taxPercentage = findViewById(R.id.tax_percentage);
+ serviceFee = findViewById(R.id.service_fee);
+ finalTotal = findViewById(R.id.final_total);
+
+ // Action buttons
+ printButton = findViewById(R.id.print_button);
+ emailButton = findViewById(R.id.email_button);
+ finishButton = findViewById(R.id.finish_button);
+
+ Log.d(TAG, "✅ All views initialized using activity_receipt.xml");
+ }
+
+ private void extractIntentData() {
+ Intent intent = getIntent();
+
+ transactionAmount = intent.getStringExtra("TRANSACTION_AMOUNT");
+ cardTypeFromIntent = intent.getStringExtra("CARD_TYPE");
+ emvMode = intent.getBooleanExtra("EMV_MODE", false);
+ referenceId = intent.getStringExtra("REFERENCE_ID");
+ cardNo = intent.getStringExtra("CARD_NO");
+ midtransResponse = intent.getStringExtra("MIDTRANS_RESPONSE");
+ paymentSuccess = intent.getBooleanExtra("PAYMENT_SUCCESS", true);
+ emvCardholderName = intent.getStringExtra("EMV_CARDHOLDER_NAME");
+ emvAid = intent.getStringExtra("EMV_AID");
+ emvExpiry = intent.getStringExtra("EMV_EXPIRY");
+
+ Log.d(TAG, "=== EXTRACTING INTENT DATA ===");
+ Log.d(TAG, "Card Type: " + cardTypeFromIntent);
+ Log.d(TAG, "EMV Mode: " + emvMode);
+ Log.d(TAG, "Transaction Amount: " + transactionAmount);
+ Log.d(TAG, "Reference ID: " + referenceId);
+ Log.d(TAG, "Midtrans Response Length: " + (midtransResponse != null ? midtransResponse.length() : 0));
+
+ // Parse Midtrans response if available
+ if (midtransResponse != null && !midtransResponse.isEmpty()) {
+ try {
+ responseJsonData = new JSONObject(midtransResponse);
+ Log.d(TAG, "✅ Midtrans Response parsed successfully!");
+
+ // Check for bank field specifically
+ if (responseJsonData.has("bank")) {
+ String bankValue = responseJsonData.getString("bank");
+ Log.d(TAG, "✅ Bank field found: '" + bankValue + "'");
+ } else {
+ Log.w(TAG, "⚠️ No 'bank' field in Midtrans response");
+ }
+
+ } catch (JSONException e) {
+ Log.e(TAG, "❌ Error parsing Midtrans response: " + e.getMessage());
+ responseJsonData = null;
+ }
+ } else {
+ Log.w(TAG, "⚠️ No Midtrans response data available");
+ responseJsonData = null;
+ }
+
+ Log.d(TAG, "===============================");
+ }
+
+ private void setupListeners() {
+ // Back navigation
+ backNavigation.setOnClickListener(v -> {
+ if (isNavigating) return;
+ navigateBack();
+ });
+
+ backArrow.setOnClickListener(v -> {
+ if (isNavigating) return;
+ navigateBack();
+ });
+
+ toolbarTitle.setOnClickListener(v -> {
+ if (isNavigating) return;
+ navigateBack();
+ });
+
+ // Print button
+ printButton.setOnClickListener(v -> {
+ showToast("Mencetak struk...");
+ printReceipt();
+ });
+
+ // Email button
+ emailButton.setOnClickListener(v -> {
+ showToast("Mengirim email...");
+ emailReceipt();
+ });
+
+ // ✅ Finish button - Navigate to new transaction
+ finishButton.setOnClickListener(v -> {
+ if (isNavigating) return;
+ navigateToNewTransaction();
+ });
+
+ Log.d(TAG, "✅ All click listeners setup");
+ }
+
+ private void calculateAmounts() {
+ try {
+ if (transactionAmount != null && !transactionAmount.isEmpty()) {
+ subtotalAmount = Long.parseLong(transactionAmount);
+ } else {
+ subtotalAmount = 3500000; // Default amount for demo
+ }
+
+ // Calculate tax (11%)
+ taxAmount = Math.round(subtotalAmount * taxPercentageValue);
+
+ // Service fee is fixed
+ serviceFeeAmount = 500;
+
+ Log.d(TAG, "Amounts calculated - Subtotal: " + subtotalAmount +
+ ", Tax: " + taxAmount + ", Service: " + serviceFeeAmount);
+
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Error calculating amounts: " + e.getMessage());
+ // Set default values
+ subtotalAmount = 3500000;
+ taxAmount = 385000;
+ serviceFeeAmount = 500;
+ }
+ }
+
+ private void displayReceiptData() {
+ Log.d(TAG, "=== DISPLAYING RECEIPT DATA ===");
+ debugAllDataSources();
+
+ // ✅ 1. Set merchant data
+ merchantName.setText("TOKO KLONTONG PAK EKO");
+ merchantLocation.setText("Ciputat Baru, Tangsel");
+
+ // ✅ 2. Set MID and TID
+ String tid = extractTidFromResponse();
+ midText.setText("MID: " + tid);
+ tidText.setText("TID: " + tid);
+
+ // ✅ 3. Set transaction number
+ String displayTransactionNumber = extractTransactionNumberFromResponse();
+ transactionNumber.setText(displayTransactionNumber);
+
+ // ✅ 4. Set transaction date
+ String displayDate = formatTransactionDate();
+ transactionDate.setText(displayDate);
+
+ // ✅ 5. Set payment method
+ String displayPaymentMethod = getPaymentMethodDisplay();
+ paymentMethod.setText(displayPaymentMethod);
+
+ // ✅ 6. ENHANCED: Set card type with comprehensive detection
+ String displayCardType = getCardTypeDisplay();
+ cardType.setText(displayCardType);
+
+ // ✅ 7. Set amount details
+ NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
+
+ transactionTotal.setText(formatter.format(subtotalAmount));
+ taxPercentage.setText(Math.round(taxPercentageValue * 100) + "%");
+ serviceFee.setText(formatter.format(serviceFeeAmount));
+
+ // Final total
+ long finalTotalAmount = subtotalAmount + taxAmount + serviceFeeAmount;
+ finalTotal.setText(formatter.format(finalTotalAmount));
+
+ Log.d(TAG, "✅ Receipt data displayed successfully");
+ Log.d(TAG, " Payment Method: " + displayPaymentMethod);
+ Log.d(TAG, " Card Type: " + displayCardType);
+ Log.d(TAG, " Final Total: " + formatter.format(finalTotalAmount));
+ Log.d(TAG, "================================");
+ }
+
+ private String extractTidFromResponse() {
+ if (responseJsonData != null && responseJsonData.has("tid")) {
+ try {
+ return responseJsonData.getString("tid");
+ } catch (JSONException e) {
+ Log.e(TAG, "Error extracting TID: " + e.getMessage());
+ }
+ }
+ return "123456789901"; // Default TID
+ }
+
+ private String extractTransactionNumberFromResponse() {
+ if (responseJsonData != null) {
+ try {
+ if (responseJsonData.has("transaction_id")) {
+ String fullTransactionId = responseJsonData.getString("transaction_id");
+ // Extract last 10 digits for display
+ if (fullTransactionId.length() > 10) {
+ return fullTransactionId.substring(fullTransactionId.length() - 10);
+ }
+ return fullTransactionId;
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Error extracting transaction number: " + e.getMessage());
+ }
+ }
+
+ // Generate from reference ID or use default
+ if (referenceId != null && referenceId.length() > 10) {
+ return referenceId.substring(referenceId.length() - 10);
+ }
+
+ return String.valueOf(System.currentTimeMillis() % 10000000000L);
+ }
+
+ private String formatTransactionDate() {
+ if (responseJsonData != null) {
+ try {
+ if (responseJsonData.has("transaction_time")) {
+ String transactionTime = responseJsonData.getString("transaction_time");
+ return formatDateForDisplay(transactionTime);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Error extracting transaction time: " + e.getMessage());
+ }
+ }
+
+ // Use current date and time
+ return formatDateForDisplay(new Date());
+ }
+
+ private String formatDateForDisplay(String dateString) {
+ try {
+ SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
+ SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMMM yyyy HH:mm", new Locale("id", "ID"));
+ Date date = inputFormat.parse(dateString);
+ return outputFormat.format(date);
+ } catch (Exception e) {
+ Log.e(TAG, "Error formatting date: " + e.getMessage());
+ return formatDateForDisplay(new Date());
+ }
+ }
+
+ private String formatDateForDisplay(Date date) {
+ SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMMM yyyy HH:mm", new Locale("id", "ID"));
+ return outputFormat.format(date);
+ }
+
+ private String getPaymentMethodDisplay() {
+ if (cardTypeFromIntent == null) return "Kartu Kredit";
+
+ switch (cardTypeFromIntent.toUpperCase()) {
+ case "EMV_MIDTRANS":
+ case "IC":
+ case "NFC":
+ return emvMode ? "Kartu Kredit (EMV)" : "Kartu Kredit";
+ case "DEBIT":
+ return emvMode ? "Kartu Debit (EMV)" : "Kartu Debit";
+ case "MAGNETIC":
+ return "Kartu Kredit";
+ default:
+ return "Kartu Kredit";
+ }
+ }
+
+ // ✅ ENHANCED: Comprehensive bank detection for EMV transactions
+ private String getCardTypeDisplay() {
+ Log.d(TAG, "=== DETERMINING CARD TYPE DISPLAY (EMV) ===");
+
+ // Priority 1: Get bank from Midtrans response (most accurate)
+ if (responseJsonData != null) {
+ Log.d(TAG, "✅ Midtrans response available, checking for bank...");
+
+ try {
+ String bankFromResponse = null;
+
+ if (responseJsonData.has("bank")) {
+ bankFromResponse = responseJsonData.getString("bank");
+ Log.d(TAG, "Found 'bank' field: '" + bankFromResponse + "'");
+ } else if (responseJsonData.has("issuer")) {
+ bankFromResponse = responseJsonData.getString("issuer");
+ Log.d(TAG, "Found 'issuer' field: '" + bankFromResponse + "'");
+ } else if (responseJsonData.has("acquiring_bank")) {
+ bankFromResponse = responseJsonData.getString("acquiring_bank");
+ Log.d(TAG, "Found 'acquiring_bank' field: '" + bankFromResponse + "'");
+ }
+
+ if (bankFromResponse != null && !bankFromResponse.trim().isEmpty()) {
+ String formattedBank = formatBankName(bankFromResponse);
+ Log.d(TAG, "✅ Bank from Midtrans response: '" + bankFromResponse + "' -> '" + formattedBank + "'");
+ return formattedBank;
+ }
+
+ } catch (JSONException e) {
+ Log.e(TAG, "❌ Error extracting bank from response: " + e.getMessage());
+ }
+ }
+
+ // Priority 2: EMV AID detection
+ Log.d(TAG, "Trying EMV AID detection...");
+ if (emvAid != null && !emvAid.trim().isEmpty()) {
+ String bankFromAid = getBankFromAid(emvAid);
+ if (!bankFromAid.equals("BCA")) { // If not default
+ Log.d(TAG, "✅ Bank from EMV AID: " + bankFromAid);
+ return bankFromAid;
+ }
+ }
+
+ // Priority 3: Card BIN detection
+ Log.d(TAG, "Trying Card BIN detection...");
+ if (cardNo != null && cardNo.length() >= 6) {
+ String cardBin = cardNo.substring(0, 6);
+ String bankFromBin = getBankFromComprehensiveBin(cardBin);
+ Log.d(TAG, "✅ Bank from BIN (" + cardBin + "): " + bankFromBin);
+ return bankFromBin;
+ }
+
+ Log.d(TAG, "⚠️ Using default bank: BCA");
+ Log.d(TAG, "====================================");
+ return "BCA"; // Default fallback
+ }
+
+ // ✅ ENHANCED: Better bank name formatting
+ private String formatBankName(String bankName) {
+ if (bankName == null || bankName.trim().isEmpty()) {
+ return "BCA"; // Default
+ }
+
+ String formatted = bankName.trim().toUpperCase();
+
+ // Handle common bank name variations
+ switch (formatted) {
+ case "BCA":
+ case "BANK BCA":
+ case "BANK CENTRAL ASIA":
+ return "BCA";
+
+ case "MANDIRI":
+ case "BANK MANDIRI":
+ return "Mandiri";
+
+ case "BNI":
+ case "BANK BNI":
+ case "BANK NEGARA INDONESIA":
+ return "BNI";
+
+ case "BRI":
+ case "BANK BRI":
+ case "BANK RAKYAT INDONESIA":
+ return "BRI";
+
+ case "CIMB":
+ case "CIMB NIAGA":
+ case "BANK CIMB NIAGA":
+ return "CIMB Niaga";
+
+ case "DANAMON":
+ case "BANK DANAMON":
+ return "Danamon";
+
+ case "PERMATA":
+ case "BANK PERMATA":
+ return "Permata";
+
+ default:
+ return capitalizeFirstLetter(bankName);
+ }
+ }
+
+ private String getBankFromAid(String aid) {
+ // AID to Indonesian bank mapping
+ if (aid.contains("A0000000031010")) {
+ // VISA - check if we have card number for better detection
+ if (cardNo != null && cardNo.length() >= 6) {
+ return getBankFromComprehensiveBin(cardNo.substring(0, 6));
+ }
+ return "BCA"; // Default for VISA
+ }
+
+ if (aid.contains("A0000000041010")) {
+ // MASTERCARD
+ if (cardNo != null && cardNo.length() >= 6) {
+ return getBankFromComprehensiveBin(cardNo.substring(0, 6));
+ }
+ return "Mandiri"; // Default for Mastercard
+ }
+
+ return "BCA"; // Ultimate fallback
+ }
+
+ // ✅ ENHANCED: Comprehensive Indonesian bank BIN mapping
+ private String getBankFromComprehensiveBin(String bin) {
+ if (bin == null || bin.length() < 4) {
+ return "BCA"; // Default
+ }
+
+ String bin4 = bin.substring(0, 4);
+ String bin6 = bin.length() >= 6 ? bin.substring(0, 6) : bin4;
+
+ // BCA patterns
+ if (bin4.equals("4621") || bin4.equals("4699") || bin4.equals("5221") || bin4.equals("6277")) {
+ return "BCA";
+ }
+
+ // MANDIRI patterns
+ if (bin4.equals("4313") || bin4.equals("5573") || bin4.equals("6011") || bin4.equals("6234")) {
+ return "Mandiri";
+ }
+
+ // BNI patterns
+ if (bin4.equals("4603") || bin4.equals("1946") || bin4.equals("5264")) {
+ return "BNI";
+ }
+
+ // BRI patterns
+ if (bin4.equals("4578") || bin4.equals("4479") || bin4.equals("5208")) {
+ return "BRI";
+ }
+
+ // CIMB NIAGA patterns
+ if (bin4.equals("4599") || bin4.equals("5249")) {
+ return "CIMB Niaga";
+ }
+
+ // DANAMON patterns
+ if (bin4.equals("4055") || bin4.equals("5108")) {
+ return "Danamon";
+ }
+
+ // Default fallback
+ Log.d(TAG, "Unknown BIN pattern: " + bin6 + ", using default BCA");
+ return "BCA";
+ }
+
+ private String capitalizeFirstLetter(String input) {
+ if (input == null || input.isEmpty()) {
+ return input;
+ }
+ return input.substring(0, 1).toUpperCase() + input.substring(1).toLowerCase();
+ }
+
+ // ✅ Debug methods
+ private void debugAllDataSources() {
+ Log.d(TAG, "=== DEBUGGING ALL DATA SOURCES ===");
+
+ if (responseJsonData != null) {
+ Log.d(TAG, "Midtrans Response Available:");
+ try {
+ java.util.Iterator keys = responseJsonData.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ Object value = responseJsonData.get(key);
+ Log.d(TAG, " " + key + ": " + value);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error iterating response: " + e.getMessage());
+ }
+ } else {
+ Log.d(TAG, "❌ No Midtrans Response Data");
+ }
+
+ Log.d(TAG, "EMV Data:");
+ Log.d(TAG, " Card Number: " + (cardNo != null ? maskCardNumber(cardNo) : "null"));
+ Log.d(TAG, " EMV AID: " + emvAid);
+ Log.d(TAG, " EMV Cardholder: " + emvCardholderName);
+ Log.d(TAG, " EMV Mode: " + emvMode);
+
+ Log.d(TAG, "==================================");
+ }
+
+ private void logTransactionDetails() {
+ Log.d(TAG, "=== RECEIPT DETAILS ===");
+ Log.d(TAG, "Reference ID: " + referenceId);
+ Log.d(TAG, "Card Number: " + (cardNo != null ? maskCardNumber(cardNo) : "N/A"));
+ Log.d(TAG, "Subtotal: " + subtotalAmount);
+ Log.d(TAG, "Tax: " + taxAmount);
+ Log.d(TAG, "Service Fee: " + serviceFeeAmount);
+ Log.d(TAG, "Final Total: " + (subtotalAmount + taxAmount + serviceFeeAmount));
+ Log.d(TAG, "======================");
+ }
+
+ // Action Methods
+ private void printReceipt() {
+ Log.d(TAG, "Print receipt requested");
+ showToast("Fitur cetak akan segera tersedia");
+ }
+
+ private void emailReceipt() {
+ Log.d(TAG, "Email receipt requested");
+
+ Intent emailIntent = new Intent(Intent.ACTION_SEND);
+ emailIntent.setType("text/plain");
+ emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Struk Pembayaran - " + extractTransactionNumberFromResponse());
+ emailIntent.putExtra(Intent.EXTRA_TEXT, generateEmailContent());
+
+ try {
+ startActivity(Intent.createChooser(emailIntent, "Kirim Email"));
+ } catch (Exception e) {
+ Log.e(TAG, "Error sending email: " + e.getMessage());
+ showToast("Tidak dapat mengirim email");
+ }
+ }
+
+ private String generateEmailContent() {
+ StringBuilder content = new StringBuilder();
+ NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
+
+ content.append("STRUK PEMBAYARAN EMV/CARD\n");
+ content.append("==========================\n\n");
+ content.append("TOKO KLONTONG PAK EKO\n");
+ content.append("Ciputat Baru, Tangsel\n\n");
+ content.append("TID: ").append(extractTidFromResponse()).append("\n");
+ content.append("Nomor Transaksi: ").append(extractTransactionNumberFromResponse()).append("\n");
+ content.append("Tanggal: ").append(formatTransactionDate()).append("\n");
+ content.append("Metode: ").append(getPaymentMethodDisplay()).append("\n");
+ content.append("Jenis Kartu: ").append(getCardTypeDisplay()).append("\n\n");
+
+ if (emvMode && emvCardholderName != null) {
+ content.append("DETAIL EMV:\n");
+ content.append("Cardholder: ").append(emvCardholderName).append("\n");
+ content.append("AID: ").append(emvAid).append("\n\n");
+ }
+
+ content.append("RINCIAN PEMBAYARAN:\n");
+ content.append("Total Transaksi: Rp ").append(formatter.format(subtotalAmount)).append("\n");
+ content.append("Pajak (11%): Rp ").append(formatter.format(taxAmount)).append("\n");
+ content.append("Biaya Layanan: Rp ").append(formatter.format(serviceFeeAmount)).append("\n");
+ content.append("------------------------\n");
+ content.append("TOTAL: Rp ").append(formatter.format(subtotalAmount + taxAmount + serviceFeeAmount)).append("\n");
+ content.append("\nTerima kasih atas pembayaran Anda!");
+
+ return content.toString();
+ }
+
+ // Navigation Methods
+ private void navigateBack() {
+ if (isNavigating) return;
+
+ Log.d(TAG, "Navigating back");
+ isNavigating = true;
+ finish();
+ }
+
+ private void navigateToNewTransaction() {
+ if (isNavigating) return;
+
+ new AlertDialog.Builder(this)
+ .setTitle("Transaksi Baru")
+ .setMessage("Apakah Anda ingin melakukan transaksi baru?")
+ .setPositiveButton("Ya", (dialog, which) -> {
+ performNavigateToNewTransaction();
+ })
+ .setNegativeButton("Tidak", null)
+ .show();
+ }
+
+ private void performNavigateToNewTransaction() {
+ Log.d(TAG, "=== NAVIGATING TO NEW TRANSACTION ===");
+ isNavigating = true;
+
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ try {
+ Intent intent = new Intent(this, CreateTransactionActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ } catch (Exception e) {
+ Log.e(TAG, "Error navigating to new transaction: " + e.getMessage());
+ isNavigating = false;
+ showToast("Gagal membuka transaksi baru");
+ }
+ }, 300);
+ }
+
+ // Helper Methods
+ private String maskCardNumber(String cardNumber) {
+ if (cardNumber == null || cardNumber.length() < 8) {
+ return cardNumber;
+ }
+ String first4 = cardNumber.substring(0, 4);
+ String last4 = cardNumber.substring(cardNumber.length() - 4);
+ return first4 + "****" + last4;
+ }
+
+ private void showToast(String message) {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (isNavigating) {
+ return;
+ }
+ navigateBack();
+ super.onBackPressed();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.d(TAG, "ResultTransactionActivity destroyed");
+ super.onDestroy();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/transaction/managers/CardScannerManager.java b/app/src/main/java/com/example/bdkipoc/transaction/managers/CardScannerManager.java
new file mode 100644
index 0000000..39889da
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/transaction/managers/CardScannerManager.java
@@ -0,0 +1,258 @@
+package com.example.bdkipoc.transaction.managers;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.example.bdkipoc.MyApplication;
+import com.sunmi.pay.hardware.aidl.AidlConstants.CardType;
+import com.sunmi.pay.hardware.aidlv2.AidlConstantsV2;
+import com.sunmi.pay.hardware.aidlv2.readcard.CheckCardCallbackV2;
+
+/**
+ * CardScannerManager - Handles card detection for both EMV and Simple modes
+ */
+public class CardScannerManager {
+ private static final String TAG = "CardScannerManager";
+
+ private CardScannerCallback callback;
+ private boolean isProcessing = false;
+
+ public interface CardScannerCallback {
+ void onCardDetected(String cardType, Bundle cardData);
+ void onEMVCardDetected(int cardType);
+ void onScanError(String errorMessage);
+ void onScanProgress(String message);
+ }
+
+ public CardScannerManager(CardScannerCallback callback) {
+ this.callback = callback;
+ }
+
+ public void startScanning(boolean isEMVMode) {
+ if (isProcessing) {
+ Log.d(TAG, "Card check already in progress - ignoring call");
+ return;
+ }
+
+ Log.d(TAG, "Starting card check - setting isProcessing = true");
+ isProcessing = true;
+
+ try {
+ // Small delay to ensure everything is ready
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ if (isProcessing) {
+ if (isEMVMode) {
+ startEMVCardCheck();
+ } else {
+ startSimpleCardCheck();
+ }
+ }
+ }, 500);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error in startScanning: " + e.getMessage(), e);
+ handleScanError("Error: " + e.getMessage());
+ }
+ }
+
+ public void stopScanning() {
+ try {
+ if (MyApplication.app != null && MyApplication.app.readCardOptV2 != null) {
+ MyApplication.app.readCardOptV2.cancelCheckCard();
+ }
+ isProcessing = false;
+ Log.d(TAG, "Card scanning stopped");
+ } catch (Exception e) {
+ Log.e(TAG, "Error stopping card check: " + e.getMessage());
+ }
+ }
+
+ public boolean isScanning() {
+ return isProcessing;
+ }
+
+ private void startEMVCardCheck() {
+ try {
+ if (callback != null) {
+ callback.onScanProgress("EMV Mode: Starting card scan...");
+ }
+
+ int cardType = AidlConstantsV2.CardType.NFC.getValue() | AidlConstantsV2.CardType.IC.getValue();
+ Log.d(TAG, "Starting EMV checkCard with cardType: " + cardType);
+
+ MyApplication.app.readCardOptV2.checkCard(cardType, mEMVCheckCardCallback, 60);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(TAG, "Error in startEMVCardCheck: " + e.getMessage());
+ handleScanError("Error starting EMV card scan: " + e.getMessage());
+ }
+ }
+
+ private void startSimpleCardCheck() {
+ try {
+ if (!MyApplication.app.isConnectPaySDK()) {
+ if (callback != null) {
+ callback.onScanProgress("Connecting to PaySDK...");
+ }
+ MyApplication.app.bindPaySDKService();
+ return;
+ }
+
+ if (callback != null) {
+ callback.onScanProgress("Simple Mode: Starting card scan...");
+ }
+
+ int cardType = CardType.MAGNETIC.getValue() | CardType.IC.getValue() | CardType.NFC.getValue();
+
+ MyApplication.app.readCardOptV2.checkCard(cardType, mSimpleCheckCardCallback, 60);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ handleScanError("Error starting card scan: " + e.getMessage());
+ }
+ }
+
+ private void handleScanError(String errorMessage) {
+ Log.e(TAG, "Scan error: " + errorMessage);
+ isProcessing = false;
+
+ if (callback != null) {
+ callback.onScanError(errorMessage);
+ }
+ }
+
+ public void resetScanning() {
+ Log.d(TAG, "Resetting scanning state");
+ isProcessing = false;
+ }
+
+ // Simple Card Detection Callback
+ private final CheckCardCallbackV2 mSimpleCheckCardCallback = new CheckCardCallbackV2.Stub() {
+ @Override
+ public void findMagCard(Bundle info) throws RemoteException {
+ Log.d(TAG, "Simple Mode: findMagCard callback triggered");
+ isProcessing = false;
+ if (callback != null) {
+ callback.onCardDetected("MAGNETIC", info);
+ }
+ }
+
+ @Override
+ public void findICCard(String atr) throws RemoteException {
+ Bundle info = new Bundle();
+ info.putString("atr", atr);
+ isProcessing = false;
+ if (callback != null) {
+ callback.onCardDetected("IC", info);
+ }
+ }
+
+ @Override
+ public void findRFCard(String uuid) throws RemoteException {
+ Bundle info = new Bundle();
+ info.putString("uuid", uuid);
+ isProcessing = false;
+ if (callback != null) {
+ callback.onCardDetected("NFC", info);
+ }
+ }
+
+ @Override
+ public void onError(int code, String message) throws RemoteException {
+ isProcessing = false;
+ if (callback != null) {
+ callback.onScanError("Card error: " + message);
+ }
+ }
+
+ @Override
+ public void findICCardEx(Bundle info) throws RemoteException {
+ isProcessing = false;
+ if (callback != null) {
+ callback.onCardDetected("IC", info);
+ }
+ }
+
+ @Override
+ public void findRFCardEx(Bundle info) throws RemoteException {
+ isProcessing = false;
+ if (callback != null) {
+ callback.onCardDetected("NFC", info);
+ }
+ }
+
+ @Override
+ public void onErrorEx(Bundle info) throws RemoteException {
+ isProcessing = false;
+ String msg = info.getString("message", "Unknown error");
+ if (callback != null) {
+ callback.onScanError("Card error: " + msg);
+ }
+ }
+ };
+
+ // EMV Card Detection Callback
+ private final CheckCardCallbackV2 mEMVCheckCardCallback = new CheckCardCallbackV2.Stub() {
+ @Override
+ public void findMagCard(Bundle info) throws RemoteException {
+ isProcessing = false;
+ if (callback != null) {
+ callback.onCardDetected("MAGNETIC", info);
+ }
+ }
+
+ @Override
+ public void findICCard(String atr) throws RemoteException {
+ MyApplication.app.basicOptV2.buzzerOnDevice(1, 2750, 200, 0);
+ isProcessing = false;
+ if (callback != null) {
+ callback.onEMVCardDetected(AidlConstantsV2.CardType.IC.getValue());
+ }
+ }
+
+ @Override
+ public void findRFCard(String uuid) throws RemoteException {
+ isProcessing = false;
+ if (callback != null) {
+ callback.onEMVCardDetected(AidlConstantsV2.CardType.NFC.getValue());
+ }
+ }
+
+ @Override
+ public void onError(int code, String message) throws RemoteException {
+ isProcessing = false;
+ if (callback != null) {
+ callback.onScanError("EMV Error: " + message);
+ }
+ }
+
+ @Override
+ public void findICCardEx(Bundle info) throws RemoteException {
+ isProcessing = false;
+ if (callback != null) {
+ callback.onEMVCardDetected(AidlConstantsV2.CardType.IC.getValue());
+ }
+ }
+
+ @Override
+ public void findRFCardEx(Bundle info) throws RemoteException {
+ isProcessing = false;
+ if (callback != null) {
+ callback.onEMVCardDetected(AidlConstantsV2.CardType.NFC.getValue());
+ }
+ }
+
+ @Override
+ public void onErrorEx(Bundle info) throws RemoteException {
+ isProcessing = false;
+ String msg = info.getString("message", "Unknown error");
+ if (callback != null) {
+ callback.onScanError("EMV Error: " + msg);
+ }
+ }
+ };
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/transaction/managers/EMVManager.java b/app/src/main/java/com/example/bdkipoc/transaction/managers/EMVManager.java
new file mode 100644
index 0000000..9e07462
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/transaction/managers/EMVManager.java
@@ -0,0 +1,417 @@
+package com.example.bdkipoc.transaction.managers;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.example.bdkipoc.MyApplication;
+import com.sunmi.pay.hardware.aidlv2.AidlConstantsV2;
+import com.sunmi.pay.hardware.aidlv2.bean.EMVCandidateV2;
+import com.sunmi.pay.hardware.aidlv2.emv.EMVListenerV2;
+import com.sunmi.pay.hardware.aidlv2.emv.EMVOptV2;
+
+import java.util.List;
+
+/**
+ * EMVManager - Handles all EMV related operations
+ */
+public class EMVManager {
+ private static final String TAG = "EMVManager";
+
+ private EMVOptV2 mEMVOptV2;
+ private EMVManagerCallback callback;
+
+ // EMV Process Variables
+ private int mCardType;
+ private String mCardNo;
+ private int mPinType;
+ private String mCertInfo;
+ private int mProcessStep;
+
+ public interface EMVManagerCallback {
+ void onAppSelect(String[] candidateNames);
+ void onFinalAppSelect();
+ void onConfirmCardNo(String cardNo);
+ void onCertVerify(String certInfo);
+ void onShowPinPad(int pinType);
+ void onOnlineProcess();
+ void onSignature();
+ void onTransactionSuccess(int code, String desc);
+ void onTransactionFailed(int code, String desc);
+ }
+
+ public EMVManager(EMVManagerCallback callback) {
+ this.callback = callback;
+ initEMVComponents();
+ }
+
+ private void initEMVComponents() {
+ if (MyApplication.app != null) {
+ mEMVOptV2 = MyApplication.app.emvOptV2;
+ Log.d(TAG, "EMV components initialized");
+ } else {
+ Log.e(TAG, "MyApplication.app is null");
+ }
+ }
+
+ public void initEMVData() {
+ try {
+ if (mEMVOptV2 != null) {
+ mEMVOptV2.initEmvProcess();
+
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ try {
+ initEmvTlvData();
+ Log.d(TAG, "EMV data initialized successfully");
+ } catch (Exception e) {
+ Log.e(TAG, "Error in delayed EMV init: " + e.getMessage());
+ }
+ }, 500);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error initializing EMV data: " + e.getMessage());
+ }
+ }
+
+ private void initEmvTlvData() {
+ try {
+ // Set PayPass (MasterCard) TLV data
+ String[] tagsPayPass = {"DF8117", "DF8118", "DF8119", "DF811F", "DF811E", "DF812C",
+ "DF8123", "DF8124", "DF8125", "DF8126", "DF811B", "DF811D", "DF8122", "DF8120", "DF8121"};
+ String[] valuesPayPass = {"E0", "F8", "F8", "E8", "00", "00",
+ "000000000000", "000000100000", "999999999999", "000000100000",
+ "30", "02", "0000000000", "000000000000", "000000000000"};
+ mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_PAYPASS, tagsPayPass, valuesPayPass);
+
+ // Set AMEX TLV data
+ String[] tagsAE = {"9F6D", "9F6E", "9F33", "9F35", "DF8168", "DF8167", "DF8169", "DF8170"};
+ String[] valuesAE = {"C0", "D8E00000", "E0E888", "22", "00", "00", "00", "60"};
+ mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_AE, tagsAE, valuesAE);
+
+ // Set JCB TLV data
+ String[] tagsJCB = {"9F53", "DF8161"};
+ String[] valuesJCB = {"708000", "7F00"};
+ mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_JCB, tagsJCB, valuesJCB);
+
+ // Set DPAS TLV data
+ String[] tagsDPAS = {"9F66"};
+ String[] valuesDPAS = {"B600C000"};
+ mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_DPAS, tagsDPAS, valuesDPAS);
+
+ // Set Flash TLV data
+ String[] tagsFLASH = {"9F58", "9F59", "9F5A", "9F5D", "9F5E"};
+ String[] valuesFLASH = {"03", "D88700", "00", "000000000000", "E000"};
+ mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_FLASH, tagsFLASH, valuesFLASH);
+
+ // Set Pure TLV data
+ String[] tagsPURE = {"DF7F", "DF8134", "DF8133"};
+ String[] valuesPURE = {"A0000007271010", "DF", "36006043F9"};
+ mEMVOptV2.setTlvList(AidlConstantsV2.EMV.TLVOpCode.OP_PURE, tagsPURE, valuesPURE);
+
+ Bundle bundle = new Bundle();
+ bundle.putBoolean("optOnlineRes", true);
+ mEMVOptV2.setTermParamEx(bundle);
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error setting EMV TLV data: " + e.getMessage());
+ }
+ }
+
+ public void startEMVTransaction(String transactionAmount, int cardType) {
+ if (mProcessStep != 0) {
+ Log.d(TAG, "EMV transaction already in progress (step: " + mProcessStep + ") - ignoring call");
+ return;
+ }
+
+ Log.d(TAG, "Starting EMV transaction process");
+ mProcessStep = 1;
+ mCardType = cardType;
+
+ try {
+ mEMVOptV2.initEmvProcess();
+
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ try {
+ if (mProcessStep <= 0) {
+ Log.d(TAG, "EMV process was cancelled - not starting");
+ return;
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putString("amount", transactionAmount);
+ bundle.putString("transType", "00");
+ bundle.putInt("flowType", AidlConstantsV2.EMV.FlowType.TYPE_EMV_STANDARD);
+ bundle.putInt("cardType", mCardType);
+
+ Log.d(TAG, "Starting transactProcessEx with reset EMV");
+ mEMVOptV2.transactProcessEx(bundle, mEMVListener);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error in delayed EMV start: " + e.getMessage(), e);
+ if (callback != null) {
+ callback.onTransactionFailed(-1, "Error starting EMV: " + e.getMessage());
+ }
+ }
+ }, 300);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error starting EMV transaction: " + e.getMessage());
+ if (callback != null) {
+ callback.onTransactionFailed(-1, "Error starting EMV transaction: " + e.getMessage());
+ }
+ }
+ }
+
+ public void resetEMVProcess() {
+ try {
+ if (mEMVOptV2 != null) {
+ mEMVOptV2.initEmvProcess();
+ }
+ mProcessStep = 0;
+ mCardNo = null;
+ Log.d(TAG, "EMV process reset");
+ } catch (Exception e) {
+ Log.e(TAG, "Error resetting EMV process: " + e.getMessage());
+ }
+ }
+
+ // EMV Import Methods
+ public void importAppSelect(int selectIndex) {
+ try {
+ mEMVOptV2.importAppSelect(selectIndex);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void importFinalAppSelectStatus(int status) {
+ try {
+ mEMVOptV2.importAppFinalSelectStatus(status);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void importCardNoStatus(int status) {
+ try {
+ mEMVOptV2.importCardNoStatus(status);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void importCertStatus(int status) {
+ try {
+ mEMVOptV2.importCertStatus(status);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void importPinInputStatus(int inputResult) {
+ try {
+ mEMVOptV2.importPinInputStatus(mPinType, inputResult);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void importSignatureStatus(int status) {
+ try {
+ mEMVOptV2.importSignatureStatus(status);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void mockOnlineProcess() {
+ new Thread(() -> {
+ try {
+ Thread.sleep(2000);
+
+ try {
+ String[] tags = {"71", "72", "91", "8A", "89"};
+ String[] values = {"", "", "", "", ""};
+ byte[] out = new byte[1024];
+ int len = mEMVOptV2.importOnlineProcStatus(0, tags, values, out);
+ if (len < 0) {
+ Log.e(TAG, "importOnlineProcessStatus error,code:" + len);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }).start();
+ }
+
+ // Getters
+ public String getCardNo() {
+ return mCardNo;
+ }
+
+ public int getCardType() {
+ return mCardType;
+ }
+
+ public int getPinType() {
+ return mPinType;
+ }
+
+ // Helper Methods
+ public String maskCardNumber(String cardNo) {
+ if (cardNo == null || cardNo.length() < 8) {
+ return cardNo;
+ }
+ String first4 = cardNo.substring(0, 4);
+ String last4 = cardNo.substring(cardNo.length() - 4);
+ StringBuilder middle = new StringBuilder();
+ for (int i = 0; i < cardNo.length() - 8; i++) {
+ middle.append("*");
+ }
+ return first4 + middle.toString() + last4;
+ }
+
+ public String[] getCandidateNames(List candiList) {
+ if (candiList == null || candiList.size() == 0) return new String[0];
+ String[] result = new String[candiList.size()];
+ for (int i = 0; i < candiList.size(); i++) {
+ EMVCandidateV2 candi = candiList.get(i);
+ String name = candi.appPreName;
+ name = TextUtils.isEmpty(name) ? candi.appLabel : name;
+ name = TextUtils.isEmpty(name) ? candi.appName : name;
+ name = TextUtils.isEmpty(name) ? "" : name;
+ result[i] = name;
+ }
+ return result;
+ }
+
+ // EMV Listener
+ private final EMVListenerV2 mEMVListener = new EMVListenerV2.Stub() {
+
+ @Override
+ public void onWaitAppSelect(List appNameList, boolean isFirstSelect) throws RemoteException {
+ Log.d(TAG, "onWaitAppSelect isFirstSelect:" + isFirstSelect);
+ mProcessStep = 1; // EMV_APP_SELECT
+ String[] candidateNames = getCandidateNames(appNameList);
+ if (callback != null) {
+ callback.onAppSelect(candidateNames);
+ }
+ }
+
+ @Override
+ public void onAppFinalSelect(String tag9F06Value) throws RemoteException {
+ Log.d(TAG, "onAppFinalSelect tag9F06Value:" + tag9F06Value);
+ mProcessStep = 2; // EMV_FINAL_APP_SELECT
+ if (callback != null) {
+ callback.onFinalAppSelect();
+ }
+ }
+
+ @Override
+ public void onConfirmCardNo(String cardNo) throws RemoteException {
+ Log.d(TAG, "onConfirmCardNo cardNo:" + maskCardNumber(cardNo));
+ mCardNo = cardNo;
+ mProcessStep = 3; // EMV_CONFIRM_CARD_NO
+ if (callback != null) {
+ callback.onConfirmCardNo(cardNo);
+ }
+ }
+
+ @Override
+ public void onRequestShowPinPad(int pinType, int remainTime) throws RemoteException {
+ Log.d(TAG, "onRequestShowPinPad pinType:" + pinType + " remainTime:" + remainTime);
+ mPinType = pinType;
+ mProcessStep = 5; // EMV_SHOW_PIN_PAD
+ if (callback != null) {
+ callback.onShowPinPad(pinType);
+ }
+ }
+
+ @Override
+ public void onRequestSignature() throws RemoteException {
+ Log.d(TAG, "onRequestSignature");
+ mProcessStep = 7; // EMV_SIGNATURE
+ if (callback != null) {
+ callback.onSignature();
+ }
+ }
+
+ @Override
+ public void onCertVerify(int certType, String certInfo) throws RemoteException {
+ Log.d(TAG, "onCertVerify certType:" + certType + " certInfo:" + certInfo);
+ mCertInfo = certInfo;
+ mProcessStep = 4; // EMV_CERT_VERIFY
+ if (callback != null) {
+ callback.onCertVerify(certInfo);
+ }
+ }
+
+ @Override
+ public void onOnlineProc() throws RemoteException {
+ Log.d(TAG, "onOnlineProcess");
+ mProcessStep = 6; // EMV_ONLINE_PROCESS
+ if (callback != null) {
+ callback.onOnlineProcess();
+ }
+ }
+
+ @Override
+ public void onCardDataExchangeComplete() throws RemoteException {
+ Log.d(TAG, "onCardDataExchangeComplete");
+ if (mCardType == AidlConstantsV2.CardType.NFC.getValue()) {
+ MyApplication.app.basicOptV2.buzzerOnDevice(1, 2750, 200, 0);
+ }
+ }
+
+ @Override
+ public void onTransResult(int code, String desc) throws RemoteException {
+ Log.d(TAG, "onTransResult code:" + code + " desc:" + desc);
+
+ if (code == 1 || code == 2 || code == 5 || code == 6) {
+ if (callback != null) {
+ callback.onTransactionSuccess(code, desc);
+ }
+ } else {
+ if (callback != null) {
+ callback.onTransactionFailed(code, desc);
+ }
+ }
+ }
+
+ @Override
+ public void onConfirmationCodeVerified() throws RemoteException {
+ Log.d(TAG, "onConfirmationCodeVerified");
+ }
+
+ @Override
+ public void onRequestDataExchange(String cardNo) throws RemoteException {
+ Log.d(TAG, "onRequestDataExchange,cardNo:" + cardNo);
+ mEMVOptV2.importDataExchangeStatus(0);
+ }
+
+ @Override
+ public void onTermRiskManagement() throws RemoteException {
+ Log.d(TAG, "onTermRiskManagement");
+ mEMVOptV2.importTermRiskManagementStatus(0);
+ }
+
+ @Override
+ public void onPreFirstGenAC() throws RemoteException {
+ Log.d(TAG, "onPreFirstGenAC");
+ mEMVOptV2.importPreFirstGenACStatus(0);
+ }
+
+ @Override
+ public void onDataStorageProc(String[] containerID, String[] containerContent) throws RemoteException {
+ Log.d(TAG, "onDataStorageProc");
+ String[] tags = new String[0];
+ String[] values = new String[0];
+ mEMVOptV2.importDataStorage(tags, values);
+ }
+ };
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/transaction/managers/MidtransCardPaymentManager.java b/app/src/main/java/com/example/bdkipoc/transaction/managers/MidtransCardPaymentManager.java
new file mode 100644
index 0000000..5566b16
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/transaction/managers/MidtransCardPaymentManager.java
@@ -0,0 +1,1408 @@
+package com.example.bdkipoc.transaction.managers;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+
+/**
+ * MidtransCardPaymentManager - Enhanced Version with Backend Integration
+ *
+ * Key Features:
+ * - Uses static CVV "493" for all transactions (both EMV and regular cards)
+ * - EMV-first approach with tokenization fallback
+ * - Supports custom order_id from backend transaction_uuid
+ * - Handles Midtrans API requirements where CVV is mandatory even for EMV
+ * - Comprehensive error handling and retry logic
+ * - Enhanced response processing with bank detection
+ */
+public class MidtransCardPaymentManager {
+ private static final String TAG = "MidtransCardPayment";
+
+ // Midtrans Configuration
+ private static final String MIDTRANS_BASE_URL = "https://api.sandbox.midtrans.com";
+ private static final String MIDTRANS_TOKEN_URL = MIDTRANS_BASE_URL + "/v2/token";
+ private static final String MIDTRANS_CHARGE_URL = MIDTRANS_BASE_URL + "/v2/charge";
+ private static final String MIDTRANS_CLIENT_KEY = "SB-Mid-client-zPs7DafB_fag5kOP";
+ private static final String MIDTRANS_SERVER_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc6";
+
+ // EMV-specific configuration
+ private static final String STATIC_CVV = "493"; // Static CVV for all tokenization (as per curl example)
+
+ private Context context;
+ private MidtransCardPaymentCallback callback;
+ private int retryCount = 0;
+ private static final int MAX_RETRY = 2;
+
+ // ✅ ENHANCED: Store last response and custom order ID
+ private JSONObject lastResponse;
+ private String lastErrorMessage;
+ private String customOrderId; // ✅ NEW: For backend transaction_uuid
+
+ public interface MidtransCardPaymentCallback {
+ void onTokenizeSuccess(String cardToken);
+ void onTokenizeError(String errorMessage);
+ void onChargeSuccess(JSONObject chargeResponse);
+ void onChargeError(String errorMessage);
+ void onPaymentProgress(String message);
+ }
+
+ public MidtransCardPaymentManager(Context context, MidtransCardPaymentCallback callback) {
+ this.context = context;
+ this.callback = callback;
+ }
+
+ // ✅ ENHANCED: Getter for last response
+ public JSONObject getLastResponse() {
+ return lastResponse;
+ }
+
+ // ✅ ENHANCED: Getter for last error message
+ public String getLastErrorMessage() {
+ return lastErrorMessage;
+ }
+
+ /**
+ * ✅ NEW: Process EMV card payment with custom order ID (transaction_uuid from backend)
+ */
+ public void processEMVCardPaymentWithOrderId(CardData cardData, long amount, String orderId, String emvData) {
+ this.customOrderId = orderId; // Store custom order ID
+
+ Log.d(TAG, "=== PROCESSING EMV PAYMENT WITH CUSTOM ORDER ID ===");
+ Log.d(TAG, "Custom Order ID (Transaction UUID): " + orderId);
+ Log.d(TAG, "Amount: " + amount);
+ Log.d(TAG, "Card PAN: " + maskCardNumber(cardData.getPan()));
+ Log.d(TAG, "Payment Mode: EMV with Backend Integration");
+ Log.d(TAG, "================================================");
+
+ // Reset retry counter
+ retryCount = 0;
+
+ if (callback != null) {
+ callback.onPaymentProgress("Processing EMV payment with backend UUID...");
+ }
+
+ // Use direct charge with custom order ID
+ new EMVDirectChargeWithOrderIdTask(cardData, amount, orderId, emvData).execute();
+ }
+
+ /**
+ * Process EMV card payment - handles EMV-specific requirements
+ */
+ public void processEMVCardPayment(CardData cardData, long amount, String referenceId, String emvData) {
+ if (cardData == null || !cardData.isValid()) {
+ if (callback != null) {
+ callback.onChargeError("Invalid card data");
+ }
+ return;
+ }
+
+ // Reset retry counter and custom order ID
+ retryCount = 0;
+ customOrderId = null;
+
+ Log.d(TAG, "=== STARTING EMV MIDTRANS PAYMENT ===");
+ Log.d(TAG, "Reference ID: " + referenceId);
+ Log.d(TAG, "Amount: " + amount);
+ Log.d(TAG, "Card PAN: " + maskCardNumber(cardData.getPan()));
+ Log.d(TAG, "Payment Mode: EMV with static CVV (" + STATIC_CVV + ")");
+ Log.d(TAG, "==========================================");
+
+ if (callback != null) {
+ callback.onPaymentProgress("Processing EMV payment...");
+ }
+
+ // For EMV cards, try direct charge without tokenization first
+ new EMVDirectChargeTask(cardData, amount, referenceId, emvData).execute();
+ }
+
+ /**
+ * Process regular card payment with tokenization
+ */
+ public void processCardPayment(CardData cardData, long amount, String referenceId) {
+ if (cardData == null || !cardData.isValid()) {
+ if (callback != null) {
+ callback.onChargeError("Invalid card data");
+ }
+ return;
+ }
+
+ retryCount = 0;
+ customOrderId = null;
+
+ Log.d(TAG, "=== STARTING REGULAR CARD PAYMENT ===");
+ Log.d(TAG, "Using tokenization flow");
+
+ if (callback != null) {
+ callback.onPaymentProgress("Tokenizing card...");
+ }
+
+ // Use tokenization flow for regular cards
+ new TokenizeCardTask(cardData, amount, referenceId).execute();
+ }
+
+ /**
+ * ✅ NEW: EMV Direct Charge with Custom Order ID - uses backend transaction_uuid
+ */
+ private class EMVDirectChargeWithOrderIdTask extends AsyncTask {
+ private CardData cardData;
+ private long amount;
+ private String orderId;
+ private String emvData;
+ private String errorMessage;
+ private JSONObject chargeResponse;
+
+ public EMVDirectChargeWithOrderIdTask(CardData cardData, long amount, String orderId, String emvData) {
+ this.cardData = cardData;
+ this.amount = amount;
+ this.orderId = orderId;
+ this.emvData = emvData;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ try {
+ // Build EMV-specific charge payload with custom order ID
+ JSONObject payload = new JSONObject();
+ payload.put("payment_type", "credit_card");
+
+ // Transaction details with custom order ID
+ JSONObject transactionDetails = new JSONObject();
+ transactionDetails.put("order_id", orderId); // ✅ Use backend transaction_uuid
+ transactionDetails.put("gross_amount", amount);
+ payload.put("transaction_details", transactionDetails);
+
+ // EMV Credit card data (no tokenization)
+ JSONObject creditCard = new JSONObject();
+ creditCard.put("card_number", cardData.getPan());
+ creditCard.put("card_exp_month", cardData.getExpiryMonth());
+ creditCard.put("card_exp_year", cardData.getExpiryYear());
+
+ // Include static CVV even for EMV (Midtrans may require it)
+ creditCard.put("card_cvv", STATIC_CVV);
+ Log.d(TAG, "EMV Transaction: Including static CVV (" + STATIC_CVV + ") for Midtrans compatibility");
+
+ // Add EMV data if available
+ if (emvData != null && !emvData.isEmpty()) {
+ creditCard.put("emv_data", emvData);
+ creditCard.put("authentication_mode", "chip");
+ }
+
+ payload.put("credit_card", creditCard);
+
+ // Item details
+ JSONArray itemDetails = new JSONArray();
+ JSONObject item = new JSONObject();
+ item.put("id", "emv_backend1");
+ item.put("price", amount);
+ item.put("quantity", 1);
+ item.put("name", "EMV Backend Transaction");
+ item.put("brand", "EMV Backend Payment");
+ item.put("category", "Backend Transaction");
+ item.put("merchant_name", "EDC-Store-Backend");
+ itemDetails.put(item);
+ payload.put("item_details", itemDetails);
+
+ // Customer details (same as curl example)
+ addCustomerDetails(payload);
+
+ Log.d(TAG, "=== EMV BACKEND DIRECT CHARGE ===");
+ Log.d(TAG, "Order ID (Backend UUID): " + orderId);
+ Log.d(TAG, "Amount: " + amount);
+ Log.d(TAG, "Card: " + maskCardNumber(cardData.getPan()));
+ Log.d(TAG, "Mode: EMV Backend Direct (No Token)");
+ Log.d(TAG, "=================================");
+
+ // Make charge request
+ return makeChargeRequest(payload);
+
+ } catch (Exception e) {
+ Log.e(TAG, "EMV Backend Direct Charge exception: " + e.getMessage(), e);
+ errorMessage = "EMV backend payment error: " + e.getMessage();
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ if (success && chargeResponse != null && callback != null) {
+ Log.d(TAG, "✅ EMV Backend charge successful!");
+ callback.onChargeSuccess(chargeResponse);
+ } else if (callback != null) {
+ // Fallback to tokenization if direct charge fails
+ Log.w(TAG, "EMV backend direct charge failed, trying tokenization fallback...");
+ callback.onPaymentProgress("Retrying with tokenization...");
+ new TokenizeCardWithOrderIdTask(cardData, amount, orderId).execute();
+ }
+ }
+
+ private Boolean makeChargeRequest(JSONObject payload) {
+ try {
+ 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_SERVER_AUTH);
+ 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, "EMV Backend Charge response code: " + responseCode);
+
+ BufferedReader br;
+ StringBuilder response = new StringBuilder();
+ String responseLine;
+
+ if (responseCode == 200 || responseCode == 201) {
+ br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ } else {
+ br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
+ }
+
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ String responseString = response.toString();
+ Log.d(TAG, "EMV Backend Charge response: " + responseString);
+
+ // Store response for debugging
+ try {
+ chargeResponse = new JSONObject(responseString);
+ lastResponse = chargeResponse;
+
+ // Enhanced: Add bank detection if missing
+ enhanceResponseWithBankInfo(chargeResponse);
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error parsing response JSON: " + e.getMessage());
+ chargeResponse = createFallbackResponse(responseString, responseCode);
+ lastResponse = chargeResponse;
+ }
+
+ return processChargeResponse(chargeResponse, responseCode);
+
+ } catch (Exception e) {
+ Log.e(TAG, "EMV Backend Charge request exception: " + e.getMessage(), e);
+ errorMessage = "Network error: " + e.getMessage();
+ lastErrorMessage = errorMessage;
+ return false;
+ }
+ }
+
+ private Boolean processChargeResponse(JSONObject response, int httpCode) {
+ try {
+ String statusCode = response.optString("status_code", "");
+ String statusMessage = response.optString("status_message", "");
+ String transactionStatus = response.optString("transaction_status", "");
+ String fraudStatus = response.optString("fraud_status", "");
+
+ Log.d(TAG, "=== BACKEND CHARGE RESPONSE ANALYSIS ===");
+ Log.d(TAG, "HTTP Code: " + httpCode);
+ Log.d(TAG, "Status Code: " + statusCode);
+ Log.d(TAG, "Status Message: " + statusMessage);
+ Log.d(TAG, "Transaction Status: " + transactionStatus);
+ Log.d(TAG, "Fraud Status: " + fraudStatus);
+ Log.d(TAG, "Order ID: " + response.optString("order_id", ""));
+ Log.d(TAG, "========================================");
+
+ // Handle specific error cases
+ if ("411".equals(statusCode)) {
+ errorMessage = "Token expired: " + statusMessage;
+ return false;
+ } else if ("400".equals(statusCode)) {
+ errorMessage = "Bad request: " + statusMessage;
+ return false;
+ } else if ("202".equals(statusCode) && "deny".equals(transactionStatus)) {
+ errorMessage = "Transaction denied: " + statusMessage;
+ return false;
+ } else if (httpCode != 200 && httpCode != 201) {
+ errorMessage = "HTTP error: " + httpCode + " - " + statusMessage;
+ return false;
+ }
+
+ // Success conditions
+ if ("200".equals(statusCode) &&
+ ("capture".equals(transactionStatus) ||
+ "settlement".equals(transactionStatus) ||
+ "pending".equals(transactionStatus))) {
+ return true;
+ } else if ("201".equals(statusCode)) {
+ return true;
+ } else {
+ errorMessage = "Transaction failed: " + statusMessage;
+ return false;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error processing backend charge response: " + e.getMessage(), e);
+ errorMessage = "Response processing error: " + e.getMessage();
+ return false;
+ }
+ }
+ }
+
+ /**
+ * ✅ NEW: Tokenize Card with Custom Order ID - fallback method
+ */
+ private class TokenizeCardWithOrderIdTask extends AsyncTask {
+ private CardData cardData;
+ private long amount;
+ private String orderId;
+ private String errorMessage;
+
+ public TokenizeCardWithOrderIdTask(CardData cardData, long amount, String orderId) {
+ this.cardData = cardData;
+ this.amount = amount;
+ this.orderId = orderId;
+ }
+
+ @Override
+ protected String doInBackground(Void... voids) {
+ try {
+ StringBuilder urlBuilder = new StringBuilder(MIDTRANS_TOKEN_URL);
+ urlBuilder.append("?card_number=").append(cardData.getPan());
+ urlBuilder.append("&card_exp_month=").append(cardData.getExpiryMonth());
+ urlBuilder.append("&card_exp_year=").append(cardData.getExpiryYear());
+
+ // Always include CVV for tokenization
+ String cvvToUse = determineCVV(cardData);
+ urlBuilder.append("&card_cvv=").append(cvvToUse);
+ Log.d(TAG, "Using CVV " + cvvToUse + " for backend tokenization");
+
+ urlBuilder.append("&client_key=").append(MIDTRANS_CLIENT_KEY);
+
+ Log.d(TAG, "Backend Tokenization URL: " + maskUrl(urlBuilder.toString()));
+
+ URL url = new URI(urlBuilder.toString()).toURL();
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.setRequestProperty("Accept", "application/json");
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setConnectTimeout(30000);
+ conn.setReadTimeout(30000);
+
+ int responseCode = conn.getResponseCode();
+ Log.d(TAG, "Backend Tokenization response code: " + responseCode);
+
+ if (responseCode == 200) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ StringBuilder response = new StringBuilder();
+ String responseLine;
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ Log.d(TAG, "Backend Tokenization success response: " + response.toString());
+
+ JSONObject jsonResponse = new JSONObject(response.toString());
+ if (jsonResponse.has("token_id")) {
+ return jsonResponse.getString("token_id");
+ } else {
+ errorMessage = "Token ID not found in backend response";
+ return null;
+ }
+
+ } else {
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
+ StringBuilder errorResponse = new StringBuilder();
+ String responseLine;
+ while ((responseLine = br.readLine()) != null) {
+ errorResponse.append(responseLine.trim());
+ }
+
+ Log.e(TAG, "Backend Tokenization error: " + errorResponse.toString());
+ errorMessage = "Backend tokenization failed: " + errorResponse.toString();
+ return null;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Backend Tokenization exception: " + e.getMessage(), e);
+ errorMessage = "Network error: " + e.getMessage();
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(String cardToken) {
+ if (cardToken != null && callback != null) {
+ callback.onTokenizeSuccess(cardToken);
+
+ if (callback != null) {
+ callback.onPaymentProgress("Processing backend payment...");
+ }
+ new ChargeCardWithOrderIdTask(cardToken, amount, orderId, cardData).execute();
+
+ } else if (callback != null) {
+ callback.onTokenizeError(errorMessage != null ? errorMessage : "Unknown backend tokenization error");
+ }
+ }
+ }
+
+ /**
+ * ✅ NEW: Charge Card with Custom Order ID
+ */
+ private class ChargeCardWithOrderIdTask extends AsyncTask {
+ private String cardToken;
+ private long amount;
+ private String orderId;
+ private String errorMessage;
+ private JSONObject chargeResponse;
+ private CardData cardData;
+
+ public ChargeCardWithOrderIdTask(String cardToken, long amount, String orderId, CardData cardData) {
+ this.cardToken = cardToken;
+ this.amount = amount;
+ this.orderId = orderId;
+ this.cardData = cardData;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ try {
+ JSONObject payload = new JSONObject();
+ payload.put("payment_type", "credit_card");
+
+ JSONObject transactionDetails = new JSONObject();
+ transactionDetails.put("order_id", orderId); // ✅ Use backend transaction_uuid
+ transactionDetails.put("gross_amount", amount);
+ payload.put("transaction_details", transactionDetails);
+
+ JSONObject creditCard = new JSONObject();
+ creditCard.put("token_id", cardToken);
+ payload.put("credit_card", creditCard);
+
+ // Item details
+ JSONArray itemDetails = new JSONArray();
+ JSONObject item = new JSONObject();
+ item.put("id", "tkn_backend1");
+ item.put("price", amount);
+ item.put("quantity", 1);
+ item.put("name", "Token Backend Transaction");
+ item.put("brand", "Token Backend Payment");
+ item.put("category", "Backend Transaction");
+ item.put("merchant_name", "EDC-Store-Backend");
+ itemDetails.put(item);
+ payload.put("item_details", itemDetails);
+
+ addCustomerDetails(payload);
+
+ Log.d(TAG, "=== TOKEN BACKEND CHARGE ===");
+ Log.d(TAG, "Order ID (Backend UUID): " + orderId);
+ Log.d(TAG, "Amount: " + amount);
+ Log.d(TAG, "Token: " + maskToken(cardToken));
+ Log.d(TAG, "============================");
+
+ return makeChargeRequest(payload);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Token backend charge exception: " + e.getMessage(), e);
+ errorMessage = "Token backend charge error: " + e.getMessage();
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ if (success && chargeResponse != null && callback != null) {
+ Log.d(TAG, "✅ Token backend charge successful!");
+ callback.onChargeSuccess(chargeResponse);
+ } else if (callback != null) {
+ // Check for retry scenarios
+ if (shouldRetry(errorMessage) && retryCount < MAX_RETRY) {
+ retryCount++;
+ Log.w(TAG, "Retrying backend charge... (attempt " + retryCount + "/" + MAX_RETRY + ")");
+ callback.onPaymentProgress("Retrying backend... (" + retryCount + "/" + MAX_RETRY + ")");
+ new TokenizeCardWithOrderIdTask(cardData, amount, orderId).execute();
+ } else {
+ if (retryCount >= MAX_RETRY) {
+ errorMessage = "Max retry attempts reached. " + errorMessage;
+ }
+ callback.onChargeError(errorMessage != null ? errorMessage : "Unknown backend charge error");
+ }
+ }
+ }
+
+ private Boolean makeChargeRequest(JSONObject payload) {
+ try {
+ 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_SERVER_AUTH);
+ 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, "Token Backend Charge response code: " + responseCode);
+
+ BufferedReader br;
+ StringBuilder response = new StringBuilder();
+ String responseLine;
+
+ if (responseCode == 200 || responseCode == 201) {
+ br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ } else {
+ br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
+ }
+
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ String responseString = response.toString();
+ Log.d(TAG, "Token Backend Charge response: " + responseString);
+
+ // Store response for debugging
+ try {
+ chargeResponse = new JSONObject(responseString);
+ lastResponse = chargeResponse;
+
+ // Enhanced: Add bank detection if missing
+ enhanceResponseWithBankInfo(chargeResponse);
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error parsing response JSON: " + e.getMessage());
+ chargeResponse = createFallbackResponse(responseString, responseCode);
+ lastResponse = chargeResponse;
+ }
+
+ return processChargeResponse(chargeResponse, responseCode);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Token backend charge request exception: " + e.getMessage(), e);
+ errorMessage = "Network error: " + e.getMessage();
+ lastErrorMessage = errorMessage;
+ return false;
+ }
+ }
+
+ private Boolean processChargeResponse(JSONObject response, int httpCode) {
+ try {
+ String statusCode = response.optString("status_code", "");
+ String statusMessage = response.optString("status_message", "");
+ String transactionStatus = response.optString("transaction_status", "");
+
+ Log.d(TAG, "Token Backend Charge Response - Status: " + statusCode + ", Message: " + statusMessage);
+
+ if ("411".equals(statusCode)) {
+ errorMessage = "Token expired: " + statusMessage;
+ return false;
+ } else if ("400".equals(statusCode)) {
+ errorMessage = "Bad request: " + statusMessage;
+ return false;
+ } else if ("202".equals(statusCode) && "deny".equals(transactionStatus)) {
+ errorMessage = "Transaction denied: " + statusMessage;
+ return false;
+ } else if (httpCode != 200 && httpCode != 201) {
+ errorMessage = "HTTP error: " + httpCode + " - " + statusMessage;
+ return false;
+ }
+
+ if ("200".equals(statusCode) &&
+ ("capture".equals(transactionStatus) ||
+ "settlement".equals(transactionStatus) ||
+ "pending".equals(transactionStatus))) {
+ return true;
+ } else if ("201".equals(statusCode)) {
+ return true;
+ } else {
+ errorMessage = "Transaction failed: " + statusMessage;
+ return false;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error processing token backend charge response: " + e.getMessage(), e);
+ errorMessage = "Response processing error: " + e.getMessage();
+ return false;
+ }
+ }
+ }
+
+ /**
+ * EMV Direct Charge - bypasses tokenization for EMV cards
+ */
+ private class EMVDirectChargeTask extends AsyncTask {
+ private CardData cardData;
+ private long amount;
+ private String referenceId;
+ private String emvData;
+ private String errorMessage;
+ private JSONObject chargeResponse;
+
+ public EMVDirectChargeTask(CardData cardData, long amount, String referenceId, String emvData) {
+ this.cardData = cardData;
+ this.amount = amount;
+ this.referenceId = referenceId;
+ this.emvData = emvData;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ try {
+ String orderId = "EMV" + System.currentTimeMillis();
+
+ // Build EMV-specific charge payload
+ JSONObject payload = new JSONObject();
+ payload.put("payment_type", "credit_card");
+
+ // Transaction details
+ JSONObject transactionDetails = new JSONObject();
+ transactionDetails.put("order_id", orderId);
+ transactionDetails.put("gross_amount", amount);
+ payload.put("transaction_details", transactionDetails);
+
+ // EMV Credit card data (no tokenization)
+ JSONObject creditCard = new JSONObject();
+ creditCard.put("card_number", cardData.getPan());
+ creditCard.put("card_exp_month", cardData.getExpiryMonth());
+ creditCard.put("card_exp_year", cardData.getExpiryYear());
+
+ // Include static CVV even for EMV (Midtrans may require it)
+ creditCard.put("card_cvv", STATIC_CVV);
+ Log.d(TAG, "EMV Transaction: Including static CVV (" + STATIC_CVV + ") for Midtrans compatibility");
+
+ // Add EMV data if available
+ if (emvData != null && !emvData.isEmpty()) {
+ creditCard.put("emv_data", emvData);
+ creditCard.put("authentication_mode", "chip");
+ }
+
+ payload.put("credit_card", creditCard);
+
+ // Item details
+ JSONArray itemDetails = new JSONArray();
+ JSONObject item = new JSONObject();
+ item.put("id", "emv1");
+ item.put("price", amount);
+ item.put("quantity", 1);
+ item.put("name", "EMV Transaction");
+ item.put("brand", "EMV Payment");
+ item.put("category", "Transaction");
+ item.put("merchant_name", "EDC-Store");
+ itemDetails.put(item);
+ payload.put("item_details", itemDetails);
+
+ // Customer details (same as curl example)
+ addCustomerDetails(payload);
+
+ Log.d(TAG, "=== EMV DIRECT CHARGE ===");
+ Log.d(TAG, "Order ID: " + orderId);
+ Log.d(TAG, "Amount: " + amount);
+ Log.d(TAG, "Card: " + maskCardNumber(cardData.getPan()));
+ Log.d(TAG, "Mode: EMV Direct (No Token)");
+ Log.d(TAG, "========================");
+
+ // Make charge request
+ return makeChargeRequest(payload);
+
+ } catch (Exception e) {
+ Log.e(TAG, "EMV Direct Charge exception: " + e.getMessage(), e);
+ errorMessage = "EMV payment error: " + e.getMessage();
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ if (success && chargeResponse != null && callback != null) {
+ callback.onChargeSuccess(chargeResponse);
+ } else if (callback != null) {
+ // Fallback to tokenization if direct charge fails
+ Log.w(TAG, "EMV direct charge failed, trying tokenization fallback...");
+ callback.onPaymentProgress("Retrying with tokenization...");
+ new TokenizeCardTask(cardData, amount, referenceId).execute();
+ }
+ }
+
+ private Boolean makeChargeRequest(JSONObject payload) {
+ try {
+ 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_SERVER_AUTH);
+ 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, "EMV Charge response code: " + responseCode);
+
+ BufferedReader br;
+ StringBuilder response = new StringBuilder();
+ String responseLine;
+
+ if (responseCode == 200 || responseCode == 201) {
+ br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ } else {
+ br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
+ }
+
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ String responseString = response.toString();
+ Log.d(TAG, "EMV Charge response: " + responseString);
+
+ // Store response for debugging
+ try {
+ chargeResponse = new JSONObject(responseString);
+ lastResponse = chargeResponse;
+
+ // Enhanced: Add bank detection if missing
+ enhanceResponseWithBankInfo(chargeResponse);
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error parsing response JSON: " + e.getMessage());
+ chargeResponse = createFallbackResponse(responseString, responseCode);
+ lastResponse = chargeResponse;
+ }
+
+ return processChargeResponse(chargeResponse, responseCode);
+
+ } catch (Exception e) {
+ Log.e(TAG, "EMV Charge request exception: " + e.getMessage(), e);
+ errorMessage = "Network error: " + e.getMessage();
+ lastErrorMessage = errorMessage;
+ return false;
+ }
+ }
+
+ private Boolean processChargeResponse(JSONObject response, int httpCode) {
+ try {
+ String statusCode = response.optString("status_code", "");
+ String statusMessage = response.optString("status_message", "");
+ String transactionStatus = response.optString("transaction_status", "");
+ String fraudStatus = response.optString("fraud_status", "");
+
+ Log.d(TAG, "=== CHARGE RESPONSE ANALYSIS ===");
+ Log.d(TAG, "HTTP Code: " + httpCode);
+ Log.d(TAG, "Status Code: " + statusCode);
+ Log.d(TAG, "Status Message: " + statusMessage);
+ Log.d(TAG, "Transaction Status: " + transactionStatus);
+ Log.d(TAG, "Fraud Status: " + fraudStatus);
+ Log.d(TAG, "===============================");
+
+ // Handle specific error cases
+ if ("411".equals(statusCode)) {
+ errorMessage = "Token expired: " + statusMessage;
+ return false;
+ } else if ("400".equals(statusCode)) {
+ errorMessage = "Bad request: " + statusMessage;
+ return false;
+ } else if ("202".equals(statusCode) && "deny".equals(transactionStatus)) {
+ errorMessage = "Transaction denied: " + statusMessage;
+ return false;
+ } else if (httpCode != 200 && httpCode != 201) {
+ errorMessage = "HTTP error: " + httpCode + " - " + statusMessage;
+ return false;
+ }
+
+ // Success conditions
+ if ("200".equals(statusCode) &&
+ ("capture".equals(transactionStatus) ||
+ "settlement".equals(transactionStatus) ||
+ "pending".equals(transactionStatus))) {
+ return true;
+ } else if ("201".equals(statusCode)) {
+ return true;
+ } else {
+ errorMessage = "Transaction failed: " + statusMessage;
+ return false;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error processing charge response: " + e.getMessage(), e);
+ errorMessage = "Response processing error: " + e.getMessage();
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Enhanced Tokenize Card Task with better CVV handling
+ */
+ private class TokenizeCardTask extends AsyncTask {
+ private CardData cardData;
+ private long amount;
+ private String referenceId;
+ private String errorMessage;
+
+ public TokenizeCardTask(CardData cardData, long amount, String referenceId) {
+ this.cardData = cardData;
+ this.amount = amount;
+ this.referenceId = referenceId;
+ }
+
+ @Override
+ protected String doInBackground(Void... voids) {
+ try {
+ StringBuilder urlBuilder = new StringBuilder(MIDTRANS_TOKEN_URL);
+ urlBuilder.append("?card_number=").append(cardData.getPan());
+ urlBuilder.append("&card_exp_month=").append(cardData.getExpiryMonth());
+ urlBuilder.append("&card_exp_year=").append(cardData.getExpiryYear());
+
+ // Always include CVV for tokenization (Midtrans requires it)
+ String cvvToUse = determineCVV(cardData);
+ urlBuilder.append("&card_cvv=").append(cvvToUse);
+ Log.d(TAG, "Using CVV " + cvvToUse + " for tokenization (required by Midtrans)");
+
+ urlBuilder.append("&client_key=").append(MIDTRANS_CLIENT_KEY);
+
+ Log.d(TAG, "Tokenization URL: " + maskUrl(urlBuilder.toString()));
+
+ URL url = new URI(urlBuilder.toString()).toURL();
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.setRequestProperty("Accept", "application/json");
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setConnectTimeout(30000);
+ conn.setReadTimeout(30000);
+
+ int responseCode = conn.getResponseCode();
+ Log.d(TAG, "Tokenization response code: " + responseCode);
+
+ if (responseCode == 200) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ StringBuilder response = new StringBuilder();
+ String responseLine;
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ Log.d(TAG, "Tokenization success response: " + response.toString());
+
+ JSONObject jsonResponse = new JSONObject(response.toString());
+ if (jsonResponse.has("token_id")) {
+ return jsonResponse.getString("token_id");
+ } else {
+ errorMessage = "Token ID not found in response";
+ return null;
+ }
+
+ } else {
+ BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
+ StringBuilder errorResponse = new StringBuilder();
+ String responseLine;
+ while ((responseLine = br.readLine()) != null) {
+ errorResponse.append(responseLine.trim());
+ }
+
+ Log.e(TAG, "Tokenization error: " + errorResponse.toString());
+ errorMessage = "Tokenization failed: " + errorResponse.toString();
+ return null;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Tokenization exception: " + e.getMessage(), e);
+ errorMessage = "Network error: " + e.getMessage();
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(String cardToken) {
+ if (cardToken != null && callback != null) {
+ callback.onTokenizeSuccess(cardToken);
+
+ if (callback != null) {
+ callback.onPaymentProgress("Processing payment...");
+ }
+ new ChargeCardTask(cardToken, amount, referenceId, cardData).execute();
+
+ } else if (callback != null) {
+ callback.onTokenizeError(errorMessage != null ? errorMessage : "Unknown tokenization error");
+ }
+ }
+ }
+
+ /**
+ * Enhanced Charge Card Task
+ */
+ private class ChargeCardTask extends AsyncTask {
+ private String cardToken;
+ private long amount;
+ private String referenceId;
+ private String errorMessage;
+ private JSONObject chargeResponse;
+ private CardData cardData;
+
+ public ChargeCardTask(String cardToken, long amount, String referenceId, CardData cardData) {
+ this.cardToken = cardToken;
+ this.amount = amount;
+ this.referenceId = referenceId;
+ this.cardData = cardData;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ try {
+ String orderId = "TKN" + System.currentTimeMillis();
+
+ JSONObject payload = new JSONObject();
+ payload.put("payment_type", "credit_card");
+
+ JSONObject transactionDetails = new JSONObject();
+ transactionDetails.put("order_id", orderId);
+ transactionDetails.put("gross_amount", amount);
+ payload.put("transaction_details", transactionDetails);
+
+ JSONObject creditCard = new JSONObject();
+ creditCard.put("token_id", cardToken);
+ payload.put("credit_card", creditCard);
+
+ // Item details
+ JSONArray itemDetails = new JSONArray();
+ JSONObject item = new JSONObject();
+ item.put("id", "tkn1");
+ item.put("price", amount);
+ item.put("quantity", 1);
+ item.put("name", "Token Transaction");
+ item.put("brand", "Token Payment");
+ item.put("category", "Transaction");
+ item.put("merchant_name", "EDC-Store");
+ itemDetails.put(item);
+ payload.put("item_details", itemDetails);
+
+ addCustomerDetails(payload);
+
+ Log.d(TAG, "=== TOKEN CHARGE ===");
+ Log.d(TAG, "Order ID: " + orderId);
+ Log.d(TAG, "Amount: " + amount);
+ Log.d(TAG, "Token: " + maskToken(cardToken));
+ Log.d(TAG, "===================");
+
+ return makeChargeRequest(payload);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Token charge exception: " + e.getMessage(), e);
+ errorMessage = "Token charge error: " + e.getMessage();
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ if (success && chargeResponse != null && callback != null) {
+ callback.onChargeSuccess(chargeResponse);
+ } else if (callback != null) {
+ // Check for retry scenarios
+ if (shouldRetry(errorMessage) && retryCount < MAX_RETRY) {
+ retryCount++;
+ Log.w(TAG, "Retrying charge... (attempt " + retryCount + "/" + MAX_RETRY + ")");
+ callback.onPaymentProgress("Retrying... (" + retryCount + "/" + MAX_RETRY + ")");
+ new TokenizeCardTask(cardData, amount, referenceId).execute();
+ } else {
+ if (retryCount >= MAX_RETRY) {
+ errorMessage = "Max retry attempts reached. " + errorMessage;
+ }
+ callback.onChargeError(errorMessage != null ? errorMessage : "Unknown charge error");
+ }
+ }
+ }
+
+ private Boolean makeChargeRequest(JSONObject payload) {
+ try {
+ 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_SERVER_AUTH);
+ 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, "Token Charge response code: " + responseCode);
+
+ BufferedReader br;
+ StringBuilder response = new StringBuilder();
+ String responseLine;
+
+ if (responseCode == 200 || responseCode == 201) {
+ br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ } else {
+ br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
+ }
+
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ String responseString = response.toString();
+ Log.d(TAG, "Token Charge response: " + responseString);
+
+ // Store response for debugging
+ try {
+ chargeResponse = new JSONObject(responseString);
+ lastResponse = chargeResponse;
+
+ // Enhanced: Add bank detection if missing
+ enhanceResponseWithBankInfo(chargeResponse);
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error parsing response JSON: " + e.getMessage());
+ chargeResponse = createFallbackResponse(responseString, responseCode);
+ lastResponse = chargeResponse;
+ }
+
+ return processChargeResponse(chargeResponse, responseCode);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Token charge request exception: " + e.getMessage(), e);
+ errorMessage = "Network error: " + e.getMessage();
+ lastErrorMessage = errorMessage;
+ return false;
+ }
+ }
+
+ private Boolean processChargeResponse(JSONObject response, int httpCode) {
+ try {
+ String statusCode = response.optString("status_code", "");
+ String statusMessage = response.optString("status_message", "");
+ String transactionStatus = response.optString("transaction_status", "");
+
+ Log.d(TAG, "Token Charge Response - Status: " + statusCode + ", Message: " + statusMessage);
+
+ if ("411".equals(statusCode)) {
+ errorMessage = "Token expired: " + statusMessage;
+ return false;
+ } else if ("400".equals(statusCode)) {
+ errorMessage = "Bad request: " + statusMessage;
+ return false;
+ } else if ("202".equals(statusCode) && "deny".equals(transactionStatus)) {
+ errorMessage = "Transaction denied: " + statusMessage;
+ return false;
+ } else if (httpCode != 200 && httpCode != 201) {
+ errorMessage = "HTTP error: " + httpCode + " - " + statusMessage;
+ return false;
+ }
+
+ if ("200".equals(statusCode) &&
+ ("capture".equals(transactionStatus) ||
+ "settlement".equals(transactionStatus) ||
+ "pending".equals(transactionStatus))) {
+ return true;
+ } else if ("201".equals(statusCode)) {
+ return true;
+ } else {
+ errorMessage = "Transaction failed: " + statusMessage;
+ return false;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error processing token charge response: " + e.getMessage(), e);
+ errorMessage = "Response processing error: " + e.getMessage();
+ return false;
+ }
+ }
+ }
+
+ // Method to enhance response with bank information
+ private void enhanceResponseWithBankInfo(JSONObject response) {
+ if (response == null) return;
+
+ try {
+ // If bank field is missing or empty, try to determine it
+ if (!response.has("bank") || response.getString("bank").trim().isEmpty()) {
+ String detectedBank = detectBankFromResponse(response);
+ if (detectedBank != null) {
+ response.put("bank", detectedBank);
+ Log.d(TAG, "✅ Enhanced response with detected bank: " + detectedBank);
+ }
+ } else {
+ String existingBank = response.getString("bank");
+ Log.d(TAG, "✅ Response already has bank field: " + existingBank);
+ }
+
+ // Log final bank value
+ if (response.has("bank")) {
+ Log.d(TAG, "Final bank value in response: '" + response.getString("bank") + "'");
+ }
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error enhancing response with bank info: " + e.getMessage());
+ }
+ }
+
+ // Method to detect bank from various response fields
+ private String detectBankFromResponse(JSONObject response) {
+ try {
+ // Try various fields that might contain bank information
+ String[] possibleFields = {
+ "issuer", "acquiring_bank", "card_type", "payment_method",
+ "issuer_name", "bank_name", "acquirer"
+ };
+
+ for (String field : possibleFields) {
+ if (response.has(field)) {
+ String value = response.getString(field);
+ if (value != null && !value.trim().isEmpty()) {
+ String mappedBank = mapToBankName(value);
+ if (mappedBank != null) {
+ Log.d(TAG, "Detected bank '" + mappedBank + "' from field '" + field + "': " + value);
+ return mappedBank;
+ }
+ }
+ }
+ }
+
+ // If no bank detected from response fields, return default
+ Log.d(TAG, "No bank detected from response fields, using default");
+ return "BCA"; // Default
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error detecting bank from response: " + e.getMessage());
+ return "BCA"; // Default
+ }
+ }
+
+ // Map various bank identifiers to standard bank names
+ private String mapToBankName(String identifier) {
+ if (identifier == null || identifier.trim().isEmpty()) {
+ return null;
+ }
+
+ String normalized = identifier.trim().toUpperCase();
+
+ // Map common bank identifiers
+ if (normalized.contains("BCA") || normalized.contains("CENTRAL ASIA")) {
+ return "BCA";
+ } else if (normalized.contains("MANDIRI")) {
+ return "MANDIRI";
+ } else if (normalized.contains("BNI") || normalized.contains("NEGARA INDONESIA")) {
+ return "BNI";
+ } else if (normalized.contains("BRI") || normalized.contains("RAKYAT INDONESIA")) {
+ return "BRI";
+ } else if (normalized.contains("CIMB") || normalized.contains("NIAGA")) {
+ return "CIMB NIAGA";
+ } else if (normalized.contains("DANAMON")) {
+ return "DANAMON";
+ } else if (normalized.contains("PERMATA")) {
+ return "PERMATA";
+ } else if (normalized.contains("MEGA")) {
+ return "MEGA";
+ }
+
+ return null; // No mapping found
+ }
+
+ // Create fallback response when JSON parsing fails
+ private JSONObject createFallbackResponse(String rawResponse, int httpCode) {
+ try {
+ JSONObject fallback = new JSONObject();
+ fallback.put("status_code", String.valueOf(httpCode));
+ fallback.put("raw_response", rawResponse);
+ fallback.put("transaction_status", httpCode == 200 ? "capture" : "deny");
+ fallback.put("bank", "BCA"); // Default bank
+ fallback.put("is_fallback_response", true);
+
+ Log.d(TAG, "Created fallback response for HTTP " + httpCode);
+ return fallback;
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error creating fallback response: " + e.getMessage());
+ return new JSONObject();
+ }
+ }
+
+ // Helper Methods
+
+ /**
+ * Intelligent CVV determination - Always use static CVV for tokenization
+ */
+ private String determineCVV(CardData cardData) {
+ // For tokenization, Midtrans always requires CVV even for EMV cards
+ // Use static CVV as per curl example
+ Log.d(TAG, "Using static CVV (493) for tokenization - required by Midtrans API");
+ return STATIC_CVV;
+ }
+
+ /**
+ * Add customer details to payload (extracted for reuse)
+ */
+ private void addCustomerDetails(JSONObject payload) throws JSONException {
+ JSONObject customerDetails = new JSONObject();
+ customerDetails.put("first_name", "BUDI");
+ customerDetails.put("last_name", "UTOMO");
+ customerDetails.put("email", "test@midtrans.com");
+ customerDetails.put("phone", "+628123456");
+
+ // Billing address
+ JSONObject billingAddress = new JSONObject();
+ billingAddress.put("first_name", "BUDI");
+ billingAddress.put("last_name", "UTOMO");
+ billingAddress.put("email", "test@midtrans.com");
+ billingAddress.put("phone", "081 2233 44-55");
+ billingAddress.put("address", "Sudirman");
+ billingAddress.put("city", "Jakarta");
+ billingAddress.put("postal_code", "12190");
+ billingAddress.put("country_code", "IDN");
+ customerDetails.put("billing_address", billingAddress);
+
+ // Shipping address
+ JSONObject shippingAddress = new JSONObject();
+ shippingAddress.put("first_name", "BUDI");
+ shippingAddress.put("last_name", "UTOMO");
+ shippingAddress.put("email", "test@midtrans.com");
+ shippingAddress.put("phone", "0 8128-75 7-9338");
+ shippingAddress.put("address", "Sudirman");
+ shippingAddress.put("city", "Jakarta");
+ shippingAddress.put("postal_code", "12190");
+ shippingAddress.put("country_code", "IDN");
+ customerDetails.put("shipping_address", shippingAddress);
+
+ payload.put("customer_details", customerDetails);
+ }
+
+ /**
+ * Check if error should trigger a retry
+ */
+ private boolean shouldRetry(String error) {
+ if (error == null) return false;
+
+ return error.contains("Token expired") ||
+ error.contains("Network error") ||
+ error.contains("timeout");
+ }
+
+ // Debug method to inspect response
+ public void debugResponse() {
+ if (lastResponse != null) {
+ Log.d(TAG, "=== DEBUGGING LAST RESPONSE ===");
+ try {
+ java.util.Iterator keys = lastResponse.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ Object value = lastResponse.get(key);
+ Log.d(TAG, key + ": " + value);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error debugging response: " + e.getMessage());
+ }
+ Log.d(TAG, "===============================");
+ } else {
+ Log.d(TAG, "No last response available for debugging");
+ }
+ }
+
+ /**
+ * Enhanced Card data holder class with EMV detection
+ */
+ public static class CardData {
+ private String pan;
+ private String expiryMonth;
+ private String expiryYear;
+ private String cvv;
+ private String cardholderName;
+ private String aidIdentifier;
+ private boolean isEMVCard = false;
+
+ public CardData(String pan, String expiryMonth, String expiryYear, String cardholderName) {
+ this.pan = pan;
+ this.expiryMonth = expiryMonth;
+ this.expiryYear = expiryYear;
+ this.cardholderName = cardholderName;
+ }
+
+ public static CardData fromEMVData(String pan, String expiryDate, String cardholderName, String aid) {
+ String expMonth = "";
+ String expYear = "";
+
+ if (expiryDate != null && expiryDate.length() == 6) {
+ expYear = "20" + expiryDate.substring(0, 2);
+ expMonth = expiryDate.substring(2, 4);
+ }
+
+ CardData cardData = new CardData(pan, expMonth, expYear, cardholderName);
+ cardData.aidIdentifier = aid;
+ cardData.isEMVCard = true; // Mark as EMV card
+ return cardData;
+ }
+
+ public boolean isValid() {
+ return pan != null && !pan.isEmpty() &&
+ expiryMonth != null && !expiryMonth.isEmpty() &&
+ expiryYear != null && !expiryYear.isEmpty();
+ }
+
+ // Getters
+ public String getPan() { return pan; }
+ public String getExpiryMonth() { return expiryMonth; }
+ public String getExpiryYear() { return expiryYear; }
+ public String getCvv() { return cvv; }
+ public String getCardholderName() { return cardholderName; }
+ public String getAidIdentifier() { return aidIdentifier; }
+ public boolean isEMVCard() { return isEMVCard; }
+
+ // Setters
+ public void setCvv(String cvv) { this.cvv = cvv; }
+ public void setEMVCard(boolean isEMVCard) { this.isEMVCard = isEMVCard; }
+ }
+
+ // Utility methods
+ private String maskCardNumber(String cardNumber) {
+ if (cardNumber == null || cardNumber.length() < 8) {
+ return cardNumber;
+ }
+ String first4 = cardNumber.substring(0, 4);
+ String last4 = cardNumber.substring(cardNumber.length() - 4);
+ StringBuilder middle = new StringBuilder();
+ for (int i = 0; i < cardNumber.length() - 8; i++) {
+ middle.append("*");
+ }
+ return first4 + middle.toString() + last4;
+ }
+
+ private String maskToken(String token) {
+ if (token == null || token.length() < 8) {
+ return token;
+ }
+ return token.substring(0, 8) + "***";
+ }
+
+ private String maskUrl(String url) {
+ if (url == null) return url;
+ return url.replaceAll("card_number=[^&]*", "card_number=****")
+ .replaceAll("card_cvv=[^&]*", "card_cvv=***")
+ .replaceAll("client_key=[^&]*", "client_key=***");
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/transaction/managers/ModalManager.java b/app/src/main/java/com/example/bdkipoc/transaction/managers/ModalManager.java
new file mode 100644
index 0000000..262cb27
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/transaction/managers/ModalManager.java
@@ -0,0 +1,121 @@
+package com.example.bdkipoc.transaction.managers;
+
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.util.Log;
+
+import com.example.bdkipoc.R;
+
+/**
+ * ModalManager - Handles modal UI operations
+ */
+public class ModalManager {
+ private static final String TAG = "ModalManager";
+
+ private FrameLayout modalOverlay;
+ private TextView modalText;
+ private ImageView modalIcon;
+ private Animation fadeIn;
+ private Animation fadeOut;
+ private boolean isModalShowing = false;
+
+ public ModalManager(FrameLayout modalOverlay, TextView modalText, ImageView modalIcon) {
+ this.modalOverlay = modalOverlay;
+ this.modalText = modalText;
+ this.modalIcon = modalIcon;
+ initAnimations();
+ }
+
+ private void initAnimations() {
+ fadeIn = AnimationUtils.loadAnimation(modalOverlay.getContext(), android.R.anim.fade_in);
+ fadeOut = AnimationUtils.loadAnimation(modalOverlay.getContext(), android.R.anim.fade_out);
+
+ fadeIn.setDuration(300);
+ fadeOut.setDuration(300);
+ }
+
+ public void showScanCardModal() {
+ if (isModalShowing) return;
+
+ modalOverlay.post(() -> {
+ modalText.setText("Silakan Tempelkan / Gesekkan / Masukkan Kartu ke Perangkat");
+ modalIcon.setImageResource(R.drawable.ic_card_insert);
+
+ modalOverlay.setVisibility(View.VISIBLE);
+ modalOverlay.startAnimation(fadeIn);
+
+ isModalShowing = true;
+ Log.d(TAG, "Modal scan card shown");
+ });
+ }
+
+ public void showProcessingModal(String message) {
+ if (!isModalShowing) {
+ modalOverlay.post(() -> {
+ modalText.setText(message);
+ modalIcon.setImageResource(R.drawable.ic_card_insert);
+
+ modalOverlay.setVisibility(View.VISIBLE);
+ modalOverlay.startAnimation(fadeIn);
+
+ isModalShowing = true;
+ Log.d(TAG, "Modal processing shown: " + message);
+ });
+ } else {
+ // Just update text if modal already showing
+ modalOverlay.post(() -> {
+ modalText.setText(message);
+ Log.d(TAG, "Modal text updated: " + message);
+ });
+ }
+ }
+
+ public void hideModal() {
+ if (!isModalShowing) return;
+
+ modalOverlay.post(() -> {
+ fadeOut.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ modalOverlay.setVisibility(View.GONE);
+ isModalShowing = false;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+
+ modalOverlay.startAnimation(fadeOut);
+ Log.d(TAG, "Modal hidden");
+ });
+ }
+
+ public boolean isShowing() {
+ return isModalShowing;
+ }
+
+ public void updateText(String text) {
+ if (isModalShowing) {
+ modalOverlay.post(() -> {
+ modalText.setText(text);
+ Log.d(TAG, "Modal text updated: " + text);
+ });
+ }
+ }
+
+ public void updateIcon(int iconResource) {
+ if (isModalShowing) {
+ modalOverlay.post(() -> {
+ modalIcon.setImageResource(iconResource);
+ Log.d(TAG, "Modal icon updated");
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/transaction/managers/PinPadManager.java b/app/src/main/java/com/example/bdkipoc/transaction/managers/PinPadManager.java
new file mode 100644
index 0000000..c31d10f
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/transaction/managers/PinPadManager.java
@@ -0,0 +1,142 @@
+package com.example.bdkipoc.transaction.managers;
+
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.example.bdkipoc.MyApplication;
+import com.example.bdkipoc.utils.ByteUtil;
+import com.sunmi.pay.hardware.aidlv2.AidlErrorCodeV2;
+import com.sunmi.pay.hardware.aidlv2.bean.PinPadConfigV2;
+import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadListenerV2;
+import com.sunmi.pay.hardware.aidlv2.pinpad.PinPadOptV2;
+
+/**
+ * PinPadManager - Handles PIN pad operations
+ */
+public class PinPadManager {
+ private static final String TAG = "PinPadManager";
+
+ private PinPadOptV2 mPinPadOptV2;
+ private PinPadManagerCallback callback;
+
+ public interface PinPadManagerCallback {
+ void onPinInputLength(int length);
+ void onPinInputConfirmed(byte[] pinBlock);
+ void onPinInputCancelled();
+ void onPinInputError(int code, String message);
+ }
+
+ public PinPadManager(PinPadManagerCallback callback) {
+ this.callback = callback;
+ initPinPadComponents();
+ }
+
+ private void initPinPadComponents() {
+ if (MyApplication.app != null) {
+ mPinPadOptV2 = MyApplication.app.pinPadOptV2;
+ Log.d(TAG, "PIN Pad components initialized");
+ } else {
+ Log.e(TAG, "MyApplication.app is null");
+ }
+ }
+
+ public void initPinPad(String cardNo, int pinType) {
+ Log.d(TAG, "========== PIN PAD INITIALIZATION ==========");
+ try {
+ if (mPinPadOptV2 == null) {
+ throw new IllegalStateException("PIN Pad service not available");
+ }
+
+ if (cardNo == null || cardNo.length() < 13) {
+ throw new IllegalArgumentException("Invalid card number for PIN");
+ }
+
+ PinPadConfigV2 pinPadConfig = new PinPadConfigV2();
+ pinPadConfig.setPinPadType(0);
+ pinPadConfig.setPinType(pinType);
+ pinPadConfig.setOrderNumKey(true); // Set to true for normal order, false for random
+
+ String panForPin = cardNo.substring(cardNo.length() - 13, cardNo.length() - 1);
+ byte[] panBytes = panForPin.getBytes("US-ASCII");
+ pinPadConfig.setPan(panBytes);
+
+ pinPadConfig.setTimeout(60 * 1000);
+ pinPadConfig.setPinKeyIndex(12);
+ pinPadConfig.setMaxInput(12);
+ pinPadConfig.setMinInput(0);
+ pinPadConfig.setKeySystem(0);
+ pinPadConfig.setAlgorithmType(0);
+
+ Log.d(TAG, "Initializing PIN pad with config");
+ mPinPadOptV2.initPinPad(pinPadConfig, mPinPadListener);
+
+ } catch (Exception e) {
+ Log.e(TAG, "PIN pad initialization failed: " + e.getMessage());
+ if (callback != null) {
+ callback.onPinInputError(-1, "PIN Error: " + e.getMessage());
+ }
+ }
+ }
+
+ public void cancelPinInput() {
+ try {
+ if (mPinPadOptV2 != null) {
+ // Cancel PIN input if needed
+ Log.d(TAG, "PIN input cancelled");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error cancelling PIN input: " + e.getMessage());
+ }
+ }
+
+ // PIN Pad Listener
+ private final PinPadListenerV2 mPinPadListener = new PinPadListenerV2.Stub() {
+ @Override
+ public void onPinLength(int len) throws RemoteException {
+ Log.d(TAG, "PIN input length: " + len);
+ if (callback != null) {
+ callback.onPinInputLength(len);
+ }
+ }
+
+ @Override
+ public void onConfirm(int i, byte[] pinBlock) throws RemoteException {
+ Log.d(TAG, "PIN input confirmed");
+
+ if (pinBlock != null) {
+ String hexStr = ByteUtil.bytes2HexStr(pinBlock);
+ Log.d(TAG, "PIN block received: " + hexStr);
+ if (callback != null) {
+ callback.onPinInputConfirmed(pinBlock);
+ }
+ } else {
+ Log.d(TAG, "PIN bypass confirmed");
+ if (callback != null) {
+ callback.onPinInputConfirmed(null);
+ }
+ }
+ }
+
+ @Override
+ public void onCancel() throws RemoteException {
+ Log.d(TAG, "PIN input cancelled by user");
+ if (callback != null) {
+ callback.onPinInputCancelled();
+ }
+ }
+
+ @Override
+ public void onError(int code) throws RemoteException {
+ Log.e(TAG, "PIN pad error: " + code);
+ String msg = AidlErrorCodeV2.valueOf(code).getMsg();
+ if (callback != null) {
+ callback.onPinInputError(code, msg);
+ }
+ }
+
+ @Override
+ public void onHover(int event, byte[] data) throws RemoteException {
+ Log.d(TAG, "PIN pad hover event: " + event);
+ }
+ };
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/transaction/managers/PostTransactionBackendManager.java b/app/src/main/java/com/example/bdkipoc/transaction/managers/PostTransactionBackendManager.java
new file mode 100644
index 0000000..bf9119f
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/transaction/managers/PostTransactionBackendManager.java
@@ -0,0 +1,358 @@
+package com.example.bdkipoc.transaction.managers;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.util.UUID;
+
+/**
+ * PostTransactionBackendManager - Handles backend transaction posting
+ *
+ * This manager handles the communication with the backend service for transaction posting
+ * and provides the transaction_uuid needed for Midtrans integration.
+ */
+public class PostTransactionBackendManager {
+ private static final String TAG = "PostTransactionBackend";
+
+ // Backend Configuration
+ private static final String BACKEND_BASE_URL = "https://be-edc.msvc.app";
+ private static final String TRANSACTIONS_ENDPOINT = BACKEND_BASE_URL + "/transactions";
+
+ // Default values
+ private static final String DEFAULT_DEVICE_CODE = "PB4K252T00021";
+ private static final int DEFAULT_DEVICE_ID = 1;
+ private static final String DEFAULT_CASHFLOW = "MONEY_IN";
+ private static final String DEFAULT_CHANNEL_CATEGORY = "RETAIL_OUTLET";
+
+ // ✅ NEW: Static merchant data
+ private static final String DEFAULT_MERCHANT_NAME = "BUDIAJAIB123";
+ private static final String DEFAULT_MID = "542531513";
+ private static final String DEFAULT_TID = "535151521";
+
+ private Context context;
+ private PostTransactionCallback callback;
+
+ public interface PostTransactionCallback {
+ void onPostTransactionSuccess(JSONObject response, String transactionUuid);
+ void onPostTransactionError(String errorMessage);
+ void onPostTransactionProgress(String message);
+ }
+
+ public PostTransactionBackendManager(Context context, PostTransactionCallback callback) {
+ this.context = context;
+ this.callback = callback;
+ }
+
+ /**
+ * Post transaction to backend service
+ */
+ public void postTransaction(String paymentType, String referenceId, long amount, String status) {
+ String channelCode = mapPaymentTypeToChannelCode(paymentType);
+ String transactionUuid = generateUUID();
+
+ Log.d(TAG, "=== POSTING TRANSACTION TO BACKEND ===");
+ Log.d(TAG, "Payment Type: " + paymentType);
+ Log.d(TAG, "Channel Code: " + channelCode);
+ Log.d(TAG, "Reference ID: " + referenceId);
+ Log.d(TAG, "Amount: " + amount);
+ Log.d(TAG, "Status: " + status);
+ Log.d(TAG, "Transaction UUID: " + transactionUuid);
+ Log.d(TAG, "=====================================");
+
+ if (callback != null) {
+ callback.onPostTransactionProgress("Posting transaction to backend...");
+ }
+
+ new PostTransactionTask(paymentType, channelCode, referenceId, amount, status, transactionUuid).execute();
+ }
+
+ /**
+ * Post transaction with INIT status (for pre-authorization)
+ */
+ public void postInitTransaction(String paymentType, String referenceId, long amount) {
+ postTransaction(paymentType, referenceId, amount, "INIT");
+ }
+
+ /**
+ * Post transaction with SUCCESS status (for completed transactions)
+ */
+ public void postSuccessTransaction(String paymentType, String referenceId, long amount) {
+ postTransaction(paymentType, referenceId, amount, "SUCCESS");
+ }
+
+ /**
+ * Update existing transaction status
+ */
+ public void updateTransactionStatus(String transactionUuid, String newStatus) {
+ Log.d(TAG, "Updating transaction " + transactionUuid + " to status: " + newStatus);
+ // TODO: Implement update endpoint if available
+ // For now, we'll create a new transaction with updated status
+ }
+
+ /**
+ * Map payment type to channel code for backend
+ */
+ private String mapPaymentTypeToChannelCode(String paymentType) {
+ if (paymentType == null) {
+ return "CREDIT_CARD"; // Default
+ }
+
+ switch (paymentType.toLowerCase()) {
+ case "credit_card":
+ return "CREDIT_CARD";
+ case "debit_card":
+ return "DEBIT_CARD";
+ case "e_money":
+ return "E_MONEY";
+ case "qris":
+ return "QRIS";
+ default:
+ Log.w(TAG, "Unknown payment type: " + paymentType + ", using CREDIT_CARD");
+ return "CREDIT_CARD";
+ }
+ }
+
+ /**
+ * Generate UUID v4 for transaction
+ */
+ private String generateUUID() {
+ return UUID.randomUUID().toString();
+ }
+
+ /**
+ * Get device serial number (Sunmi device code)
+ */
+ private String getDeviceCode() {
+ try {
+ // Try to get actual device serial number
+ // For Sunmi devices, this might be available through system properties
+ String serialNumber = android.os.Build.SERIAL;
+ if (serialNumber != null && !serialNumber.equals("unknown") && !serialNumber.equals(android.os.Build.UNKNOWN)) {
+ return serialNumber;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Could not get device serial number: " + e.getMessage());
+ }
+
+ // Fallback to default device code
+ return DEFAULT_DEVICE_CODE;
+ }
+
+ /**
+ * AsyncTask for posting transaction to backend
+ */
+ private class PostTransactionTask extends AsyncTask {
+ private String paymentType;
+ private String channelCode;
+ private String referenceId;
+ private long amount;
+ private String status;
+ private String transactionUuid;
+ private String errorMessage;
+ private JSONObject responseData;
+
+ public PostTransactionTask(String paymentType, String channelCode, String referenceId,
+ long amount, String status, String transactionUuid) {
+ this.paymentType = paymentType;
+ this.channelCode = channelCode;
+ this.referenceId = referenceId;
+ this.amount = amount;
+ this.status = status;
+ this.transactionUuid = transactionUuid;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... voids) {
+ try {
+ // Build transaction payload
+ JSONObject payload = buildTransactionPayload();
+
+ Log.d(TAG, "Backend payload: " + payload.toString());
+
+ // Make HTTP request
+ return makeBackendRequest(payload);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Backend transaction exception: " + e.getMessage(), e);
+ errorMessage = "Backend transaction error: " + e.getMessage();
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ if (success && responseData != null && callback != null) {
+ try {
+ // Extract transaction_uuid from response
+ JSONObject data = responseData.optJSONObject("data");
+ String returnedUuid = null;
+
+ if (data != null) {
+ returnedUuid = data.optString("transaction_uuid", transactionUuid);
+ }
+
+ Log.d(TAG, "✅ Backend transaction successful!");
+ Log.d(TAG, "Original UUID: " + transactionUuid);
+ Log.d(TAG, "Returned UUID: " + returnedUuid);
+
+ callback.onPostTransactionSuccess(responseData, returnedUuid != null ? returnedUuid : transactionUuid);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error processing backend response: " + e.getMessage());
+ if (callback != null) {
+ callback.onPostTransactionError("Error processing backend response: " + e.getMessage());
+ }
+ }
+ } else if (callback != null) {
+ callback.onPostTransactionError(errorMessage != null ? errorMessage : "Unknown backend error");
+ }
+ }
+
+ private JSONObject buildTransactionPayload() throws JSONException {
+ JSONObject payload = new JSONObject();
+
+ // Required fields
+ payload.put("type", "PAYMENT");
+ payload.put("channel_category", DEFAULT_CHANNEL_CATEGORY);
+ payload.put("channel_code", channelCode);
+ payload.put("reference_id", referenceId);
+ payload.put("amount", amount);
+ payload.put("cashflow", DEFAULT_CASHFLOW);
+ payload.put("status", status);
+ payload.put("device_id", DEFAULT_DEVICE_ID);
+ payload.put("transaction_uuid", transactionUuid);
+ payload.put("transaction_time_seconds", 2.2); // Default value as mentioned
+ payload.put("device_code", getDeviceCode());
+
+ // ✅ NEW: Static merchant data (no longer null)
+ payload.put("merchant_name", DEFAULT_MERCHANT_NAME);
+ payload.put("mid", DEFAULT_MID);
+ payload.put("tid", DEFAULT_TID);
+
+ return payload;
+ }
+
+ private Boolean makeBackendRequest(JSONObject payload) {
+ try {
+ URL url = new URI(TRANSACTIONS_ENDPOINT).toURL();
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+ // Set request properties
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Accept", "*/*");
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setDoOutput(true);
+ conn.setConnectTimeout(30000);
+ conn.setReadTimeout(30000);
+
+ // Send payload
+ try (OutputStream os = conn.getOutputStream()) {
+ byte[] input = payload.toString().getBytes("utf-8");
+ os.write(input, 0, input.length);
+ }
+
+ // Get response
+ int responseCode = conn.getResponseCode();
+ Log.d(TAG, "Backend response code: " + responseCode);
+
+ BufferedReader br;
+ StringBuilder response = new StringBuilder();
+ String responseLine;
+
+ if (responseCode >= 200 && responseCode < 300) {
+ br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
+ } else {
+ br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
+ }
+
+ while ((responseLine = br.readLine()) != null) {
+ response.append(responseLine.trim());
+ }
+
+ String responseString = response.toString();
+ Log.d(TAG, "Backend response: " + responseString);
+
+ // Parse response
+ try {
+ responseData = new JSONObject(responseString);
+
+ // Check if response indicates success
+ int status = responseData.optInt("status", 0);
+ String message = responseData.optString("message", "");
+
+ if (status == 200 && "Successfully".equals(message)) {
+ return true;
+ } else {
+ errorMessage = "Backend error: " + message + " (Status: " + status + ")";
+ return false;
+ }
+
+ } catch (JSONException e) {
+ Log.e(TAG, "Error parsing backend response: " + e.getMessage());
+ errorMessage = "Invalid backend response format";
+ return false;
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Backend request exception: " + e.getMessage(), e);
+ errorMessage = "Network error: " + e.getMessage();
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Utility method to generate reference ID
+ */
+ public static String generateReferenceId() {
+ return "ref" + System.currentTimeMillis() + (int)(Math.random() * 10000);
+ }
+
+ /**
+ * Utility method to map card menu ID to payment type
+ */
+ public static String mapCardMenuToPaymentType(int cardMenuId) {
+ // Based on MainActivity.java card IDs
+ switch (cardMenuId) {
+ case 2131296346: // R.id.card_kartu_kredit
+ return "credit_card";
+ case 2131296344: // R.id.card_kartu_debit
+ return "debit_card";
+ case 2131296360: // R.id.card_uang_elektronik
+ return "e_money";
+ case 2131296352: // R.id.card_qris
+ return "qris";
+ default:
+ Log.w(TAG, "Unknown card menu ID: " + cardMenuId + ", defaulting to credit_card");
+ return "credit_card";
+ }
+ }
+
+ /**
+ * Debug method to log transaction details
+ */
+ public void debugTransactionData(String paymentType, String referenceId, long amount, String status) {
+ Log.d(TAG, "=== TRANSACTION DEBUG INFO ===");
+ Log.d(TAG, "Payment Type: " + paymentType);
+ Log.d(TAG, "Channel Code: " + mapPaymentTypeToChannelCode(paymentType));
+ Log.d(TAG, "Reference ID: " + referenceId);
+ Log.d(TAG, "Amount: " + amount);
+ Log.d(TAG, "Status: " + status);
+ Log.d(TAG, "Device Code: " + getDeviceCode());
+ Log.d(TAG, "Device ID: " + DEFAULT_DEVICE_ID);
+ Log.d(TAG, "Merchant Name: " + DEFAULT_MERCHANT_NAME);
+ Log.d(TAG, "MID: " + DEFAULT_MID);
+ Log.d(TAG, "TID: " + DEFAULT_TID);
+ Log.d(TAG, "==============================");
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java b/app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java
new file mode 100644
index 0000000..51cb9e1
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java
@@ -0,0 +1,265 @@
+package com.example.bdkipoc.utils;
+
+import android.text.TextUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+public class ByteUtil {
+
+ /** 打印内容 */
+ public static String byte2PrintHex(byte[] raw, int offset, int count) {
+ if (raw == null) {
+ return null;
+ }
+ if (offset < 0 || offset > raw.length) {
+ offset = 0;
+ }
+ int end = offset + count;
+ if (end > raw.length) {
+ end = raw.length;
+ }
+ StringBuilder hex = new StringBuilder();
+ for (int i = offset; i < end; i++) {
+ int v = raw[i] & 0xFF;
+ String hv = Integer.toHexString(v);
+ if (hv.length() < 2) {
+ hex.append(0);
+ }
+ hex.append(hv);
+ hex.append(" ");
+ }
+ if (hex.length() > 0) {
+ hex.deleteCharAt(hex.length() - 1);
+ }
+ return hex.toString().toUpperCase();
+ }
+
+ /**
+ * 将字节数组转换成16进制字符串
+ *
+ * @param bytes 源字节数组
+ * @return 转换后的16进制字符串
+ */
+ public static String bytes2HexStr(byte... bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return "";
+ }
+ return bytes2HexStr(bytes, 0, bytes.length);
+ }
+
+ /**
+ * 将字节数组转换成16进制字符串
+ *
+ * @param src 源字节数组
+ * @param offset 偏移量
+ * @param len 数据长度
+ * @return 转换后的16进制字符串
+ */
+ public static String bytes2HexStr(byte[] src, int offset, int len) {
+ int end = offset + len;
+ if (src == null || src.length == 0 || offset < 0 || len < 0 || end > src.length) {
+ return "";
+ }
+ byte[] buffer = new byte[len * 2];
+ int h = 0, l = 0;
+ for (int i = offset, j = 0; i < end; i++) {
+ h = src[i] >> 4 & 0x0f;
+ l = src[i] & 0x0f;
+ buffer[j++] = (byte) (h > 9 ? h - 10 + 'A' : h + '0');
+ buffer[j++] = (byte) (l > 9 ? l - 10 + 'A' : l + '0');
+ }
+ return new String(buffer);
+ }
+
+ public static byte[] hexStr2Bytes(String hexStr) {
+ if (TextUtils.isEmpty(hexStr)) {
+ return new byte[0];
+ }
+ int length = hexStr.length() / 2;
+ char[] chars = hexStr.toCharArray();
+ byte[] b = new byte[length];
+ for (int i = 0; i < length; i++) {
+ b[i] = (byte) (char2Byte(chars[i * 2]) << 4 | char2Byte(chars[i * 2 + 1]));
+ }
+ return b;
+ }
+
+ public static byte hexStr2Byte(String hexStr) {
+ return (byte) Integer.parseInt(hexStr, 16);
+ }
+
+ public static String hexStr2Str(String hexStr) {
+ String vi = "0123456789ABC DEF".trim();
+ char[] array = hexStr.toCharArray();
+ byte[] bytes = new byte[hexStr.length() / 2];
+ int temp;
+ for (int i = 0; i < bytes.length; i++) {
+ char c = array[2 * i];
+ temp = vi.indexOf(c) * 16;
+ c = array[2 * i + 1];
+ temp += vi.indexOf(c);
+ bytes[i] = (byte) (temp & 0xFF);
+ }
+ return new String(bytes);
+ }
+
+ public static String hexStr2AsciiStr(String hexStr) {
+ String vi = "0123456789ABC DEF".trim();
+ hexStr = hexStr.trim().replace(" ", "").toUpperCase(Locale.US);
+ char[] array = hexStr.toCharArray();
+ byte[] bytes = new byte[hexStr.length() / 2];
+ int temp = 0x00;
+ for (int i = 0; i < bytes.length; i++) {
+ char c = array[2 * i];
+ temp = vi.indexOf(c) << 4;
+ c = array[2 * i + 1];
+ temp |= vi.indexOf(c);
+ bytes[i] = (byte) (temp & 0xFF);
+ }
+ return new String(bytes);
+ }
+
+ /**
+ * 将无符号short转换成int,大端模式(高位在前)
+ */
+ public static int unsignedShort2IntBE(byte[] src, int offset) {
+ return (src[offset] & 0xff) << 8 | (src[offset + 1] & 0xff);
+ }
+
+ /**
+ * 将无符号short转换成int,小端模式(低位在前)
+ */
+ public static int unsignedShort2IntLE(byte[] src, int offset) {
+ return (src[offset] & 0xff) | (src[offset + 1] & 0xff) << 8;
+ }
+
+ /**
+ * 将无符号byte转换成int
+ */
+ public static int unsignedByte2Int(byte[] src, int offset) {
+ return src[offset] & 0xFF;
+ }
+
+ /**
+ * 将字节数组转换成int,大端模式(高位在前)
+ */
+ public static int unsignedInt2IntBE(byte[] src, int offset) {
+ int result = 0;
+ for (int i = offset; i < offset + 4; i++) {
+ result |= (src[i] & 0xff) << (offset + 3 - i) * 8;
+ }
+ return result;
+ }
+
+ /**
+ * 将字节数组转换成int,小端模式(低位在前)
+ */
+ public static int unsignedInt2IntLE(byte[] src, int offset) {
+ int value = 0;
+ for (int i = offset; i < offset + 4; i++) {
+ value |= (src[i] & 0xff) << (i - offset) * 8;
+ }
+ return value;
+ }
+
+ /**
+ * 将int转换成byte数组,大端模式(高位在前)
+ */
+ public static byte[] int2BytesBE(int src) {
+ byte[] result = new byte[4];
+ for (int i = 0; i < 4; i++) {
+ result[i] = (byte) (src >> (3 - i) * 8);
+ }
+ return result;
+ }
+
+ /**
+ * 将int转换成byte数组,小端模式(低位在前)
+ */
+ public static byte[] int2BytesLE(int src) {
+ byte[] result = new byte[4];
+ for (int i = 0; i < 4; i++) {
+ result[i] = (byte) (src >> i * 8);
+ }
+ return result;
+ }
+
+ /**
+ * 将short转换成byte数组,大端模式(高位在前)
+ */
+ public static byte[] short2BytesBE(short src) {
+ byte[] result = new byte[2];
+ for (int i = 0; i < 2; i++) {
+ result[i] = (byte) (src >> (1 - i) * 8);
+ }
+ return result;
+ }
+
+ /**
+ * 将short转换成byte数组,小端模式(低位在前)
+ */
+ public static byte[] short2BytesLE(short src) {
+ byte[] result = new byte[2];
+ for (int i = 0; i < 2; i++) {
+ result[i] = (byte) (src >> i * 8);
+ }
+ return result;
+ }
+
+ /**
+ * 将字节数组列表合并成单个字节数组
+ */
+ public static byte[] concatByteArrays(byte[]... list) {
+ if (list == null || list.length == 0) {
+ return new byte[0];
+ }
+ return concatByteArrays(Arrays.asList(list));
+ }
+
+ /**
+ * 将字节数组列表合并成单个字节数组
+ */
+ public static byte[] concatByteArrays(List list) {
+ if (list == null || list.isEmpty()) {
+ return new byte[0];
+ }
+ int totalLen = 0;
+ for (byte[] b : list) {
+ if (b == null || b.length == 0) {
+ continue;
+ }
+ totalLen += b.length;
+ }
+ byte[] result = new byte[totalLen];
+ int index = 0;
+ for (byte[] b : list) {
+ if (b == null || b.length == 0) {
+ continue;
+ }
+ System.arraycopy(b, 0, result, index, b.length);
+ index += b.length;
+ }
+ return result;
+ }
+
+
+ /**
+ * Convert char to byte
+ *
+ * @param c char
+ * @return byte
+ */
+ private static int char2Byte(char c) {
+ if (c >= 'a') {
+ return (c - 'a' + 10) & 0x0f;
+ }
+ if (c >= 'A') {
+ return (c - 'A' + 10) & 0x0f;
+ }
+ return (c - '0') & 0x0f;
+ }
+
+
+}
diff --git a/app/src/main/java/com/example/bdkipoc/utils/LogUtil.java b/app/src/main/java/com/example/bdkipoc/utils/LogUtil.java
new file mode 100644
index 0000000..f62df04
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/utils/LogUtil.java
@@ -0,0 +1,89 @@
+package com.example.bdkipoc.utils;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+public class LogUtil {
+
+ public static final int VERBOSE = 1;
+ public static final int DEBUG = 2;
+ public static final int INFO = 3;
+ public static final int WARN = 4;
+ public static final int ERROR = 5;
+ public static final int NOTHING = 6;
+ public static int LEVEL = VERBOSE;
+
+ public static void setLevel(int Level) {
+ LEVEL = Level;
+ }
+
+ public static void v(String TAG, String msg) {
+ if (LEVEL <= VERBOSE && !TextUtils.isEmpty(msg)) {
+ MyLog(VERBOSE, TAG, msg);
+ }
+ }
+
+ public static void d(String TAG, String msg) {
+ if (LEVEL <= DEBUG && !TextUtils.isEmpty(msg)) {
+ MyLog(DEBUG, TAG, msg);
+ }
+ }
+
+ public static void i(String TAG, String msg) {
+ if (LEVEL <= INFO && !TextUtils.isEmpty(msg)) {
+ MyLog(INFO, TAG, msg);
+ }
+ }
+
+ public static void w(String TAG, String msg) {
+ if (LEVEL <= WARN && !TextUtils.isEmpty(msg)) {
+ MyLog(WARN, TAG, msg);
+ }
+ }
+
+ public static void e(String TAG, String msg) {
+ if (LEVEL <= ERROR && !TextUtils.isEmpty(msg)) {
+ MyLog(ERROR, TAG, msg);
+ }
+ }
+
+ private static void MyLog(int type, String TAG, String msg) {
+ StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ int index = 4;
+ String className = stackTrace[index].getFileName();
+ String methodName = stackTrace[index].getMethodName();
+ int lineNumber = stackTrace[index].getLineNumber();
+ methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("[ (")
+ .append(className)
+ .append(":")
+ .append(lineNumber)
+ .append(")#")
+ .append(methodName)
+ .append(" ] ");
+ stringBuilder.append(msg);
+ String logStr = stringBuilder.toString();
+ switch (type) {
+ case VERBOSE:
+ Log.v(TAG, logStr);
+ break;
+ case DEBUG:
+ Log.d(TAG, logStr);
+ break;
+ case INFO:
+ Log.i(TAG, logStr);
+ break;
+ case WARN:
+ Log.w(TAG, logStr);
+ break;
+ case ERROR:
+ Log.e(TAG, logStr);
+ break;
+ default:
+ break;
+ }
+ }
+
+
+}
diff --git a/app/src/main/java/com/example/bdkipoc/utils/Utility.java b/app/src/main/java/com/example/bdkipoc/utils/Utility.java
new file mode 100644
index 0000000..509274f
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/utils/Utility.java
@@ -0,0 +1,107 @@
+package com.example.bdkipoc.utils;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.widget.Toast;
+
+import com.example.bdkipoc.MyApplication;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+public final class Utility {
+ private Utility() {
+ throw new AssertionError("Create instance of Utility is forbidden.");
+ }
+
+ /** Bundle对象转换成字符串 */
+ public static String bundle2String(Bundle bundle) {
+ return bundle2String(bundle, 1);
+ }
+
+ /**
+ * 根据key排序后将Bundle内容拼接成字符串
+ *
+ * @param bundle 要处理的bundle
+ * @param order 排序规则,0-不排序,1-升序,2-降序
+ * @return 拼接后的字符串
+ */
+ public static String bundle2String(Bundle bundle, int order) {
+ if (bundle == null || bundle.keySet().isEmpty()) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ List list = new ArrayList<>(bundle.keySet());
+ if (order == 1) { //升序
+ Collections.sort(list, String::compareTo);
+ } else if (order == 2) {//降序
+ Collections.sort(list, Collections.reverseOrder());
+ }
+ for (String key : list) {
+ sb.append(key);
+ sb.append(":");
+ Object value = bundle.get(key);
+ if (value instanceof byte[]) {
+ sb.append(ByteUtil.bytes2HexStr((byte[]) value));
+ } else {
+ sb.append(value);
+ }
+ sb.append("\n");
+ }
+ if (sb.length() > 0) {
+ sb.deleteCharAt(sb.length() - 1);
+ }
+ return sb.toString();
+ }
+
+ /** 将null转换成空串 */
+ public static String null2String(String str) {
+ return str == null ? "" : str;
+ }
+
+ public static String formatStr(String format, Object... params) {
+ return String.format(Locale.ENGLISH, format, params);
+ }
+
+ /** check whether src is hex format */
+ public static boolean checkHexValue(String src) {
+ return Pattern.matches("[0-9a-fA-F]+", src);
+ }
+
+ /** 显示Toast */
+ public static void showToast(final String msg) {
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(() -> Toast.makeText(MyApplication.app, msg, Toast.LENGTH_SHORT).show());
+ }
+
+ /** 显示Toast */
+ public static void showToast(int resId) {
+ showToast(MyApplication.app.getString(resId));
+ }
+
+ /** 根据结果码获取成功失败信息 */
+ public static String getStateString(int code) {
+ return code == 0 ? "success" : "failed, code:" + code;
+ }
+
+ /** 根据结果状态获取成功失败信息 */
+ public static String getStateString(boolean state) {
+ return state ? "success" : "failed";
+ }
+
+ /** 将dp转成px */
+ public static int dp2px(int dp) {
+ float density = MyApplication.app.getResources().getDisplayMetrics().density;
+ return Math.round(dp * density);
+ }
+
+ /** 将px转成dp */
+ public static int px2dp(int px) {
+ float density = MyApplication.app.getResources().getDisplayMetrics().density;
+ return Math.round(px / density);
+ }
+}
diff --git a/app/src/main/java/com/example/bdkipoc/wrapper/CheckCardCallbackV2Wrapper.java b/app/src/main/java/com/example/bdkipoc/wrapper/CheckCardCallbackV2Wrapper.java
new file mode 100644
index 0000000..b26228a
--- /dev/null
+++ b/app/src/main/java/com/example/bdkipoc/wrapper/CheckCardCallbackV2Wrapper.java
@@ -0,0 +1,44 @@
+package com.example.bdkipoc.wrapper;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import com.sunmi.pay.hardware.aidlv2.readcard.CheckCardCallbackV2;
+
+
+public class CheckCardCallbackV2Wrapper extends CheckCardCallbackV2.Stub {
+ @Override
+ public void findMagCard(Bundle info) throws RemoteException {
+
+ }
+
+ @Override
+ public void findICCard(String atr) throws RemoteException {
+
+ }
+
+ @Override
+ public void findRFCard(String uuid) throws RemoteException {
+
+ }
+
+ @Override
+ public void onError(int code, String message) throws RemoteException {
+
+ }
+
+ @Override
+ public void findICCardEx(Bundle info) throws RemoteException {
+
+ }
+
+ @Override
+ public void findRFCardEx(Bundle info) throws RemoteException {
+
+ }
+
+ @Override
+ public void onErrorEx(Bundle info) throws RemoteException {
+
+ }
+}
diff --git a/app/src/main/res/anim/fade_in.xml b/app/src/main/res/anim/fade_in.xml
index b553a7a..a448dbe 100644
--- a/app/src/main/res/anim/fade_in.xml
+++ b/app/src/main/res/anim/fade_in.xml
@@ -1,4 +1,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_down.xml b/app/src/main/res/anim/slide_down.xml
new file mode 100644
index 0000000..102c5e7
--- /dev/null
+++ b/app/src/main/res/anim/slide_down.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_up.xml b/app/src/main/res/anim/slide_up.xml
new file mode 100644
index 0000000..bfb5615
--- /dev/null
+++ b/app/src/main/res/anim/slide_up.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/banner.png b/app/src/main/res/drawable/banner.png
new file mode 100644
index 0000000..39f882d
Binary files /dev/null and b/app/src/main/res/drawable/banner.png differ
diff --git a/app/src/main/res/drawable/bg_status.xml b/app/src/main/res/drawable/bg_status.xml
new file mode 100644
index 0000000..0d7aeab
--- /dev/null
+++ b/app/src/main/res/drawable/bg_status.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/button_cancel_background.xml b/app/src/main/res/drawable/button_cancel_background.xml
new file mode 100644
index 0000000..db18a5d
--- /dev/null
+++ b/app/src/main/res/drawable/button_cancel_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/button_background_selector.xml b/app/src/main/res/drawable/button_confirm_background_selector.xml
similarity index 100%
rename from app/src/main/res/drawable/button_background_selector.xml
rename to app/src/main/res/drawable/button_confirm_background_selector.xml
diff --git a/app/src/main/res/drawable/ic_e_money.png b/app/src/main/res/drawable/ic_e_money.png
new file mode 100644
index 0000000..f353341
Binary files /dev/null and b/app/src/main/res/drawable/ic_e_money.png differ
diff --git a/app/src/main/res/drawable/ic_e_money.xml b/app/src/main/res/drawable/ic_e_money.xml
deleted file mode 100644
index 7e3db0d..0000000
--- a/app/src/main/res/drawable/ic_e_money.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_settings.png b/app/src/main/res/drawable/ic_settings.png
new file mode 100644
index 0000000..11e6385
Binary files /dev/null and b/app/src/main/res/drawable/ic_settings.png differ
diff --git a/app/src/main/res/drawable/ic_settlement.png b/app/src/main/res/drawable/ic_settlement.png
new file mode 100644
index 0000000..6ae050d
Binary files /dev/null and b/app/src/main/res/drawable/ic_settlement.png differ
diff --git a/app/src/main/res/drawable/ic_settlement.xml b/app/src/main/res/drawable/ic_settlement.xml
deleted file mode 100644
index 89dfa1f..0000000
--- a/app/src/main/res/drawable/ic_settlement.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_transfer.png b/app/src/main/res/drawable/ic_transfer.png
new file mode 100644
index 0000000..4a1ed5f
Binary files /dev/null and b/app/src/main/res/drawable/ic_transfer.png differ
diff --git a/app/src/main/res/drawable/ic_transfer.xml b/app/src/main/res/drawable/ic_transfer.xml
deleted file mode 100644
index e80167e..0000000
--- a/app/src/main/res/drawable/ic_transfer.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/timer_circle_background.xml b/app/src/main/res/drawable/timer_circle_background.xml
new file mode 100644
index 0000000..0a111f2
--- /dev/null
+++ b/app/src/main/res/drawable/timer_circle_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/font/inter.xml b/app/src/main/res/font/inter.xml
index 4b565ce..93a133c 100644
--- a/app/src/main/res/font/inter.xml
+++ b/app/src/main/res/font/inter.xml
@@ -8,4 +8,8 @@
app:font="@font/inter_medium"
app:fontWeight="500"
app:fontStyle="normal"/>
+
\ No newline at end of file
diff --git a/app/src/main/res/font/inter_bold.ttf b/app/src/main/res/font/inter_bold.ttf
new file mode 100644
index 0000000..46b3583
Binary files /dev/null and b/app/src/main/res/font/inter_bold.ttf differ
diff --git a/app/src/main/res/layout/activity_create_transaction.xml b/app/src/main/res/layout/activity_create_transaction.xml
new file mode 100644
index 0000000..16c1c16
--- /dev/null
+++ b/app/src/main/res/layout/activity_create_transaction.xml
@@ -0,0 +1,328 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index dbd7f7a..d7fd674 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -216,7 +216,40 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -316,7 +384,6 @@
-
@@ -350,6 +418,7 @@
+
@@ -391,7 +460,7 @@
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
- android:visibility="visible"
+ android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
@@ -418,9 +487,8 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_payment.xml b/app/src/main/res/layout/activity_payment.xml
deleted file mode 100644
index b98a5da..0000000
--- a/app/src/main/res/layout/activity_payment.xml
+++ /dev/null
@@ -1,268 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_pin.xml b/app/src/main/res/layout/activity_pin.xml
deleted file mode 100644
index 4fc1a4a..0000000
--- a/app/src/main/res/layout/activity_pin.xml
+++ /dev/null
@@ -1,298 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_qris_result.xml b/app/src/main/res/layout/activity_qris_result.xml
index 91569ec..055ba66 100644
--- a/app/src/main/res/layout/activity_qris_result.xml
+++ b/app/src/main/res/layout/activity_qris_result.xml
@@ -1,223 +1,241 @@
-
+ android:background="#F5F5F5"
+ tools:context=".QrisResultActivity">
-
+
-
-
-
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
-
+
+
+
+
+
+ android:text="Generate QR"
+ android:textColor="@android:color/white"
+ android:textSize="18sp"
+ android:textStyle="bold"
+ android:fontFamily="@font/inter" />
-
-
-
-
-
+
+ app:layout_constraintTop_toBottomOf="@id/header_background"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/cancel_button"
+ app:layout_constraintVertical_bias="0.3">
+ android:padding="32dp"
+ android:gravity="center">
-
-
-
-
+
+ android:textColor="#000000"
+ android:letterSpacing="0.1"
+ android:layout_marginBottom="24dp"
+ android:fontFamily="@font/inter" />
+ android:layout_width="240dp"
+ android:layout_height="240dp"
+ android:layout_marginBottom="24dp"
+ android:scaleType="centerInside"
+ android:background="#FFFFFF"
+ android:padding="8dp" />
-
+
+ android:textColor="#333333"
+ android:layout_marginBottom="16dp"
+ android:fontFamily="@font/inter" />
-
+
-
-
-
+ android:gravity="center"
+ android:background="@drawable/timer_circle_background"
+ android:fontFamily="@font/inter" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+ android:textStyle="bold"
+ android:background="@drawable/button_cancel_background"
+ android:fontFamily="@font/inter"
+ android:textAllCaps="false"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_receipt.xml b/app/src/main/res/layout/activity_receipt.xml
index 36e9050..104e1a5 100644
--- a/app/src/main/res/layout/activity_receipt.xml
+++ b/app/src/main/res/layout/activity_receipt.xml
@@ -104,7 +104,7 @@
-
-
@@ -168,46 +159,36 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
+ android:gravity="center"
android:layout_marginBottom="8dp">
-
-
+
+ android:fontFamily="@font/inter"
+ android:paddingHorizontal="8dp"/>
-
diff --git a/app/src/main/res/layout/activity_transaction.xml b/app/src/main/res/layout/activity_reprint.xml
similarity index 96%
rename from app/src/main/res/layout/activity_transaction.xml
rename to app/src/main/res/layout/activity_reprint.xml
index 71f0364..1d4ab03 100644
--- a/app/src/main/res/layout/activity_transaction.xml
+++ b/app/src/main/res/layout/activity_reprint.xml
@@ -79,15 +79,14 @@
android:layout_height="wrap_content"
android:text="Cetak Ulang Struk"
android:textColor="#333333"
- android:textSize="20sp"
+ android:textSize="16sp"
android:textStyle="bold"
- android:fontFamily="sans-serif" />
+ android:fontFamily="inter-bold" />
-
-
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_transaction.xml b/app/src/main/res/layout/item_reprint.xml
similarity index 89%
rename from app/src/main/res/layout/item_transaction.xml
rename to app/src/main/res/layout/item_reprint.xml
index f14d6cd..7669f9a 100644
--- a/app/src/main/res/layout/item_transaction.xml
+++ b/app/src/main/res/layout/item_reprint.xml
@@ -101,16 +101,10 @@
android:clickable="true"
android:focusable="true">
-
-
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 72797eb..5314778 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,9 +1,28 @@
- #FF000000
- #FFFFFFFF
+ #3F51B5
+ #303F9F
+ #FF4081
+
+ #FFFFFF
+ #FF6600
+ #00000000
+
+ #F0F2F5
+ #222222
+ #666666
+ #999999
+ #d7d7d7
+ #FD5A52
+ #E6E6E6
+
+ #FF3C00
+ #999999
+ #000000
+
+ #DE0701
#1976D2
#BBDEFB
#009688
@@ -13,4 +32,88 @@
#F5F5F5
#E0E0E0
#757575
+
+
+
+
+
+ @color/primary_blue
+ @color/colorPrimaryDark
+ @color/white
+ @color/primary_blue
+ @color/medium_gray
+
+
+ @color/colorBackground
+ @color/white
+ @color/white
+
+
+ @color/colorTextTitle
+ @color/colorTextContent
+ @color/colorTextHelp
+ @color/C999999
+ @color/white
+
+
+ @color/accent_green
+ @color/FD5A52
+ @color/colorOrange
+ @color/light_blue
+
+
+ @color/colorOrange
+ @color/accent_green
+ @color/FD5A52
+ @color/dark_gray
+
+
+ @color/white
+ @color/medium_gray
+ @color/colorLineColor
+
+
+ @color/primary_blue
+ @color/light_blue
+ @color/accent_green
+
+
+ @color/colorLineColor
+ @color/medium_gray
+ @color/dark_gray
+
+
+ @color/transparent
+ @color/transparent
+ @color/medium_gray
+
+
+ @color/primary_blue
+ @color/accent_green
+ @color/colorOrange
+ @color/FD5A52
+
+
+ @color/colorTextTitle
+ @color/white
+ @color/light_gray
+
+
+ @color/primary_blue
+ @color/white
+ @color/white
+ @color/light_gray
+
+
+ @color/primary_blue
+ @color/white
+ @color/white
+
+
+ @color/primary_blue
+ @color/dark_gray
+ @color/accent_green
+ @color/FD5A52
+ @color/white
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..a30c936
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+
+
+
+ 24dp
+ 48dp
+ 56dp
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4664116..9e85eed 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -13,4 +13,11 @@
Payment Successful!
Return to Main Screen
POC
+
+ Credit Card Test
+ Magnetic Card Detected
+ IC Card Detected
+ Start Check Card
+ Stop Check Card
+ Connection failed
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 9c6f6f3..f82c7d5 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -29,6 +29,11 @@
- @style/ToolbarTitleStyle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 24e1bf4..b40964f 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,8 +1,9 @@
diff --git a/settings.gradle b/settings.gradle
index 2d00053..2478df1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -16,8 +16,16 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ // Tambahkan repositories Sunmi
+ maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots" }
+ maven { url "https://s01.oss.sonatype.org/content/groups/public/" }
+ maven { url "https://jcenter.bintray.com" }
+ maven { url "https://repo.spring.io/libs-milestone" }
+ flatDir {
+ dirs 'app/libs'
+ }
}
}
rootProject.name = "BDKI POC"
-include ':app'
+include ':app'
\ No newline at end of file