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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +