Compare commits
	
		
			92 Commits
		
	
	
		
			master
			...
			developmen
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ad08e80ae0 | |||
| ccfd3a09eb | |||
| dd57975908 | |||
| a49aab14f8 | |||
| 72b39fd9c8 | |||
| 40d0fc2402 | |||
| 78f9e95c3f | |||
| 69fd69ac3a | |||
| 0e86870b8b | |||
| 1c1d580a38 | |||
| 6d519d96cf | |||
| 64b666869e | |||
| a674574031 | |||
| 8cef8fdb22 | |||
| e0aec6e840 | |||
| 538249fc57 | |||
| a38cea065f | |||
| 671b585fe5 | |||
| c18fd2d831 | |||
| c033a26516 | |||
| f64779755a | |||
| 22d0409c0a | |||
| 2803182a02 | |||
| 960f64ee81 | |||
| 9dac55d07a | |||
| ddf76d2540 | |||
| b2442ada48 | |||
| a52f56e154 | |||
| 4209b193d7 | |||
| 44225f1d67 | |||
| 88069c0b56 | |||
| 6f98a91372 | |||
| 597921e32b | |||
| 3ac3598359 | |||
| 6660fca373 | |||
| edb1c6d09b | |||
| 6f78b6df3f | |||
| da312ec3ae | |||
| 53964211c2 | |||
| b66ef4bb00 | |||
| 7a2ddc3f15 | |||
| 8a73206a76 | |||
| f6650f99d0 | |||
| 8ac97437a2 | |||
| 2b57d35553 | |||
| f2c3de9f5f | |||
| f5d9e53118 | |||
| ece79942c1 | |||
| 0af0e836b1 | |||
| f403358554 | |||
| d43c4bad0c | |||
| 174a1461fd | |||
| f4e5e03077 | |||
| f48e3e64a4 | |||
| 2ea0792d28 | |||
| 9834d4b841 | |||
| 8add903edb | |||
| 124da43a1e | |||
| d7617186a6 | |||
| 93fc410e37 | |||
| 448dfd9835 | |||
| eac3179d8a | |||
| 729bdddad4 | |||
| c56cae64b9 | |||
| d4245c5906 | |||
| eddade3200 | |||
| 13ab6b717e | |||
| 991f77dabe | |||
| da8bcf17cc | |||
| b0ee2e8ee6 | |||
| 4aaa9957e7 | |||
| 99fab68e71 | |||
| 074a4b1f53 | |||
| a1f536b03e | |||
| edca7f92ec | |||
| 3f189f5975 | |||
| 5a03fc3aec | |||
| a30e767adc | |||
| 74f95e0374 | |||
| 1799e7eb0e | |||
| 2a24016637 | |||
| 459d9ab0f1 | |||
| 191966a2e4 | |||
| 46fb81b6a7 | |||
| 290f3015d9 | |||
| f1228db89a | |||
| 810964b4be | |||
| a7fa40d60a | |||
| a07e7a99ac | |||
| c55af6141f | |||
| 6d681f5e41 | |||
| 1ca26371a1 | 
							
								
								
									
										27
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					# ==============================================
 | 
				
			||||||
 | 
					# QRIS PAYMENT CONFIGURATION - ENVIRONMENT VARIABLES
 | 
				
			||||||
 | 
					# ==============================================
 | 
				
			||||||
 | 
					# Copy this file to .env and fill in the values
 | 
				
			||||||
 | 
					# ==============================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Midtrans API Configuration
 | 
				
			||||||
 | 
					MIDTRANS_SANDBOX_AUTH=your_midtrans_sandbox_auth_here
 | 
				
			||||||
 | 
					MIDTRANS_PRODUCTION_AUTH=your_midtrans_production_auth_here
 | 
				
			||||||
 | 
					MIDTRANS_CHARGE_URL=https://api.sandbox.midtrans.com/v2/charge
 | 
				
			||||||
 | 
					MIDTRANS_STATUS_BASE_URL=https://api.sandbox.midtrans.com/v2/
 | 
				
			||||||
 | 
					MIDTRANS_SIMULATOR_URL=https://simulator.sandbox.midtrans.com/v2/qris/index
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Backend Configuration
 | 
				
			||||||
 | 
					BACKEND_BASE_URL=your_backend_base_url_here
 | 
				
			||||||
 | 
					WEBHOOK_URL=your_webhook_url_here
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Application Settings
 | 
				
			||||||
 | 
					MAX_REFRESH_ATTEMPTS=5
 | 
				
			||||||
 | 
					DEFAULT_QR_EXPIRATION_MINUTES=1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# ==============================================
 | 
				
			||||||
 | 
					# INSTRUCTIONS:
 | 
				
			||||||
 | 
					# 1. Copy this file to .env in the same directory
 | 
				
			||||||
 | 
					# 2. Fill in the actual values
 | 
				
			||||||
 | 
					# 3. NEVER commit .env to version control!
 | 
				
			||||||
 | 
					# ==============================================
 | 
				
			||||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -1,5 +1,7 @@
 | 
				
			|||||||
*.iml
 | 
					*.iml
 | 
				
			||||||
.gradle
 | 
					.gradle
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					*.env
 | 
				
			||||||
/local.properties
 | 
					/local.properties
 | 
				
			||||||
/.idea/caches
 | 
					/.idea/caches
 | 
				
			||||||
/.idea/libraries
 | 
					/.idea/libraries
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "java.configuration.updateBuildConfiguration": "automatic"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -6,10 +6,17 @@ android {
 | 
				
			|||||||
    namespace 'com.example.bdkipoc'
 | 
					    namespace 'com.example.bdkipoc'
 | 
				
			||||||
    compileSdk 35
 | 
					    compileSdk 35
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    // Tambahkan lint options
 | 
				
			||||||
 | 
					    lint {
 | 
				
			||||||
 | 
					        abortOnError false
 | 
				
			||||||
 | 
					        disable 'GoogleAppIndexingWarning'
 | 
				
			||||||
 | 
					        disable 'NonConstantResourceId'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    defaultConfig {
 | 
					    defaultConfig {
 | 
				
			||||||
        applicationId "com.example.bdkipoc"
 | 
					        applicationId "com.example.bdkipoc"
 | 
				
			||||||
        minSdk 21
 | 
					        minSdk 21
 | 
				
			||||||
        targetSdk 30
 | 
					        targetSdk 33
 | 
				
			||||||
        versionCode 1
 | 
					        versionCode 1
 | 
				
			||||||
        versionName "1.0"
 | 
					        versionName "1.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,19 +29,51 @@ android {
 | 
				
			|||||||
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
 | 
					            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Keep Java 11 - lebih modern dari referensi
 | 
				
			||||||
    compileOptions {
 | 
					    compileOptions {
 | 
				
			||||||
        sourceCompatibility JavaVersion.VERSION_11
 | 
					        sourceCompatibility JavaVersion.VERSION_11
 | 
				
			||||||
        targetCompatibility JavaVersion.VERSION_11
 | 
					        targetCompatibility JavaVersion.VERSION_11
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Tambahkan sourceSets untuk native libs jika diperlukan
 | 
				
			||||||
 | 
					    sourceSets {
 | 
				
			||||||
 | 
					        main {
 | 
				
			||||||
 | 
					            jniLibs.srcDirs = ['libs']
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    buildFeatures {
 | 
				
			||||||
 | 
					        buildConfig true  // Ini yang mengaktifkan fitur BuildConfig
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    defaultConfig {
 | 
				
			||||||
 | 
					        // Tambahkan semua buildConfigField yang dibutuhkan
 | 
				
			||||||
 | 
					        buildConfigField "int", "MAX_REFRESH_ATTEMPTS", "3"
 | 
				
			||||||
 | 
					        buildConfigField "int", "DEFAULT_QR_EXPIRATION_MINUTES", "15"
 | 
				
			||||||
 | 
					        buildConfigField "String", "MIDTRANS_SANDBOX_AUTH", "\"Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc=\""
 | 
				
			||||||
 | 
					        buildConfigField "String", "MIDTRANS_PRODUCTION_AUTH", "\"TWlkLXNlcnZlci1sMlZPalotdVlVanpvNnU4VzAtYmF1a2o=\""
 | 
				
			||||||
 | 
					        buildConfigField "String", "MIDTRANS_CHARGE_URL", "\"https://api.sandbox.midtrans.com/v2/charge\""
 | 
				
			||||||
 | 
					        buildConfigField "String", "MIDTRANS_STATUS_BASE_URL", "\"https://api.sandbox.midtrans.com/v2/\""
 | 
				
			||||||
 | 
					        buildConfigField "String", "BACKEND_BASE_URL", "\"https://be-edc.msvc.app\""
 | 
				
			||||||
 | 
					        buildConfigField "String", "WEBHOOK_URL", "\"https://be-edc.msvc.app/webhooks/midtrans\""
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dependencies {
 | 
					dependencies {
 | 
				
			||||||
 | 
					    implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
 | 
				
			||||||
    implementation libs.appcompat
 | 
					    implementation libs.appcompat
 | 
				
			||||||
    implementation libs.material
 | 
					    implementation libs.material
 | 
				
			||||||
    implementation libs.activity
 | 
					    implementation libs.activity
 | 
				
			||||||
    implementation libs.constraintlayout
 | 
					    implementation libs.constraintlayout
 | 
				
			||||||
    implementation libs.cardview
 | 
					    implementation libs.cardview
 | 
				
			||||||
 | 
					    implementation 'androidx.recyclerview:recyclerview:1.3.0'
 | 
				
			||||||
 | 
					    implementation 'androidx.cardview:cardview:1.0.0'
 | 
				
			||||||
 | 
					    implementation 'com.google.android.material:material:1.11.0'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    implementation 'com.sunmi:printerlibrary:1.0.15'
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Test dependencies
 | 
				
			||||||
    testImplementation libs.junit
 | 
					    testImplementation libs.junit
 | 
				
			||||||
    androidTestImplementation libs.ext.junit
 | 
					    androidTestImplementation libs.ext.junit
 | 
				
			||||||
    androidTestImplementation libs.espresso.core
 | 
					    androidTestImplementation libs.espresso.core
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								app/libs/PayLib-release-2.0.17-sources.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/PayLib-release-2.0.17.aar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libAE_100.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libCPACE_100.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libDPAS_100.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libEFTPOS_001.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libEMVL2Base.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libEMVL2Dirct.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libEMV_100.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libEntry.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libFLASH_001.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libJCB_100.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libMIR_001.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libPAGO_001.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libPURE_001.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libPaypass_100.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libPaywave_100.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libQPBOC_100.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libRupay_001.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libSamsungPay_001.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/armeabi-v7a/libsunmiemvl2.so
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								app/libs/sunmiemvl2split-1.0.1.jar
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -8,7 +8,23 @@
 | 
				
			|||||||
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 | 
					    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 | 
				
			||||||
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 | 
					    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <uses-permission android:name="com.sunmi.perm.LED" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="com.sunmi.perm.MSR" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="com.sunmi.perm.ICC" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="com.sunmi.perm.PINPAD" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="com.sunmi.perm.SECURITY" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="com.sunmi.perm.CONTACTLESS_CARD" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.WAKE_LOCK" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 | 
				
			||||||
 | 
					    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <uses-permission 
 | 
				
			||||||
 | 
					        android:name="android.permission.QUERY_ALL_PACKAGES"
 | 
				
			||||||
 | 
					        tools:ignore="QueryAllPackagesPermission" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <application
 | 
					    <application
 | 
				
			||||||
 | 
					        android:name=".MyApplication"
 | 
				
			||||||
        android:allowBackup="true"
 | 
					        android:allowBackup="true"
 | 
				
			||||||
        android:dataExtractionRules="@xml/data_extraction_rules"
 | 
					        android:dataExtractionRules="@xml/data_extraction_rules"
 | 
				
			||||||
        android:fullBackupContent="@xml/backup_rules"
 | 
					        android:fullBackupContent="@xml/backup_rules"
 | 
				
			||||||
@ -17,6 +33,7 @@
 | 
				
			|||||||
        android:roundIcon="@mipmap/ic_launcher_round"
 | 
					        android:roundIcon="@mipmap/ic_launcher_round"
 | 
				
			||||||
        android:supportsRtl="true"
 | 
					        android:supportsRtl="true"
 | 
				
			||||||
        android:theme="@style/Theme.BDKIPOC"
 | 
					        android:theme="@style/Theme.BDKIPOC"
 | 
				
			||||||
 | 
					        android:usesCleartextTraffic="true"
 | 
				
			||||||
        tools:targetApi="31">
 | 
					        tools:targetApi="31">
 | 
				
			||||||
        <activity
 | 
					        <activity
 | 
				
			||||||
            android:name=".MainActivity"
 | 
					            android:name=".MainActivity"
 | 
				
			||||||
@ -28,12 +45,72 @@
 | 
				
			|||||||
            </intent-filter>
 | 
					            </intent-filter>
 | 
				
			||||||
        </activity>
 | 
					        </activity>
 | 
				
			||||||
        <activity
 | 
					        <activity
 | 
				
			||||||
            android:name=".TransactionActivity"
 | 
					            android:name=".cetakulang.ReprintActivity"
 | 
				
			||||||
            android:exported="false" />
 | 
					            android:exported="false" />
 | 
				
			||||||
        <activity
 | 
					        <activity
 | 
				
			||||||
            android:name=".PaymentActivity"
 | 
					            android:name=".cetakulang.ReprintAdapterActivity"
 | 
				
			||||||
            android:exported="false" />
 | 
					            android:exported="false" />
 | 
				
			||||||
        <activity android:name=".QrisResultActivity" />
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".ReceiptActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".QrisActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- FIXED: Updated to correct package path -->
 | 
				
			||||||
 | 
					        <activity 
 | 
				
			||||||
 | 
					            android:name=".qris.view.QrisResultActivity"            
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".SettlementActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".LoginActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".histori.HistoryActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".histori.HistoryListActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".transaction.CreateTransactionActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />                
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".transaction.ResultTransactionActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".settlement.SettlementActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".settlement.SettlementDetailActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".bantuan.BantuanActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".bantuan.BantuanFormActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".infotoko.InfoTokoActivity"
 | 
				
			||||||
 | 
					            android:exported="false" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <activity android:name="com.sunmi.emv.l2.view.AppSelectActivity"/>
 | 
				
			||||||
    </application>
 | 
					    </application>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</manifest>
 | 
					</manifest>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								app/src/main/java/com/example/bdkipoc/CacheHelper.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								app/src/main/java/com/example/bdkipoc/Constant.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -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";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										323
									
								
								app/src/main/java/com/example/bdkipoc/LoginActivity.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,323 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import android.content.SharedPreferences;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.text.TextUtils;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.view.WindowManager;
 | 
				
			||||||
 | 
					import android.widget.EditText;
 | 
				
			||||||
 | 
					import android.widget.ProgressBar;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.google.android.material.button.MaterialButton;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.io.OutputStream;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.util.concurrent.ExecutorService;
 | 
				
			||||||
 | 
					import java.util.concurrent.Executors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class LoginActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final String TAG = "LoginActivity";
 | 
				
			||||||
 | 
					    private static final String PREFS_NAME = "LoginPrefs";
 | 
				
			||||||
 | 
					    private static final String KEY_TOKEN = "token";
 | 
				
			||||||
 | 
					    private static final String KEY_USER_DATA = "user_data";
 | 
				
			||||||
 | 
					    private static final String KEY_IS_LOGGED_IN = "is_logged_in";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private EditText etIdentifier, etPassword;
 | 
				
			||||||
 | 
					    private MaterialButton btnLogin;
 | 
				
			||||||
 | 
					    private ProgressBar progressBar;
 | 
				
			||||||
 | 
					    private ExecutorService executor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String currentPassword;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onWindowFocusChanged(boolean hasFocus) {
 | 
				
			||||||
 | 
					        super.onWindowFocusChanged(hasFocus);
 | 
				
			||||||
 | 
					        if (hasFocus) {
 | 
				
			||||||
 | 
					            getWindow().getDecorView().setSystemUiVisibility(
 | 
				
			||||||
 | 
					                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_FULLSCREEN
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        // Enable hardware acceleration
 | 
				
			||||||
 | 
					        getWindow().setFlags(
 | 
				
			||||||
 | 
					            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
 | 
				
			||||||
 | 
					            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_login);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check if user is already logged in
 | 
				
			||||||
 | 
					        if (isUserLoggedIn()) {
 | 
				
			||||||
 | 
					            navigateToMainActivity();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        initializeViews();
 | 
				
			||||||
 | 
					        setupListeners();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        executor = Executors.newSingleThreadExecutor();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void initializeViews() {
 | 
				
			||||||
 | 
					        etIdentifier = findViewById(R.id.et_identifier);
 | 
				
			||||||
 | 
					        etPassword = findViewById(R.id.et_password);
 | 
				
			||||||
 | 
					        btnLogin = findViewById(R.id.btn_login);
 | 
				
			||||||
 | 
					        progressBar = findViewById(R.id.progress_bar);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void setupListeners() {
 | 
				
			||||||
 | 
					        btnLogin.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					            String identifier = etIdentifier.getText().toString().trim();
 | 
				
			||||||
 | 
					            String password = etPassword.getText().toString().trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (validateInput(identifier, password)) {
 | 
				
			||||||
 | 
					                performLogin(identifier, password);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private boolean validateInput(String identifier, String password) {
 | 
				
			||||||
 | 
					        if (TextUtils.isEmpty(identifier)) {
 | 
				
			||||||
 | 
					            etIdentifier.setError("Email/Username tidak boleh kosong");
 | 
				
			||||||
 | 
					            etIdentifier.requestFocus();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (TextUtils.isEmpty(password)) {
 | 
				
			||||||
 | 
					            etPassword.setError("Password tidak boleh kosong");
 | 
				
			||||||
 | 
					            etPassword.requestFocus();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (password.length() < 6) {
 | 
				
			||||||
 | 
					            etPassword.setError("Password minimal 6 karakter");
 | 
				
			||||||
 | 
					            etPassword.requestFocus();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void performLogin(String identifier, String password) {
 | 
				
			||||||
 | 
					        setLoadingState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        currentPassword = password;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        executor.execute(() -> {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                // Create JSON payload
 | 
				
			||||||
 | 
					                JSONObject jsonPayload = new JSONObject();
 | 
				
			||||||
 | 
					                jsonPayload.put("identifier", identifier);
 | 
				
			||||||
 | 
					                jsonPayload.put("password", password);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Setup HTTP connection using BuildConfig
 | 
				
			||||||
 | 
					                URL url = new URL(BuildConfig.BACKEND_BASE_URL + "/users/auth");
 | 
				
			||||||
 | 
					                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                connection.setRequestMethod("POST");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("Content-Type", "application/json");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("accept", "*/*");
 | 
				
			||||||
 | 
					                connection.setDoOutput(true);
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(10000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(10000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Send request
 | 
				
			||||||
 | 
					                try (OutputStream os = connection.getOutputStream()) {
 | 
				
			||||||
 | 
					                    byte[] input = jsonPayload.toString().getBytes("utf-8");
 | 
				
			||||||
 | 
					                    os.write(input, 0, input.length);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Get response
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                Log.d(TAG, "Response Code: " + responseCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                BufferedReader reader;
 | 
				
			||||||
 | 
					                if (responseCode >= 200 && responseCode < 300) {
 | 
				
			||||||
 | 
					                    reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    reader = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                String line;
 | 
				
			||||||
 | 
					                while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                    response.append(line);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                reader.close();
 | 
				
			||||||
 | 
					                connection.disconnect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Log.d(TAG, "Response: " + response.toString());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Parse response on main thread
 | 
				
			||||||
 | 
					                runOnUiThread(() -> handleLoginResponse(responseCode, response.toString()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "Login error: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                runOnUiThread(() -> {
 | 
				
			||||||
 | 
					                    setLoadingState(false);
 | 
				
			||||||
 | 
					                    Toast.makeText(this, "Koneksi gagal: " + e.getMessage(), Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void handleLoginResponse(int responseCode, String responseBody) {
 | 
				
			||||||
 | 
					        setLoadingState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            JSONObject jsonResponse = new JSONObject(responseBody);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (responseCode >= 200 && responseCode < 300) {
 | 
				
			||||||
 | 
					                // Login successful
 | 
				
			||||||
 | 
					                String message = jsonResponse.optString("message", "");
 | 
				
			||||||
 | 
					                int status = jsonResponse.optInt("status", 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (status == 200) {
 | 
				
			||||||
 | 
					                    JSONObject result = jsonResponse.getJSONObject("result");
 | 
				
			||||||
 | 
					                    String token = result.getString("token");
 | 
				
			||||||
 | 
					                    JSONObject userData = result.getJSONObject("user");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Log user data to console
 | 
				
			||||||
 | 
					                    logUserData(userData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Save login data
 | 
				
			||||||
 | 
					                    SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
 | 
				
			||||||
 | 
					                    SharedPreferences.Editor editor = prefs.edit();
 | 
				
			||||||
 | 
					                    editor.putString(KEY_TOKEN, token);
 | 
				
			||||||
 | 
					                    editor.putString(KEY_USER_DATA, userData.toString());
 | 
				
			||||||
 | 
					                    editor.putBoolean(KEY_IS_LOGGED_IN, true);
 | 
				
			||||||
 | 
					                    editor.putString("current_password", currentPassword);
 | 
				
			||||||
 | 
					                    editor.apply();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Toast.makeText(this, "Login berhasil! " + message, Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Navigate to MainActivity
 | 
				
			||||||
 | 
					                    navigateToMainActivity();
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Toast.makeText(this, "Login gagal: " + message, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Login failed
 | 
				
			||||||
 | 
					                String errorMessage = jsonResponse.optString("message", "Login gagal");
 | 
				
			||||||
 | 
					                Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                Log.e(TAG, "Login failed: " + errorMessage);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Error parsing response: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Error parsing response", Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Method to log user data to console
 | 
				
			||||||
 | 
					    private void logUserData(JSONObject userData) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            StringBuilder userInfo = new StringBuilder();
 | 
				
			||||||
 | 
					            userInfo.append("\n=== USER LOGIN DETAILS ===");
 | 
				
			||||||
 | 
					            userInfo.append("\nID: ").append(userData.optString("id", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nName: ").append(userData.optString("name", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nEmail: ").append(userData.optString("email", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nRole: ").append(userData.optString("role", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nPhone: ").append(userData.optString("phone", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nPosition: ").append(userData.optString("position", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nMID: ").append(userData.optString("mid", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nTID: ").append(userData.optString("tid", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nLast Login: ").append(userData.optString("last_login", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\n==========================");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Log.i(TAG, userInfo.toString());
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Error logging user data: " + e.getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void setLoadingState(boolean isLoading) {
 | 
				
			||||||
 | 
					        btnLogin.setEnabled(!isLoading);
 | 
				
			||||||
 | 
					        etIdentifier.setEnabled(!isLoading);
 | 
				
			||||||
 | 
					        etPassword.setEnabled(!isLoading);
 | 
				
			||||||
 | 
					        progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (isLoading) {
 | 
				
			||||||
 | 
					            btnLogin.setText("Memproses...");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            btnLogin.setText("MASUK");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private boolean isUserLoggedIn() {
 | 
				
			||||||
 | 
					        SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
 | 
				
			||||||
 | 
					        return prefs.getBoolean(KEY_IS_LOGGED_IN, false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void navigateToMainActivity() {
 | 
				
			||||||
 | 
					        Intent intent = new Intent(LoginActivity.this, MainActivity.class);
 | 
				
			||||||
 | 
					        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 | 
				
			||||||
 | 
					        startActivity(intent);
 | 
				
			||||||
 | 
					        finish();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Public static methods untuk mengakses data login dari activity lain
 | 
				
			||||||
 | 
					    public static String getToken(android.content.Context context) {
 | 
				
			||||||
 | 
					        SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
 | 
				
			||||||
 | 
					        return prefs.getString(KEY_TOKEN, "");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static String getUserData(android.content.Context context) {
 | 
				
			||||||
 | 
					        SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
 | 
				
			||||||
 | 
					        return prefs.getString(KEY_USER_DATA, "");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static JSONObject getUserDataAsJson(android.content.Context context) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            String userData = getUserData(context);
 | 
				
			||||||
 | 
					            if (!TextUtils.isEmpty(userData)) {
 | 
				
			||||||
 | 
					                return new JSONObject(userData);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e("LoginActivity", "Error parsing user data: " + e.getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static void logout(android.content.Context context) {
 | 
				
			||||||
 | 
					        SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
 | 
				
			||||||
 | 
					        SharedPreferences.Editor editor = prefs.edit();
 | 
				
			||||||
 | 
					        editor.clear();
 | 
				
			||||||
 | 
					        editor.apply();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Navigate back to login
 | 
				
			||||||
 | 
					        Intent intent = new Intent(context, LoginActivity.class);
 | 
				
			||||||
 | 
					        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 | 
				
			||||||
 | 
					        context.startActivity(intent);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static boolean isLoggedIn(android.content.Context context) {
 | 
				
			||||||
 | 
					        SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
 | 
				
			||||||
 | 
					        return prefs.getBoolean(KEY_IS_LOGGED_IN, false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onDestroy() {
 | 
				
			||||||
 | 
					        super.onDestroy();
 | 
				
			||||||
 | 
					        if (executor != null && !executor.isShutdown()) {
 | 
				
			||||||
 | 
					            executor.shutdown();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,41 +1,675 @@
 | 
				
			|||||||
package com.example.bdkipoc;
 | 
					package com.example.bdkipoc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
import android.os.Bundle;
 | 
					import android.os.Bundle;
 | 
				
			||||||
import android.view.View;
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
import android.widget.Toast;
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					import android.widget.ImageView;
 | 
				
			||||||
 | 
					import android.view.animation.AccelerateDecelerateInterpolator;
 | 
				
			||||||
 | 
					import android.view.WindowManager;
 | 
				
			||||||
 | 
					import android.view.Menu;
 | 
				
			||||||
 | 
					import android.view.MenuItem;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import androidx.activity.EdgeToEdge;
 | 
					import androidx.activity.EdgeToEdge;
 | 
				
			||||||
import androidx.appcompat.app.AppCompatActivity;
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
import androidx.cardview.widget.CardView;
 | 
					import androidx.cardview.widget.CardView;
 | 
				
			||||||
 | 
					import androidx.constraintlayout.widget.ConstraintLayout;
 | 
				
			||||||
import androidx.core.graphics.Insets;
 | 
					import androidx.core.graphics.Insets;
 | 
				
			||||||
import androidx.core.view.ViewCompat;
 | 
					import androidx.core.view.ViewCompat;
 | 
				
			||||||
import androidx.core.view.WindowInsetsCompat;
 | 
					import androidx.core.view.WindowInsetsCompat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.google.android.material.button.MaterialButton;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.cetakulang.ReprintActivity;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.cetakulang.ReprintAdapterActivity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.histori.HistoryActivity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.transaction.CreateTransactionActivity;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.transaction.ResultTransactionActivity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.settlement.SettlementActivity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.bantuan.BantuanActivity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.infotoko.InfoTokoActivity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class MainActivity extends AppCompatActivity {
 | 
					public class MainActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final String TAG = "MainActivity";
 | 
				
			||||||
 | 
					    private boolean isExpanded = false; // False = showing only 9 main menus, True = showing all 15 menus
 | 
				
			||||||
 | 
					    private MaterialButton btnLainnya;
 | 
				
			||||||
 | 
					    private MaterialButton logoutButton;
 | 
				
			||||||
 | 
					    private TextView tvUserName, tvUserRole;
 | 
				
			||||||
 | 
					    private LinearLayout userInfoSection;
 | 
				
			||||||
 | 
					    private String authToken;
 | 
				
			||||||
 | 
					    private JSONObject userData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onWindowFocusChanged(boolean hasFocus) {
 | 
				
			||||||
 | 
					        super.onWindowFocusChanged(hasFocus);
 | 
				
			||||||
 | 
					        if (hasFocus) {
 | 
				
			||||||
 | 
					            getWindow().getDecorView().setSystemUiVisibility(
 | 
				
			||||||
 | 
					                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_FULLSCREEN
 | 
				
			||||||
 | 
					                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        // Enable hardware acceleration for smoother scrolling
 | 
				
			||||||
 | 
					        getWindow().setFlags(
 | 
				
			||||||
 | 
					            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
 | 
				
			||||||
 | 
					            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        super.onCreate(savedInstanceState);
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check if user is logged in
 | 
				
			||||||
 | 
					        if (!LoginActivity.isLoggedIn(this)) {
 | 
				
			||||||
 | 
					            // User is not logged in, redirect to login
 | 
				
			||||||
 | 
					            Intent intent = new Intent(this, LoginActivity.class);
 | 
				
			||||||
 | 
					            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 | 
				
			||||||
 | 
					            startActivity(intent);
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        EdgeToEdge.enable(this);
 | 
					        EdgeToEdge.enable(this);
 | 
				
			||||||
        setContentView(R.layout.activity_main);
 | 
					        setContentView(R.layout.activity_main);
 | 
				
			||||||
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
 | 
					
 | 
				
			||||||
 | 
					        ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content), (v, insets) -> {
 | 
				
			||||||
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
 | 
					            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
 | 
				
			||||||
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
 | 
					            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
 | 
				
			||||||
            return insets;
 | 
					            return insets;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Set up click listeners for the cards
 | 
					        // Load user data
 | 
				
			||||||
        CardView paymentCard = findViewById(R.id.card_payment);
 | 
					        loadUserData();
 | 
				
			||||||
        CardView transactionsCard = findViewById(R.id.card_transactions);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        paymentCard.setOnClickListener(v -> {
 | 
					        // Initialize views
 | 
				
			||||||
            // Launch payment activity
 | 
					        btnLainnya = findViewById(R.id.btn_lainnya);
 | 
				
			||||||
            startActivity(new android.content.Intent(MainActivity.this, PaymentActivity.class));
 | 
					        logoutButton = findViewById(R.id.logout_button);
 | 
				
			||||||
 | 
					        tvUserName = findViewById(R.id.tv_user_name);
 | 
				
			||||||
 | 
					        tvUserRole = findViewById(R.id.tv_user_role);
 | 
				
			||||||
 | 
					        userInfoSection = findViewById(R.id.user_info_section);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Setup logout button
 | 
				
			||||||
 | 
					        if (logoutButton != null) {
 | 
				
			||||||
 | 
					            logoutButton.setOnClickListener(v -> performLogout());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check if we're returning from a completed transaction
 | 
				
			||||||
 | 
					        checkTransactionCompletion();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Setup initial state - 9 main menus visible, 6 dummy menus hidden
 | 
				
			||||||
 | 
					        setupInitialMenuState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Setup menu listeners
 | 
				
			||||||
 | 
					        setupMenuListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Display user info
 | 
				
			||||||
 | 
					        displayUserInfo();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void loadUserData() {
 | 
				
			||||||
 | 
					        // Get authentication token
 | 
				
			||||||
 | 
					        authToken = LoginActivity.getToken(this);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Get user data
 | 
				
			||||||
 | 
					        userData = LoginActivity.getUserDataAsJson(this);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (userData != null) {
 | 
				
			||||||
 | 
					            StringBuilder userInfo = new StringBuilder();
 | 
				
			||||||
 | 
					            userInfo.append("\n=== CURRENT USER DETAILS ===");
 | 
				
			||||||
 | 
					            userInfo.append("\nID: ").append(userData.optString("id", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nName: ").append(userData.optString("name", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nEmail: ").append(userData.optString("email", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nRole: ").append(userData.optString("role", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nPhone: ").append(userData.optString("phone", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nPosition: ").append(userData.optString("position", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nMID: ").append(userData.optString("mid", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nTID: ").append(userData.optString("tid", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\nLast Login: ").append(userData.optString("last_login", "N/A"));
 | 
				
			||||||
 | 
					            userInfo.append("\n==========================");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Log.i(TAG, userInfo.toString());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void displayUserInfo() {
 | 
				
			||||||
 | 
					        if (userData != null) {
 | 
				
			||||||
 | 
					            String userName = userData.optString("name", "User");
 | 
				
			||||||
 | 
					            String userRole = userData.optString("role", "");
 | 
				
			||||||
 | 
					            String mid = userData.optString("mid", "");
 | 
				
			||||||
 | 
					            String tid = userData.optString("tid", "");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Display welcome message
 | 
				
			||||||
 | 
					            String welcomeMessage = "Selamat datang, " + userName;
 | 
				
			||||||
 | 
					            if (!userRole.isEmpty()) {
 | 
				
			||||||
 | 
					                welcomeMessage += " (" + userRole + ")";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Show welcome toast
 | 
				
			||||||
 | 
					            Toast.makeText(this, welcomeMessage, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Update merchant card with user info
 | 
				
			||||||
 | 
					            if (tvUserName != null) {
 | 
				
			||||||
 | 
					                tvUserName.setText(userName);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (tvUserRole != null && !userRole.isEmpty()) {
 | 
				
			||||||
 | 
					                tvUserRole.setText("(" + userRole + ")");
 | 
				
			||||||
 | 
					                tvUserRole.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					            } else if (tvUserRole != null) {
 | 
				
			||||||
 | 
					                tvUserRole.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Update MID and TID
 | 
				
			||||||
 | 
					            TextView tvStoreName = findViewById(R.id.tv_store_name);
 | 
				
			||||||
 | 
					            TextView tvStoreAddress = findViewById(R.id.tv_store_address);
 | 
				
			||||||
 | 
					            TextView tvMid = findViewById(R.id.tv_mid);
 | 
				
			||||||
 | 
					            TextView tvTid = findViewById(R.id.tv_tid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (tvStoreName != null) {
 | 
				
			||||||
 | 
					                String storeName = userData.optString("store_name", "TOKO KLONTONG PAK EKO");
 | 
				
			||||||
 | 
					                tvStoreName.setText(storeName);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (tvStoreAddress != null) {
 | 
				
			||||||
 | 
					                String storeAddress = userData.optString("store_address", "Ciputat Baru, Tangsel");
 | 
				
			||||||
 | 
					                tvStoreAddress.setText(storeAddress);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (tvMid != null && !mid.isEmpty()) {
 | 
				
			||||||
 | 
					                tvMid.setText("MID: " + mid);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (tvTid != null && !tid.isEmpty()) {
 | 
				
			||||||
 | 
					                tvTid.setText("TID: " + tid);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Show user info section in merchant card
 | 
				
			||||||
 | 
					            if (userInfoSection != null) {
 | 
				
			||||||
 | 
					                userInfoSection.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Method to get auth token for use in other activities
 | 
				
			||||||
 | 
					    public static String getAuthToken(android.content.Context context) {
 | 
				
			||||||
 | 
					        return LoginActivity.getToken(context);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Method to get user data for use in other activities
 | 
				
			||||||
 | 
					    public static JSONObject getUserData(android.content.Context context) {
 | 
				
			||||||
 | 
					        return LoginActivity.getUserDataAsJson(context);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public boolean onCreateOptionsMenu(Menu menu) {
 | 
				
			||||||
 | 
					        // Menu logout sudah ada di custom toolbar, tidak perlu action bar menu
 | 
				
			||||||
 | 
					        return false; // Return false to not show action bar menu
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public boolean onOptionsItemSelected(MenuItem item) {
 | 
				
			||||||
 | 
					        // No longer needed since logout is handled by custom toolbar button
 | 
				
			||||||
 | 
					        return super.onOptionsItemSelected(item);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void performLogout() {
 | 
				
			||||||
 | 
					        // Show confirmation dialog with red theme
 | 
				
			||||||
 | 
					        androidx.appcompat.app.AlertDialog.Builder builder = new androidx.appcompat.app.AlertDialog.Builder(this);
 | 
				
			||||||
 | 
					        builder.setTitle("Logout");
 | 
				
			||||||
 | 
					        builder.setMessage("Apakah Anda yakin ingin keluar dari aplikasi?");
 | 
				
			||||||
 | 
					        builder.setIcon(android.R.drawable.ic_dialog_alert);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set positive button (Ya)
 | 
				
			||||||
 | 
					        builder.setPositiveButton("Ya", (dialog, which) -> {
 | 
				
			||||||
 | 
					            // Show logout progress
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Logging out...", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Perform logout
 | 
				
			||||||
 | 
					            LoginActivity.logout(this);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        transactionsCard.setOnClickListener(v -> {
 | 
					        // Set negative button (Batal)
 | 
				
			||||||
            // Launch transactions activity
 | 
					        builder.setNegativeButton("Batal", (dialog, which) -> {
 | 
				
			||||||
            startActivity(new android.content.Intent(MainActivity.this, TransactionActivity.class));
 | 
					            dialog.dismiss();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Create and show dialog
 | 
				
			||||||
 | 
					        androidx.appcompat.app.AlertDialog dialog = builder.create();
 | 
				
			||||||
 | 
					        dialog.show();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Customize button colors
 | 
				
			||||||
 | 
					        dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE)
 | 
				
			||||||
 | 
					            .setTextColor(getResources().getColor(android.R.color.holo_red_dark));
 | 
				
			||||||
 | 
					        dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE)
 | 
				
			||||||
 | 
					            .setTextColor(getResources().getColor(android.R.color.black));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void setupInitialMenuState() {
 | 
				
			||||||
 | 
					        // 9 main menus should always be visible
 | 
				
			||||||
 | 
					        CardView cardBantuan = findViewById(R.id.card_bantuan);
 | 
				
			||||||
 | 
					        CardView cardInfoToko = findViewById(R.id.card_info_toko);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (cardBantuan != null) {
 | 
				
			||||||
 | 
					            cardBantuan.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (cardInfoToko != null) {
 | 
				
			||||||
 | 
					            cardInfoToko.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 6 dummy menus should be hidden initially
 | 
				
			||||||
 | 
					        CardView[] dummyCards = {
 | 
				
			||||||
 | 
					            findViewById(R.id.card_bantuan),
 | 
				
			||||||
 | 
					            findViewById(R.id.card_info_toko),
 | 
				
			||||||
 | 
					            findViewById(R.id.card_pengaturan),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (CardView card : dummyCards) {
 | 
				
			||||||
 | 
					            if (card != null) {
 | 
				
			||||||
 | 
					                card.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set initial button text
 | 
				
			||||||
 | 
					        isExpanded = false;
 | 
				
			||||||
 | 
					        btnLainnya.setText("Lainnya");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void checkTransactionCompletion() {
 | 
				
			||||||
 | 
					        Intent intent = getIntent();
 | 
				
			||||||
 | 
					        if (intent != null) {
 | 
				
			||||||
 | 
					            boolean transactionCompleted = intent.getBooleanExtra("transaction_completed", false);
 | 
				
			||||||
 | 
					            String transactionAmount = intent.getStringExtra("transaction_amount");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (transactionCompleted) {
 | 
				
			||||||
 | 
					                if (transactionAmount != null) {
 | 
				
			||||||
 | 
					                    Toast.makeText(this, "Transaksi berhasil! Jumlah: Rp " + formatCurrency(transactionAmount), Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Toast.makeText(this, "Transaksi berhasil diselesaikan!", Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String formatCurrency(String amount) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            long amountValue = Long.parseLong(amount);
 | 
				
			||||||
 | 
					            return String.format("%,d", amountValue).replace(',', '.');
 | 
				
			||||||
 | 
					        } catch (NumberFormatException e) {
 | 
				
			||||||
 | 
					            return amount;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void setupMenuListeners() {
 | 
				
			||||||
 | 
					        // Card IDs to set up listeners - Total 15 menu items
 | 
				
			||||||
 | 
					        int[] cardIds = {
 | 
				
			||||||
 | 
					            // Row 1 (Always visible - 3 items)
 | 
				
			||||||
 | 
					            R.id.card_kartu_kredit,
 | 
				
			||||||
 | 
					            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,
 | 
				
			||||||
 | 
					            // Row 3 (Always visible - 3 items)
 | 
				
			||||||
 | 
					            R.id.card_refund,
 | 
				
			||||||
 | 
					            R.id.card_settlement,
 | 
				
			||||||
 | 
					            R.id.card_histori,
 | 
				
			||||||
 | 
					            // Row 4 (Hidden initially - 3 items)
 | 
				
			||||||
 | 
					            R.id.card_bantuan,
 | 
				
			||||||
 | 
					            R.id.card_info_toko,            
 | 
				
			||||||
 | 
					            R.id.card_pengaturan,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set up click listeners for all cards
 | 
				
			||||||
 | 
					        for (int cardId : cardIds) {
 | 
				
			||||||
 | 
					            CardView cardView = findViewById(cardId);
 | 
				
			||||||
 | 
					            if (cardView != null) {
 | 
				
			||||||
 | 
					                cardView.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                    // ✅ ENHANCED: Navigate with payment type information and auth token
 | 
				
			||||||
 | 
					                    if (cardId == R.id.card_kartu_kredit) {
 | 
				
			||||||
 | 
					                        navigateToCreateTransaction("credit_card", cardId, "Kartu Kredit");
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_kartu_debit) {
 | 
				
			||||||
 | 
					                        navigateToCreateTransaction("debit_card", cardId, "Kartu Debit");
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_qris) {
 | 
				
			||||||
 | 
					                        startActivityWithAuth(new Intent(MainActivity.this, QrisActivity.class));
 | 
				
			||||||
 | 
					                    // Col-2
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_transfer) {
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Transfer - Coming Soon", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_uang_elektronik) {
 | 
				
			||||||
 | 
					                        navigateToCreateTransaction("e_money", cardId, "Uang Elektronik");
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_cetak_ulang) {
 | 
				
			||||||
 | 
					                        startActivityWithAuth(new Intent(MainActivity.this, ReprintActivity.class));
 | 
				
			||||||
 | 
					                    // Col-3
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_refund) {
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Refund - Coming Soon", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_settlement) {
 | 
				
			||||||
 | 
					                        startActivityWithAuth(new Intent(MainActivity.this, SettlementActivity.class));
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_histori) {
 | 
				
			||||||
 | 
					                        startActivityWithAuth(new Intent(MainActivity.this, HistoryActivity.class));
 | 
				
			||||||
 | 
					                    // Col-4
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_bantuan) {
 | 
				
			||||||
 | 
					                        startActivityWithAuth(new Intent(MainActivity.this, BantuanActivity.class));
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_info_toko) {
 | 
				
			||||||
 | 
					                        startActivityWithAuth(new Intent(MainActivity.this, InfoTokoActivity.class));
 | 
				
			||||||
 | 
					                    } else if (cardId == R.id.card_pengaturan) {
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Pengaturan - Coming Soon", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        // Fallback for any other cards
 | 
				
			||||||
 | 
					                        navigateToCreateTransaction("credit_card", cardId, "Unknown");
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get references to ONLY the dummy cards that need to be toggled
 | 
				
			||||||
 | 
					        CardView[] toggleableCards = {
 | 
				
			||||||
 | 
					            findViewById(R.id.card_bantuan),
 | 
				
			||||||
 | 
					            findViewById(R.id.card_info_toko),
 | 
				
			||||||
 | 
					            findViewById(R.id.card_pengaturan),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set up "Lainnya" button click listener
 | 
				
			||||||
 | 
					        btnLainnya.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					            isExpanded = !isExpanded;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (isExpanded) {
 | 
				
			||||||
 | 
					                // Show the 6 dummy menus with animation
 | 
				
			||||||
 | 
					                for (CardView card : toggleableCards) {
 | 
				
			||||||
 | 
					                    if (card != null) {
 | 
				
			||||||
 | 
					                        card.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					                        card.setAlpha(0f);
 | 
				
			||||||
 | 
					                        card.animate()
 | 
				
			||||||
 | 
					                            .alpha(1f)
 | 
				
			||||||
 | 
					                            .setDuration(300)
 | 
				
			||||||
 | 
					                            .setInterpolator(new AccelerateDecelerateInterpolator())
 | 
				
			||||||
 | 
					                            .start();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                btnLainnya.setText("Tampilkan Lebih Sedikit");
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Hide the 6 dummy menus with animation
 | 
				
			||||||
 | 
					                for (CardView card : toggleableCards) {
 | 
				
			||||||
 | 
					                    if (card != null) {
 | 
				
			||||||
 | 
					                        card.animate()
 | 
				
			||||||
 | 
					                            .alpha(0f)
 | 
				
			||||||
 | 
					                            .setDuration(300)
 | 
				
			||||||
 | 
					                            .setInterpolator(new AccelerateDecelerateInterpolator())
 | 
				
			||||||
 | 
					                            .withEndAction(() -> card.setVisibility(View.GONE))
 | 
				
			||||||
 | 
					                            .start();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                btnLainnya.setText("Lainnya");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Setup Banner Image
 | 
				
			||||||
 | 
					        ImageView bannerQris = findViewById(R.id.banner_qris);
 | 
				
			||||||
 | 
					        if (bannerQris != null) {
 | 
				
			||||||
 | 
					            bannerQris.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                // Pindah ke halaman QRIS
 | 
				
			||||||
 | 
					                startActivityWithAuth(new Intent(MainActivity.this, QrisActivity.class));
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ✅ NEW: Enhanced navigation method with payment type information and auth token
 | 
				
			||||||
 | 
					    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");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // ✅ NEW: Pass authentication data
 | 
				
			||||||
 | 
					            intent.putExtra("AUTH_TOKEN", authToken);
 | 
				
			||||||
 | 
					            if (userData != null) {
 | 
				
			||||||
 | 
					                intent.putExtra("USER_DATA", userData.toString());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // ✅ DEBUG: Log navigation details
 | 
				
			||||||
 | 
					            Log.d(TAG, "=== NAVIGATING TO CREATE TRANSACTION ===");
 | 
				
			||||||
 | 
					            Log.d(TAG, "Payment Type: " + paymentType);
 | 
				
			||||||
 | 
					            Log.d(TAG, "Card Menu ID: " + cardMenuId);
 | 
				
			||||||
 | 
					            Log.d(TAG, "Card Name: " + cardName);
 | 
				
			||||||
 | 
					            Log.d(TAG, "Auth Token: " + (authToken != null ? "✓" : "✗"));
 | 
				
			||||||
 | 
					            Log.d(TAG, "========================================");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            startActivity(intent);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Error navigating to CreateTransaction: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Error opening transaction: " + e.getMessage(), Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ✅ NEW: Helper method to start activity with authentication data
 | 
				
			||||||
 | 
					    private void startActivityWithAuth(Intent intent) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Add authentication data to intent
 | 
				
			||||||
 | 
					            intent.putExtra("AUTH_TOKEN", authToken);
 | 
				
			||||||
 | 
					            if (userData != null) {
 | 
				
			||||||
 | 
					                intent.putExtra("USER_DATA", userData.toString());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            startActivity(intent);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Error starting activity: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Error opening activity: " + 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 {
 | 
				
			||||||
 | 
					            Log.w(TAG, "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);
 | 
				
			||||||
 | 
					        setIntent(intent);
 | 
				
			||||||
 | 
					        // Check for transaction completion when returning to MainActivity
 | 
				
			||||||
 | 
					        checkTransactionCompletion();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onResume() {
 | 
				
			||||||
 | 
					        super.onResume();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check if user is still logged in
 | 
				
			||||||
 | 
					        if (!LoginActivity.isLoggedIn(this)) {
 | 
				
			||||||
 | 
					            // User is not logged in, redirect to login
 | 
				
			||||||
 | 
					            Intent intent = new Intent(this, LoginActivity.class);
 | 
				
			||||||
 | 
					            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 | 
				
			||||||
 | 
					            startActivity(intent);
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Clear any transaction completion flags to avoid repeated messages
 | 
				
			||||||
 | 
					        getIntent().removeExtra("transaction_completed");
 | 
				
			||||||
 | 
					        getIntent().removeExtra("transaction_amount");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // ✅ NEW: Log resume for debugging
 | 
				
			||||||
 | 
					        Log.d(TAG, "MainActivity resumed");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onPause() {
 | 
				
			||||||
 | 
					        super.onPause();
 | 
				
			||||||
 | 
					        Log.d(TAG, "MainActivity paused");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onDestroy() {
 | 
				
			||||||
 | 
					        super.onDestroy();
 | 
				
			||||||
 | 
					        Log.d(TAG, "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");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Add authentication data
 | 
				
			||||||
 | 
					        intent.putExtra("AUTH_TOKEN", LoginActivity.getToken(context));
 | 
				
			||||||
 | 
					        String userData = LoginActivity.getUserData(context);
 | 
				
			||||||
 | 
					        if (!userData.isEmpty()) {
 | 
				
			||||||
 | 
					            intent.putExtra("USER_DATA", userData);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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 {
 | 
				
			||||||
 | 
					            Log.w(TAG, "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() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "=== 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);
 | 
				
			||||||
 | 
					            Log.d(TAG, 
 | 
				
			||||||
 | 
					                "Card ID: " + cardId + " -> Payment Type: " + paymentType + " -> Name: " + cardName);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "==================================");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										197
									
								
								app/src/main/java/com/example/bdkipoc/MyApplication.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -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();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,557 +0,0 @@
 | 
				
			|||||||
package com.example.bdkipoc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import android.content.Context;
 | 
					 | 
				
			||||||
import android.content.Intent;
 | 
					 | 
				
			||||||
import android.graphics.Bitmap;
 | 
					 | 
				
			||||||
import android.os.AsyncTask;
 | 
					 | 
				
			||||||
import android.os.Bundle;
 | 
					 | 
				
			||||||
import android.util.Log;
 | 
					 | 
				
			||||||
import android.view.MenuItem;
 | 
					 | 
				
			||||||
import android.view.View;
 | 
					 | 
				
			||||||
import android.view.inputmethod.InputMethodManager;
 | 
					 | 
				
			||||||
import android.widget.Button;
 | 
					 | 
				
			||||||
import android.widget.EditText;
 | 
					 | 
				
			||||||
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.appcompat.widget.Toolbar;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import org.json.JSONException;
 | 
					 | 
				
			||||||
import org.json.JSONObject;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.io.BufferedReader;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.io.InputStream;
 | 
					 | 
				
			||||||
import java.io.InputStreamReader;
 | 
					 | 
				
			||||||
import java.io.OutputStream;
 | 
					 | 
				
			||||||
import java.net.HttpURLConnection;
 | 
					 | 
				
			||||||
import java.net.URI;
 | 
					 | 
				
			||||||
import java.net.URISyntaxException;
 | 
					 | 
				
			||||||
import java.net.URL;
 | 
					 | 
				
			||||||
import java.util.Random;
 | 
					 | 
				
			||||||
import java.util.UUID;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public class PaymentActivity extends AppCompatActivity {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private ProgressBar progressBar;
 | 
					 | 
				
			||||||
    private Button initiatePaymentButton;
 | 
					 | 
				
			||||||
    private Button simulatePaymentButton;
 | 
					 | 
				
			||||||
    private ImageView qrCodeImageView;
 | 
					 | 
				
			||||||
    private TextView statusTextView;
 | 
					 | 
				
			||||||
    private EditText editTextAmount;
 | 
					 | 
				
			||||||
    private TextView referenceIdTextView;
 | 
					 | 
				
			||||||
    private View paymentDetailsLayout;
 | 
					 | 
				
			||||||
    private View paymentSuccessLayout;
 | 
					 | 
				
			||||||
    private Button returnToMainButton;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private String transactionId;
 | 
					 | 
				
			||||||
    private String transactionUuid;
 | 
					 | 
				
			||||||
    private String referenceId;
 | 
					 | 
				
			||||||
    private int amount;
 | 
					 | 
				
			||||||
    private JSONObject midtransResponse;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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 WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    protected void onCreate(@Nullable Bundle savedInstanceState) {
 | 
					 | 
				
			||||||
        super.onCreate(savedInstanceState);
 | 
					 | 
				
			||||||
        setContentView(R.layout.activity_payment);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Set up the toolbar
 | 
					 | 
				
			||||||
        Toolbar toolbar = findViewById(R.id.toolbar);
 | 
					 | 
				
			||||||
        setSupportActionBar(toolbar);
 | 
					 | 
				
			||||||
        if (getSupportActionBar() != null) {
 | 
					 | 
				
			||||||
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
 | 
					 | 
				
			||||||
            getSupportActionBar().setDisplayShowHomeEnabled(true);
 | 
					 | 
				
			||||||
            getSupportActionBar().setTitle("QRIS Payment");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Initialize views
 | 
					 | 
				
			||||||
        progressBar = findViewById(R.id.progressBar);
 | 
					 | 
				
			||||||
        initiatePaymentButton = findViewById(R.id.initiatePaymentButton);
 | 
					 | 
				
			||||||
        simulatePaymentButton = findViewById(R.id.simulatePaymentButton);
 | 
					 | 
				
			||||||
        qrCodeImageView = findViewById(R.id.qrCodeImageView);
 | 
					 | 
				
			||||||
        statusTextView = findViewById(R.id.statusTextView);
 | 
					 | 
				
			||||||
        editTextAmount = findViewById(R.id.editTextAmount);
 | 
					 | 
				
			||||||
        referenceIdTextView = findViewById(R.id.referenceIdTextView);
 | 
					 | 
				
			||||||
        paymentDetailsLayout = findViewById(R.id.paymentDetailsLayout);
 | 
					 | 
				
			||||||
        paymentSuccessLayout = findViewById(R.id.paymentSuccessLayout);
 | 
					 | 
				
			||||||
        returnToMainButton = findViewById(R.id.returnToMainButton);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Generate a random amount between 100,000 and 999,999
 | 
					 | 
				
			||||||
        amount = new Random().nextInt(900000) + 100000;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Format and display the amount
 | 
					 | 
				
			||||||
        editTextAmount.setText("");
 | 
					 | 
				
			||||||
        editTextAmount.requestFocus();
 | 
					 | 
				
			||||||
        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 | 
					 | 
				
			||||||
        if (imm != null) {
 | 
					 | 
				
			||||||
            imm.showSoftInput(editTextAmount, InputMethodManager.SHOW_IMPLICIT);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Generate reference ID
 | 
					 | 
				
			||||||
        referenceId = "ref-" + generateRandomString(8);
 | 
					 | 
				
			||||||
        referenceIdTextView.setText(referenceId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Set up click listeners
 | 
					 | 
				
			||||||
        initiatePaymentButton.setOnClickListener(v -> createTransaction());
 | 
					 | 
				
			||||||
        simulatePaymentButton.setOnClickListener(v -> simulateWebhook());
 | 
					 | 
				
			||||||
        returnToMainButton.setOnClickListener(v -> finish());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Initially hide the QR code and payment success views
 | 
					 | 
				
			||||||
        paymentDetailsLayout.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
        paymentSuccessLayout.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
        simulatePaymentButton.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private void createTransaction() {
 | 
					 | 
				
			||||||
        progressBar.setVisibility(View.VISIBLE);
 | 
					 | 
				
			||||||
        initiatePaymentButton.setEnabled(false);
 | 
					 | 
				
			||||||
        statusTextView.setText("Creating transaction...");
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        new CreateTransactionTask().execute();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private void displayQrCode(String qrImageUrl) {
 | 
					 | 
				
			||||||
        new DownloadImageTask().execute(qrImageUrl);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private void simulateWebhook() {
 | 
					 | 
				
			||||||
        progressBar.setVisibility(View.VISIBLE);
 | 
					 | 
				
			||||||
        simulatePaymentButton.setEnabled(false);
 | 
					 | 
				
			||||||
        statusTextView.setText("Processing payment...");
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        new SimulateWebhookTask().execute();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private void showSuccessScreen() {
 | 
					 | 
				
			||||||
        paymentDetailsLayout.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
        paymentSuccessLayout.setVisibility(View.VISIBLE);
 | 
					 | 
				
			||||||
        statusTextView.setText("Payment successful!");
 | 
					 | 
				
			||||||
        progressBar.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private String generateRandomString(int length) {
 | 
					 | 
				
			||||||
        String chars = "abcdefghijklmnopqrstuvwxyz0123456789";
 | 
					 | 
				
			||||||
        StringBuilder sb = new StringBuilder();
 | 
					 | 
				
			||||||
        Random random = new Random();
 | 
					 | 
				
			||||||
        for (int i = 0; i < length; i++) {
 | 
					 | 
				
			||||||
            int index = random.nextInt(chars.length());
 | 
					 | 
				
			||||||
            sb.append(chars.charAt(index));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return sb.toString();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private String getServerKey() {
 | 
					 | 
				
			||||||
        // MIDTRANS_AUTH = 'Basic base64string'
 | 
					 | 
				
			||||||
        String base64 = MIDTRANS_AUTH.replace("Basic ", "");
 | 
					 | 
				
			||||||
        String decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT).toString();
 | 
					 | 
				
			||||||
        // Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present.
 | 
					 | 
				
			||||||
        return decoded.replace(":\n", "");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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) {
 | 
					 | 
				
			||||||
            return "";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public boolean onOptionsItemSelected(MenuItem item) {
 | 
					 | 
				
			||||||
        if (item.getItemId() == android.R.id.home) {
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return super.onOptionsItemSelected(item);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    private class CreateTransactionTask extends AsyncTask<Void, Void, Boolean> {
 | 
					 | 
				
			||||||
        private String errorMessage;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        @Override
 | 
					 | 
				
			||||||
        protected Boolean doInBackground(Void... voids) {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                // Generate a UUID for the transaction
 | 
					 | 
				
			||||||
                transactionUuid = UUID.randomUUID().toString();
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                // Create transaction JSON payload
 | 
					 | 
				
			||||||
                JSONObject payload = new JSONObject();
 | 
					 | 
				
			||||||
                payload.put("type", "PAYMENT");
 | 
					 | 
				
			||||||
                payload.put("channel_category", "RETAIL_OUTLET");
 | 
					 | 
				
			||||||
                payload.put("channel_code", "QRIS");
 | 
					 | 
				
			||||||
                payload.put("reference_id", referenceId);
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                // Read amount from EditText and log it
 | 
					 | 
				
			||||||
                String amountText = editTextAmount.getText().toString().trim();
 | 
					 | 
				
			||||||
                Log.d("MidtransCharge", "Raw amount text: " + amountText);
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    // Parse amount - expecting integer in lowest denomination (Indonesian Rupiah)
 | 
					 | 
				
			||||||
                    amount = Integer.parseInt(amountText);
 | 
					 | 
				
			||||||
                    Log.d("MidtransCharge", "Parsed amount: " + amount);
 | 
					 | 
				
			||||||
                } catch (NumberFormatException e) {
 | 
					 | 
				
			||||||
                    Log.e("MidtransCharge", "Amount parsing error: " + e.getMessage());
 | 
					 | 
				
			||||||
                    errorMessage = "Invalid amount format";
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                payload.put("amount", amount);
 | 
					 | 
				
			||||||
                payload.put("cashflow", "MONEY_IN");
 | 
					 | 
				
			||||||
                payload.put("status", "INIT");
 | 
					 | 
				
			||||||
                payload.put("device_id", 1);
 | 
					 | 
				
			||||||
                payload.put("transaction_uuid", transactionUuid);
 | 
					 | 
				
			||||||
                payload.put("transaction_time_seconds", 0.0);
 | 
					 | 
				
			||||||
                payload.put("device_code", "PB4K252T00021");
 | 
					 | 
				
			||||||
                payload.put("merchant_name", "Marcel Panjaitan");
 | 
					 | 
				
			||||||
                payload.put("mid", "71000026521");
 | 
					 | 
				
			||||||
                payload.put("tid", "73001500");
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                // Make the API call
 | 
					 | 
				
			||||||
                URL url = new URI(BACKEND_BASE + "/transactions").toURL();
 | 
					 | 
				
			||||||
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
					 | 
				
			||||||
                conn.setRequestMethod("POST");
 | 
					 | 
				
			||||||
                conn.setRequestProperty("Content-Type", "application/json");
 | 
					 | 
				
			||||||
                conn.setRequestProperty("Accept", "application/json");
 | 
					 | 
				
			||||||
                conn.setDoOutput(true);
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                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) {
 | 
					 | 
				
			||||||
                    // Read the response
 | 
					 | 
				
			||||||
                    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());
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                    // Parse the response to get transaction ID
 | 
					 | 
				
			||||||
                    JSONObject jsonResponse = new JSONObject(response.toString());
 | 
					 | 
				
			||||||
                    JSONObject data = jsonResponse.getJSONObject("data");
 | 
					 | 
				
			||||||
                    transactionId = String.valueOf(data.getInt("id"));
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                    // Now generate QRIS via Midtrans
 | 
					 | 
				
			||||||
                    return generateQris(amount);
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    // Read error response
 | 
					 | 
				
			||||||
                    BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
 | 
					 | 
				
			||||||
                    StringBuilder response = new StringBuilder();
 | 
					 | 
				
			||||||
                    String responseLine;
 | 
					 | 
				
			||||||
                    while ((responseLine = br.readLine()) != null) {
 | 
					 | 
				
			||||||
                        response.append(responseLine.trim());
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    errorMessage = "Error creating transaction: " + response.toString();
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } catch (Exception e) {
 | 
					 | 
				
			||||||
                Log.e("MidtransCharge", "Exception: " + e.getMessage(), e);
 | 
					 | 
				
			||||||
                errorMessage = "Unexpected error: " + e.getMessage();
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        private boolean generateQris(int amount) {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                // Create QRIS charge JSON payload
 | 
					 | 
				
			||||||
                JSONObject payload = new JSONObject();
 | 
					 | 
				
			||||||
                payload.put("payment_type", "qris");
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                JSONObject transactionDetails = new JSONObject();
 | 
					 | 
				
			||||||
                transactionDetails.put("order_id", transactionUuid);
 | 
					 | 
				
			||||||
                transactionDetails.put("gross_amount", amount);
 | 
					 | 
				
			||||||
                payload.put("transaction_details", transactionDetails);
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                // Log the request details
 | 
					 | 
				
			||||||
                Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL);
 | 
					 | 
				
			||||||
                Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
 | 
					 | 
				
			||||||
                Log.d("MidtransCharge", "Accept: application/json");
 | 
					 | 
				
			||||||
                Log.d("MidtransCharge", "Content-Type: application/json");
 | 
					 | 
				
			||||||
                Log.d("MidtransCharge", "X-Override-Notification: " + WEBHOOK_URL);
 | 
					 | 
				
			||||||
                Log.d("MidtransCharge", "Payload: " + payload.toString());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // Make the 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", WEBHOOK_URL);
 | 
					 | 
				
			||||||
                conn.setDoOutput(true);
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                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) {
 | 
					 | 
				
			||||||
                    InputStream inputStream = conn.getInputStream();
 | 
					 | 
				
			||||||
                    if (inputStream != null) {
 | 
					 | 
				
			||||||
                        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
 | 
					 | 
				
			||||||
                        StringBuilder response = new StringBuilder();
 | 
					 | 
				
			||||||
                        String responseLine;
 | 
					 | 
				
			||||||
                        while ((responseLine = br.readLine()) != null) {
 | 
					 | 
				
			||||||
                            response.append(responseLine.trim());
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        
 | 
					 | 
				
			||||||
                        // Parse the response
 | 
					 | 
				
			||||||
                        midtransResponse = new JSONObject(response.toString());
 | 
					 | 
				
			||||||
                        return true;
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available");
 | 
					 | 
				
			||||||
                        errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No input stream available";
 | 
					 | 
				
			||||||
                        return false;
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    InputStream errorStream = conn.getErrorStream();
 | 
					 | 
				
			||||||
                    if (errorStream != null) {
 | 
					 | 
				
			||||||
                        BufferedReader br = new BufferedReader(new InputStreamReader(errorStream, "utf-8"));
 | 
					 | 
				
			||||||
                        StringBuilder errorResponse = new StringBuilder();
 | 
					 | 
				
			||||||
                        String responseLine;
 | 
					 | 
				
			||||||
                        while ((responseLine = br.readLine()) != null) {
 | 
					 | 
				
			||||||
                            errorResponse.append(responseLine.trim());
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        Log.e("MidtransCharge", "HTTP " + responseCode + ": " + errorResponse.toString());
 | 
					 | 
				
			||||||
                        errorMessage = "Error generating QRIS: HTTP " + responseCode + ": " + errorResponse.toString();
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        Log.e("MidtransCharge", "HTTP " + responseCode + ": No error stream available");
 | 
					 | 
				
			||||||
                        errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No error stream available";
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } catch (Exception e) {
 | 
					 | 
				
			||||||
                Log.e("MidtransCharge", "Exception: " + e.getMessage(), e);
 | 
					 | 
				
			||||||
                errorMessage = "Unexpected error: " + e.getMessage();
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        @Override
 | 
					 | 
				
			||||||
        protected void onPostExecute(Boolean success) {
 | 
					 | 
				
			||||||
            if (success && midtransResponse != null) {
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    // Extract needed values from midtransResponse
 | 
					 | 
				
			||||||
                    JSONObject actions = midtransResponse.getJSONArray("actions").getJSONObject(0);
 | 
					 | 
				
			||||||
                    String qrImageUrl = actions.getString("url");
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                    // Extract transaction_id
 | 
					 | 
				
			||||||
                    String transactionId = midtransResponse.getString("transaction_id");
 | 
					 | 
				
			||||||
                    String transactionTime = midtransResponse.getString("transaction_time");
 | 
					 | 
				
			||||||
                    String acquirer = midtransResponse.getString("acquirer");
 | 
					 | 
				
			||||||
                    String merchantId = midtransResponse.getString("merchant_id");
 | 
					 | 
				
			||||||
                    String exactGrossAmount = midtransResponse.getString("gross_amount");
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                    // Log everything before launching activity
 | 
					 | 
				
			||||||
                    Log.d("MidtransCharge", "Creating QrisResultActivity intent with:");
 | 
					 | 
				
			||||||
                    Log.d("MidtransCharge", "qrImageUrl: " + qrImageUrl);
 | 
					 | 
				
			||||||
                    Log.d("MidtransCharge", "amount: " + amount);
 | 
					 | 
				
			||||||
                    Log.d("MidtransCharge", "referenceId: " + referenceId);
 | 
					 | 
				
			||||||
                    Log.d("MidtransCharge", "transactionUuid (orderId): " + transactionUuid);
 | 
					 | 
				
			||||||
                    Log.d("MidtransCharge", "transaction_id: " + transactionId);
 | 
					 | 
				
			||||||
                    Log.d("MidtransCharge", "exactGrossAmount: " + exactGrossAmount);
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                    // Instead of showing QR inline, launch QrisResultActivity
 | 
					 | 
				
			||||||
                    Intent intent = new Intent(PaymentActivity.this, QrisResultActivity.class);
 | 
					 | 
				
			||||||
                    intent.putExtra("qrImageUrl", qrImageUrl);
 | 
					 | 
				
			||||||
                    intent.putExtra("amount", amount);
 | 
					 | 
				
			||||||
                    intent.putExtra("referenceId", referenceId);
 | 
					 | 
				
			||||||
                    intent.putExtra("orderId", transactionUuid); // Order ID
 | 
					 | 
				
			||||||
                    intent.putExtra("transactionId", transactionId); // Actual Midtrans transaction_id
 | 
					 | 
				
			||||||
                    intent.putExtra("grossAmount", exactGrossAmount); // Exact gross amount from response
 | 
					 | 
				
			||||||
                    intent.putExtra("transactionTime", transactionTime); // For timestamp
 | 
					 | 
				
			||||||
                    intent.putExtra("acquirer", acquirer);
 | 
					 | 
				
			||||||
                    intent.putExtra("merchantId", merchantId);
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                    try {
 | 
					 | 
				
			||||||
                        startActivity(intent);
 | 
					 | 
				
			||||||
                    } catch (Exception e) {
 | 
					 | 
				
			||||||
                        Log.e("MidtransCharge", "Failed to start QrisResultActivity: " + e.getMessage(), e);
 | 
					 | 
				
			||||||
                        Toast.makeText(PaymentActivity.this, "Error launching QR display: " + e.getMessage(), Toast.LENGTH_LONG).show();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    return;
 | 
					 | 
				
			||||||
                } catch (JSONException e) {
 | 
					 | 
				
			||||||
                    Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e);
 | 
					 | 
				
			||||||
                    Toast.makeText(PaymentActivity.this, "Error processing QRIS response", Toast.LENGTH_LONG).show();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                String message = (errorMessage != null && !errorMessage.isEmpty()) ? errorMessage : "Unknown error occurred. Please check Logcat for details.";
 | 
					 | 
				
			||||||
                Toast.makeText(PaymentActivity.this, message, Toast.LENGTH_LONG).show();
 | 
					 | 
				
			||||||
                initiatePaymentButton.setEnabled(true);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            progressBar.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
 | 
					 | 
				
			||||||
        @Override
 | 
					 | 
				
			||||||
        protected Bitmap doInBackground(String... urls) {
 | 
					 | 
				
			||||||
            String urlDisplay = urls[0];
 | 
					 | 
				
			||||||
            Bitmap bitmap = null;
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                URL url = new URI(urlDisplay).toURL();
 | 
					 | 
				
			||||||
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
					 | 
				
			||||||
                connection.setDoInput(true);
 | 
					 | 
				
			||||||
                connection.connect();
 | 
					 | 
				
			||||||
                java.io.InputStream input = connection.getInputStream();
 | 
					 | 
				
			||||||
                bitmap = android.graphics.BitmapFactory.decodeStream(input);
 | 
					 | 
				
			||||||
            } catch (Exception e) {
 | 
					 | 
				
			||||||
                e.printStackTrace();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return bitmap;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @Override
 | 
					 | 
				
			||||||
        protected void onPostExecute(Bitmap result) {
 | 
					 | 
				
			||||||
            if (result != null) {
 | 
					 | 
				
			||||||
                qrCodeImageView.setImageBitmap(result);
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                Toast.makeText(PaymentActivity.this, "Error loading QR code image", Toast.LENGTH_LONG).show();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    private class SimulateWebhookTask extends AsyncTask<Void, Void, Boolean> {
 | 
					 | 
				
			||||||
        private String errorMessage;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        @Override
 | 
					 | 
				
			||||||
        protected Boolean doInBackground(Void... voids) {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                // Wait a moment to simulate real-world timing
 | 
					 | 
				
			||||||
                Thread.sleep(1500);
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                // Get server key and prepare signature
 | 
					 | 
				
			||||||
                String serverKey = getServerKey();
 | 
					 | 
				
			||||||
                String grossAmount = String.valueOf(amount) + ".00";
 | 
					 | 
				
			||||||
                String signatureKey = generateSignature(
 | 
					 | 
				
			||||||
                    transactionUuid,
 | 
					 | 
				
			||||||
                    "200",
 | 
					 | 
				
			||||||
                    grossAmount,
 | 
					 | 
				
			||||||
                    serverKey
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                // Create webhook payload
 | 
					 | 
				
			||||||
                JSONObject payload = new JSONObject();
 | 
					 | 
				
			||||||
                payload.put("transaction_type", "on-us");
 | 
					 | 
				
			||||||
                payload.put("transaction_time", midtransResponse.getString("transaction_time"));
 | 
					 | 
				
			||||||
                payload.put("transaction_status", "settlement");
 | 
					 | 
				
			||||||
                payload.put("transaction_id", midtransResponse.getString("transaction_id"));
 | 
					 | 
				
			||||||
                payload.put("status_message", "midtrans payment notification");
 | 
					 | 
				
			||||||
                payload.put("status_code", "200");
 | 
					 | 
				
			||||||
                payload.put("signature_key", signatureKey);
 | 
					 | 
				
			||||||
                payload.put("settlement_time", midtransResponse.getString("transaction_time"));
 | 
					 | 
				
			||||||
                payload.put("payment_type", "qris");
 | 
					 | 
				
			||||||
                payload.put("order_id", transactionUuid);
 | 
					 | 
				
			||||||
                payload.put("merchant_id", midtransResponse.getString("merchant_id"));
 | 
					 | 
				
			||||||
                payload.put("issuer", midtransResponse.getString("acquirer"));
 | 
					 | 
				
			||||||
                payload.put("gross_amount", grossAmount);
 | 
					 | 
				
			||||||
                payload.put("fraud_status", "accept");
 | 
					 | 
				
			||||||
                payload.put("currency", "IDR");
 | 
					 | 
				
			||||||
                payload.put("acquirer", midtransResponse.getString("acquirer"));
 | 
					 | 
				
			||||||
                payload.put("shopeepay_reference_number", "");
 | 
					 | 
				
			||||||
                payload.put("reference_id", referenceId);
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                // Call the webhook URL
 | 
					 | 
				
			||||||
                URL url = new URI(WEBHOOK_URL).toURL();
 | 
					 | 
				
			||||||
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
					 | 
				
			||||||
                conn.setRequestMethod("POST");
 | 
					 | 
				
			||||||
                conn.setRequestProperty("Content-Type", "application/json");
 | 
					 | 
				
			||||||
                conn.setRequestProperty("Accept", "application/json");
 | 
					 | 
				
			||||||
                conn.setDoOutput(true);
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                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) {
 | 
					 | 
				
			||||||
                    // Wait briefly to allow the backend to process
 | 
					 | 
				
			||||||
                    Thread.sleep(2000);
 | 
					 | 
				
			||||||
                    return checkTransactionStatus();
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    // Read error response
 | 
					 | 
				
			||||||
                    BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
 | 
					 | 
				
			||||||
                    StringBuilder response = new StringBuilder();
 | 
					 | 
				
			||||||
                    String responseLine;
 | 
					 | 
				
			||||||
                    while ((responseLine = br.readLine()) != null) {
 | 
					 | 
				
			||||||
                        response.append(responseLine.trim());
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    errorMessage = "Error simulating payment: " + response.toString();
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } catch (Exception e) {
 | 
					 | 
				
			||||||
                errorMessage = "Error: " + e.getMessage();
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        private boolean checkTransactionStatus() {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                // Check transaction status
 | 
					 | 
				
			||||||
                URL url = new URI(BACKEND_BASE + "/transactions/" + transactionId).toURL();
 | 
					 | 
				
			||||||
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
					 | 
				
			||||||
                conn.setRequestMethod("GET");
 | 
					 | 
				
			||||||
                conn.setRequestProperty("Accept", "application/json");
 | 
					 | 
				
			||||||
                
 | 
					 | 
				
			||||||
                int responseCode = conn.getResponseCode();
 | 
					 | 
				
			||||||
                if (responseCode == 200) {
 | 
					 | 
				
			||||||
                    // Read the response
 | 
					 | 
				
			||||||
                    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());
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                    // Parse the response
 | 
					 | 
				
			||||||
                    JSONObject jsonResponse = new JSONObject(response.toString());
 | 
					 | 
				
			||||||
                    JSONObject data = jsonResponse.getJSONObject("data");
 | 
					 | 
				
			||||||
                    String status = data.getString("status");
 | 
					 | 
				
			||||||
                    
 | 
					 | 
				
			||||||
                    return status.equalsIgnoreCase("SUCCESS");
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    errorMessage = "Error checking transaction status. HTTP response code: " + responseCode;
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } catch (Exception e) {
 | 
					 | 
				
			||||||
                errorMessage = "Error checking transaction status: " + e.getMessage();
 | 
					 | 
				
			||||||
                return false;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        @Override
 | 
					 | 
				
			||||||
        protected void onPostExecute(Boolean success) {
 | 
					 | 
				
			||||||
            if (success) {
 | 
					 | 
				
			||||||
                showSuccessScreen();
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                String message = (errorMessage != null && !errorMessage.isEmpty()) ? errorMessage : "Unknown error occurred. Please check Logcat for details.";
 | 
					 | 
				
			||||||
                Toast.makeText(PaymentActivity.this, message, Toast.LENGTH_LONG).show();
 | 
					 | 
				
			||||||
                simulatePaymentButton.setEnabled(true);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            progressBar.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,287 +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.Button;
 | 
					 | 
				
			||||||
import android.widget.ImageView;
 | 
					 | 
				
			||||||
import android.widget.ProgressBar;
 | 
					 | 
				
			||||||
import android.widget.TextView;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import androidx.annotation.Nullable;
 | 
					 | 
				
			||||||
import androidx.appcompat.app.AppCompatActivity;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public class QrisResultActivity extends AppCompatActivity {
 | 
					 | 
				
			||||||
    private ImageView qrImageView;
 | 
					 | 
				
			||||||
    private TextView amountTextView;
 | 
					 | 
				
			||||||
    private TextView referenceTextView;
 | 
					 | 
				
			||||||
    private Button downloadQrisButton;
 | 
					 | 
				
			||||||
    private Button checkStatusButton;
 | 
					 | 
				
			||||||
    private TextView statusTextView;
 | 
					 | 
				
			||||||
    private Button returnMainButton;
 | 
					 | 
				
			||||||
    private ProgressBar progressBar;
 | 
					 | 
				
			||||||
    private String orderId;
 | 
					 | 
				
			||||||
    private String grossAmount;
 | 
					 | 
				
			||||||
    private String referenceId;
 | 
					 | 
				
			||||||
    private String transactionId;
 | 
					 | 
				
			||||||
    private String transactionTime;
 | 
					 | 
				
			||||||
    private String acquirer;
 | 
					 | 
				
			||||||
    private String merchantId;
 | 
					 | 
				
			||||||
    private String backendBase = "https://be-edc.msvc.app";
 | 
					 | 
				
			||||||
    private String webhookUrl = "https://be-edc.msvc.app/webhooks/midtrans";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    protected void onCreate(@Nullable Bundle savedInstanceState) {
 | 
					 | 
				
			||||||
        super.onCreate(savedInstanceState);
 | 
					 | 
				
			||||||
        setContentView(R.layout.activity_qris_result);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Intent intent = getIntent();
 | 
					 | 
				
			||||||
        String qrImageUrl = intent.getStringExtra("qrImageUrl");
 | 
					 | 
				
			||||||
        int amount = 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");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (orderId == null || transactionId == null) {
 | 
					 | 
				
			||||||
            Log.e("QrisResultFlow", "orderId or transactionId is null! Intent extras: " + intent.getExtras());
 | 
					 | 
				
			||||||
            android.widget.Toast.makeText(this, "Missing transaction details!", android.widget.Toast.LENGTH_LONG).show();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Get the exact amount from the grossAmount string value instead of the integer
 | 
					 | 
				
			||||||
        String amountStr = "Amount: " + grossAmount;
 | 
					 | 
				
			||||||
        amountTextView.setText(amountStr);
 | 
					 | 
				
			||||||
        referenceTextView.setText("Reference ID: " + referenceId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Load QR image
 | 
					 | 
				
			||||||
        new DownloadImageTask(qrImageView).execute(qrImageUrl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Disable check status button initially
 | 
					 | 
				
			||||||
        checkStatusButton.setEnabled(false);
 | 
					 | 
				
			||||||
        // Start polling for pending payment log
 | 
					 | 
				
			||||||
        pollPendingPaymentLog(orderId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Download QRIS button
 | 
					 | 
				
			||||||
        downloadQrisButton.setOnClickListener(new View.OnClickListener() {
 | 
					 | 
				
			||||||
            @Override
 | 
					 | 
				
			||||||
            public void onClick(View v) {
 | 
					 | 
				
			||||||
                qrImageView.setDrawingCacheEnabled(true);
 | 
					 | 
				
			||||||
                Bitmap bitmap = qrImageView.getDrawingCache();
 | 
					 | 
				
			||||||
                if (bitmap != null) {
 | 
					 | 
				
			||||||
                    saveImageToGallery(bitmap, "qris_code_" + System.currentTimeMillis());
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                qrImageView.setDrawingCacheEnabled(false);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Check Payment Status button
 | 
					 | 
				
			||||||
        checkStatusButton.setOnClickListener(new View.OnClickListener() {
 | 
					 | 
				
			||||||
            @Override
 | 
					 | 
				
			||||||
            public void onClick(View v) {
 | 
					 | 
				
			||||||
                simulateWebhook();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Return to Main Screen button
 | 
					 | 
				
			||||||
        returnMainButton.setOnClickListener(new View.OnClickListener() {
 | 
					 | 
				
			||||||
            @Override
 | 
					 | 
				
			||||||
            public void onClick(View v) {
 | 
					 | 
				
			||||||
                Intent intent = new Intent(QrisResultActivity.this, com.example.bdkipoc.MainActivity.class);
 | 
					 | 
				
			||||||
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
 | 
					 | 
				
			||||||
                startActivity(intent);
 | 
					 | 
				
			||||||
                finishAffinity();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private static class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
 | 
					 | 
				
			||||||
        ImageView bmImage;
 | 
					 | 
				
			||||||
        DownloadImageTask(ImageView bmImage) {
 | 
					 | 
				
			||||||
            this.bmImage = bmImage;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        protected Bitmap doInBackground(String... urls) {
 | 
					 | 
				
			||||||
            String urlDisplay = urls[0];
 | 
					 | 
				
			||||||
            Bitmap bitmap = null;
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                URL url = new URI(urlDisplay).toURL();
 | 
					 | 
				
			||||||
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
					 | 
				
			||||||
                connection.setDoInput(true);
 | 
					 | 
				
			||||||
                connection.connect();
 | 
					 | 
				
			||||||
                InputStream input = connection.getInputStream();
 | 
					 | 
				
			||||||
                bitmap = BitmapFactory.decodeStream(input);
 | 
					 | 
				
			||||||
            } catch (Exception e) {
 | 
					 | 
				
			||||||
                e.printStackTrace();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return bitmap;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        protected void onPostExecute(Bitmap result) {
 | 
					 | 
				
			||||||
            if (result != null) {
 | 
					 | 
				
			||||||
                bmImage.setImageBitmap(result);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Save bitmap to gallery
 | 
					 | 
				
			||||||
    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) {
 | 
					 | 
				
			||||||
                android.widget.Toast.makeText(this, "QRIS saved to gallery", android.widget.Toast.LENGTH_SHORT).show();
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                android.widget.Toast.makeText(this, "Failed to save QRIS", android.widget.Toast.LENGTH_SHORT).show();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } catch (Exception e) {
 | 
					 | 
				
			||||||
            android.widget.Toast.makeText(this, "Error saving QRIS: " + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private void pollPendingPaymentLog(final String orderId) {
 | 
					 | 
				
			||||||
        Log.d("QrisResultFlow", "Polling for orderId (transaction_uuid): " + orderId);
 | 
					 | 
				
			||||||
        progressBar.setVisibility(View.VISIBLE);
 | 
					 | 
				
			||||||
        new Thread(() -> {
 | 
					 | 
				
			||||||
            int maxAttempts = 10;
 | 
					 | 
				
			||||||
            int intervalMs = 1500;
 | 
					 | 
				
			||||||
            int attempt = 0;
 | 
					 | 
				
			||||||
            boolean found = false;
 | 
					 | 
				
			||||||
            while (attempt < maxAttempts && !found) {
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}";
 | 
					 | 
				
			||||||
                    Log.d("QrisResultFlow", "Polling URL: " + urlStr);
 | 
					 | 
				
			||||||
                    URL url = new URL(urlStr);
 | 
					 | 
				
			||||||
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
					 | 
				
			||||||
                    conn.setRequestMethod("GET");
 | 
					 | 
				
			||||||
                    conn.setRequestProperty("Accept", "application/json");
 | 
					 | 
				
			||||||
                    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) {
 | 
					 | 
				
			||||||
                            for (int i = 0; i < results.length(); i++) {
 | 
					 | 
				
			||||||
                                JSONObject log = results.getJSONObject(i);
 | 
					 | 
				
			||||||
                                JSONObject reqBody = log.optJSONObject("request_body");
 | 
					 | 
				
			||||||
                                if (reqBody != null && "pending".equals(reqBody.optString("transaction_status"))) {
 | 
					 | 
				
			||||||
                                    found = true;
 | 
					 | 
				
			||||||
                                    break;
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } catch (Exception e) {
 | 
					 | 
				
			||||||
                    Log.e("QrisResultFlow", "Polling error: " + e.getMessage(), e);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if (!found) {
 | 
					 | 
				
			||||||
                    attempt++;
 | 
					 | 
				
			||||||
                    try { Thread.sleep(intervalMs); } catch (InterruptedException ignored) {}
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            final boolean logFound = found;
 | 
					 | 
				
			||||||
            new Handler(Looper.getMainLooper()).post(() -> {
 | 
					 | 
				
			||||||
                progressBar.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
                if (logFound) {
 | 
					 | 
				
			||||||
                    checkStatusButton.setEnabled(true);
 | 
					 | 
				
			||||||
                    android.widget.Toast.makeText(QrisResultActivity.this, "Pending payment log found!", android.widget.Toast.LENGTH_SHORT).show();
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    android.widget.Toast.makeText(QrisResultActivity.this, "Pending payment log NOT found.", android.widget.Toast.LENGTH_LONG).show();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }).start();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Simulate webhook callback
 | 
					 | 
				
			||||||
    private void simulateWebhook() {
 | 
					 | 
				
			||||||
        progressBar.setVisibility(View.VISIBLE);
 | 
					 | 
				
			||||||
        new Thread(() -> {
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                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", transactionId); // Use the actual transaction_id
 | 
					 | 
				
			||||||
                payload.put("status_message", "midtrans payment notification");
 | 
					 | 
				
			||||||
                payload.put("status_code", "200");
 | 
					 | 
				
			||||||
                payload.put("signature_key", "dummy_signature");
 | 
					 | 
				
			||||||
                payload.put("settlement_time", transactionTime != null ? transactionTime : "2025-04-16T06:00:00Z");
 | 
					 | 
				
			||||||
                payload.put("payment_type", "qris");
 | 
					 | 
				
			||||||
                payload.put("order_id", orderId); // Use order_id
 | 
					 | 
				
			||||||
                payload.put("merchant_id", merchantId != null ? merchantId : "DUMMY_MERCHANT_ID");
 | 
					 | 
				
			||||||
                payload.put("issuer", acquirer != null ? acquirer : "gopay");
 | 
					 | 
				
			||||||
                payload.put("gross_amount", grossAmount); // Use exact gross amount
 | 
					 | 
				
			||||||
                payload.put("fraud_status", "accept");
 | 
					 | 
				
			||||||
                payload.put("currency", "IDR");
 | 
					 | 
				
			||||||
                payload.put("acquirer", acquirer != null ? acquirer : "gopay");
 | 
					 | 
				
			||||||
                payload.put("shopeepay_reference_number", "");
 | 
					 | 
				
			||||||
                payload.put("reference_id", referenceId != null ? referenceId : "DUMMY_REFERENCE_ID");
 | 
					 | 
				
			||||||
                Log.d("QrisResultFlow", "Webhook payload: " + payload.toString());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                URL url = new URL(webhookUrl);
 | 
					 | 
				
			||||||
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
					 | 
				
			||||||
                conn.setRequestMethod("POST");
 | 
					 | 
				
			||||||
                conn.setRequestProperty("Content-Type", "application/json");
 | 
					 | 
				
			||||||
                conn.setDoOutput(true);
 | 
					 | 
				
			||||||
                OutputStream os = conn.getOutputStream();
 | 
					 | 
				
			||||||
                os.write(payload.toString().getBytes());
 | 
					 | 
				
			||||||
                os.flush();
 | 
					 | 
				
			||||||
                os.close();
 | 
					 | 
				
			||||||
                int responseCode = conn.getResponseCode();
 | 
					 | 
				
			||||||
                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.toString());
 | 
					 | 
				
			||||||
            } catch (Exception e) {
 | 
					 | 
				
			||||||
                Log.e("QrisResultFlow", "Webhook error: " + e.getMessage(), e);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            new Handler(Looper.getMainLooper()).post(() -> {
 | 
					 | 
				
			||||||
                progressBar.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
                // Proceed to show status/result
 | 
					 | 
				
			||||||
                qrImageView.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
                amountTextView.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
                referenceTextView.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
                downloadQrisButton.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
                checkStatusButton.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
                statusTextView.setVisibility(View.VISIBLE);
 | 
					 | 
				
			||||||
                returnMainButton.setVisibility(View.VISIBLE);
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }).start();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										1463
									
								
								app/src/main/java/com/example/bdkipoc/ReceiptActivity.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										106
									
								
								app/src/main/java/com/example/bdkipoc/StyleHelper.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Context;
 | 
				
			||||||
 | 
					import android.graphics.drawable.GradientDrawable;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.view.ViewGroup;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import androidx.core.content.ContextCompat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class StyleHelper {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Create rounded rectangle drawable programmatically
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static GradientDrawable createRoundedDrawable(int color, int strokeColor, int strokeWidth, int radius) {
 | 
				
			||||||
 | 
					        GradientDrawable drawable = new GradientDrawable();
 | 
				
			||||||
 | 
					        drawable.setShape(GradientDrawable.RECTANGLE);
 | 
				
			||||||
 | 
					        drawable.setColor(color);
 | 
				
			||||||
 | 
					        drawable.setStroke(strokeWidth, strokeColor);
 | 
				
			||||||
 | 
					        drawable.setCornerRadius(radius);
 | 
				
			||||||
 | 
					        return drawable;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Apply search input styling
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void applySearchInputStyle(View view, Context context) {
 | 
				
			||||||
 | 
					        int white = ContextCompat.getColor(context, android.R.color.white);
 | 
				
			||||||
 | 
					        int lightGrey = ContextCompat.getColor(context, android.R.color.darker_gray);
 | 
				
			||||||
 | 
					        // ✅ IMPROVED: Larger corner radius and lighter border like in the image
 | 
				
			||||||
 | 
					        GradientDrawable drawable = createRoundedDrawable(white, lightGrey, 1, 75); // 25dp radius, thinner border
 | 
				
			||||||
 | 
					        view.setBackground(drawable);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Apply filter button styling
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void applyFilterButtonStyle(View view, Context context) {
 | 
				
			||||||
 | 
					        int white = ContextCompat.getColor(context, android.R.color.white);
 | 
				
			||||||
 | 
					        int lightGrey = ContextCompat.getColor(context, android.R.color.darker_gray);
 | 
				
			||||||
 | 
					        // ✅ IMPROVED: Larger corner radius like in the image
 | 
				
			||||||
 | 
					        GradientDrawable drawable = createRoundedDrawable(white, lightGrey, 1, 75); // 25dp radius, thinner border
 | 
				
			||||||
 | 
					        view.setBackground(drawable);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Apply pagination button styling (simple version)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void applyPaginationButtonStyle(View view, Context context, boolean isActive) {
 | 
				
			||||||
 | 
					        int backgroundColor, strokeColor;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (isActive) {
 | 
				
			||||||
 | 
					            backgroundColor = ContextCompat.getColor(context, android.R.color.holo_red_dark);
 | 
				
			||||||
 | 
					            strokeColor = ContextCompat.getColor(context, android.R.color.holo_red_dark);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            backgroundColor = ContextCompat.getColor(context, android.R.color.white);
 | 
				
			||||||
 | 
					            strokeColor = ContextCompat.getColor(context, android.R.color.transparent);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // ✅ IMPROVED: Larger corner radius for modern look (like in the image)
 | 
				
			||||||
 | 
					        GradientDrawable drawable = createRoundedDrawable(backgroundColor, strokeColor, 0, 48); // 16dp radius
 | 
				
			||||||
 | 
					        view.setBackground(drawable);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set text color if it's a TextView
 | 
				
			||||||
 | 
					        if (view instanceof TextView) {
 | 
				
			||||||
 | 
					            int textColor = isActive ? 
 | 
				
			||||||
 | 
					                ContextCompat.getColor(context, android.R.color.white) : 
 | 
				
			||||||
 | 
					                ContextCompat.getColor(context, android.R.color.black);
 | 
				
			||||||
 | 
					            ((TextView) view).setTextColor(textColor);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Apply status text color only (no background badge)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void applyStatusTextColor(TextView textView, Context context, String status) {
 | 
				
			||||||
 | 
					        String statusLower = status != null ? status.toLowerCase() : "";
 | 
				
			||||||
 | 
					        int textColor;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (statusLower.equals("failed") || statusLower.equals("failure") || 
 | 
				
			||||||
 | 
					            statusLower.equals("error") || statusLower.equals("declined") || 
 | 
				
			||||||
 | 
					            statusLower.equals("expire") || statusLower.equals("cancel")) {
 | 
				
			||||||
 | 
					            // Red text for failed/error statuses
 | 
				
			||||||
 | 
					            textColor = ContextCompat.getColor(context, android.R.color.holo_red_dark);
 | 
				
			||||||
 | 
					        } else if (statusLower.equals("success") || statusLower.equals("paid") || 
 | 
				
			||||||
 | 
					                statusLower.equals("settlement") || statusLower.equals("completed") ||
 | 
				
			||||||
 | 
					                statusLower.equals("capture")) {
 | 
				
			||||||
 | 
					            // Green text for successful statuses
 | 
				
			||||||
 | 
					            textColor = ContextCompat.getColor(context, android.R.color.holo_green_dark);
 | 
				
			||||||
 | 
					        } else if (statusLower.equals("pending") || statusLower.equals("processing") || 
 | 
				
			||||||
 | 
					                statusLower.equals("waiting") || statusLower.equals("checking...") || 
 | 
				
			||||||
 | 
					                statusLower.equals("checking")) {
 | 
				
			||||||
 | 
					            // Orange text for pending/processing statuses
 | 
				
			||||||
 | 
					            textColor = ContextCompat.getColor(context, android.R.color.holo_orange_dark);
 | 
				
			||||||
 | 
					        } else if (statusLower.equals("init")) {
 | 
				
			||||||
 | 
					            // Blue text for init status
 | 
				
			||||||
 | 
					            textColor = ContextCompat.getColor(context, android.R.color.holo_blue_dark);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Default gray text for unknown statuses
 | 
				
			||||||
 | 
					            textColor = ContextCompat.getColor(context, android.R.color.darker_gray);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        textView.setTextColor(textColor);
 | 
				
			||||||
 | 
					        textView.setBackground(null); // Remove any background
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,204 +0,0 @@
 | 
				
			|||||||
package com.example.bdkipoc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import android.os.AsyncTask;
 | 
					 | 
				
			||||||
import android.os.Bundle;
 | 
					 | 
				
			||||||
import android.view.View;
 | 
					 | 
				
			||||||
import android.widget.ProgressBar;
 | 
					 | 
				
			||||||
import android.widget.Toast;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import androidx.annotation.Nullable;
 | 
					 | 
				
			||||||
import androidx.appcompat.app.AppCompatActivity;
 | 
					 | 
				
			||||||
import androidx.recyclerview.widget.LinearLayoutManager;
 | 
					 | 
				
			||||||
import androidx.recyclerview.widget.RecyclerView;
 | 
					 | 
				
			||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import org.json.JSONArray;
 | 
					 | 
				
			||||||
import org.json.JSONException;
 | 
					 | 
				
			||||||
import org.json.JSONObject;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.io.BufferedReader;
 | 
					 | 
				
			||||||
import java.io.IOException;
 | 
					 | 
				
			||||||
import java.io.InputStreamReader;
 | 
					 | 
				
			||||||
import java.net.HttpURLConnection;
 | 
					 | 
				
			||||||
import java.net.URI;
 | 
					 | 
				
			||||||
import java.net.URISyntaxException;
 | 
					 | 
				
			||||||
import java.net.URL;
 | 
					 | 
				
			||||||
import java.util.ArrayList;
 | 
					 | 
				
			||||||
import java.util.List;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public class TransactionActivity extends AppCompatActivity {
 | 
					 | 
				
			||||||
    private RecyclerView recyclerView;
 | 
					 | 
				
			||||||
    private TransactionAdapter adapter;
 | 
					 | 
				
			||||||
    private ProgressBar progressBar;
 | 
					 | 
				
			||||||
    private FloatingActionButton refreshButton;
 | 
					 | 
				
			||||||
    private int page = 0;
 | 
					 | 
				
			||||||
    private final int limit = 10;
 | 
					 | 
				
			||||||
    private boolean isLoading = false;
 | 
					 | 
				
			||||||
    private boolean isLastPage = false;
 | 
					 | 
				
			||||||
    private List<Transaction> transactionList = new ArrayList<>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    protected void onCreate(@Nullable Bundle savedInstanceState) {
 | 
					 | 
				
			||||||
        super.onCreate(savedInstanceState);
 | 
					 | 
				
			||||||
        setContentView(R.layout.activity_transaction);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Set up the toolbar as the action bar
 | 
					 | 
				
			||||||
        androidx.appcompat.widget.Toolbar toolbar = findViewById(R.id.toolbar);
 | 
					 | 
				
			||||||
        setSupportActionBar(toolbar);
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Enable the back button in the action bar
 | 
					 | 
				
			||||||
        if (getSupportActionBar() != null) {
 | 
					 | 
				
			||||||
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
 | 
					 | 
				
			||||||
            getSupportActionBar().setDisplayShowHomeEnabled(true);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        recyclerView = findViewById(R.id.recyclerView);
 | 
					 | 
				
			||||||
        progressBar = findViewById(R.id.progressBar);
 | 
					 | 
				
			||||||
        refreshButton = findViewById(R.id.refreshButton);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        adapter = new TransactionAdapter(transactionList);
 | 
					 | 
				
			||||||
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
 | 
					 | 
				
			||||||
        recyclerView.setAdapter(adapter);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        refreshButton.setOnClickListener(v -> refreshTransactions());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
 | 
					 | 
				
			||||||
            @Override
 | 
					 | 
				
			||||||
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
 | 
					 | 
				
			||||||
                super.onScrolled(recyclerView, dx, dy);
 | 
					 | 
				
			||||||
                if (!recyclerView.canScrollVertically(1) && !isLoading && !isLastPage) {
 | 
					 | 
				
			||||||
                    loadTransactions(page + 1);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        loadTransactions(0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private void refreshTransactions() {
 | 
					 | 
				
			||||||
        page = 0;
 | 
					 | 
				
			||||||
        isLastPage = false;
 | 
					 | 
				
			||||||
        transactionList.clear();
 | 
					 | 
				
			||||||
        adapter.notifyDataSetChanged();
 | 
					 | 
				
			||||||
        loadTransactions(0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private void loadTransactions(int pageToLoad) {
 | 
					 | 
				
			||||||
        isLoading = true;
 | 
					 | 
				
			||||||
        progressBar.setVisibility(View.VISIBLE);
 | 
					 | 
				
			||||||
        new FetchTransactionsTask(pageToLoad).execute();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private class FetchTransactionsTask extends AsyncTask<Void, Void, List<Transaction>> {
 | 
					 | 
				
			||||||
        private int pageToLoad;
 | 
					 | 
				
			||||||
        private boolean error = false;
 | 
					 | 
				
			||||||
        private int total = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        FetchTransactionsTask(int page) {
 | 
					 | 
				
			||||||
            this.pageToLoad = page;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @Override
 | 
					 | 
				
			||||||
        protected List<Transaction> doInBackground(Void... voids) {
 | 
					 | 
				
			||||||
            List<Transaction> result = new ArrayList<>();
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                String urlString = "https://be-edc.msvc.app/transactions?page=" + pageToLoad + "&limit=" + limit + "&sortOrder=DESC&from_date=&to_date=&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=id";
 | 
					 | 
				
			||||||
                URI uri = new URI(urlString);
 | 
					 | 
				
			||||||
                URL url = uri.toURL();
 | 
					 | 
				
			||||||
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
					 | 
				
			||||||
                conn.setRequestMethod("GET");
 | 
					 | 
				
			||||||
                conn.setRequestProperty("accept", "*/*");
 | 
					 | 
				
			||||||
                int responseCode = conn.getResponseCode();
 | 
					 | 
				
			||||||
                if (responseCode == 200) {
 | 
					 | 
				
			||||||
                    BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
 | 
					 | 
				
			||||||
                    StringBuilder response = new StringBuilder();
 | 
					 | 
				
			||||||
                    String line;
 | 
					 | 
				
			||||||
                    while ((line = in.readLine()) != null) {
 | 
					 | 
				
			||||||
                        response.append(line);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    in.close();
 | 
					 | 
				
			||||||
                    JSONObject jsonObject = new JSONObject(response.toString());
 | 
					 | 
				
			||||||
                    JSONObject results = jsonObject.getJSONObject("results");
 | 
					 | 
				
			||||||
                    total = results.getInt("total");
 | 
					 | 
				
			||||||
                    JSONArray data = results.getJSONArray("data");
 | 
					 | 
				
			||||||
                    for (int i = 0; i < data.length(); i++) {
 | 
					 | 
				
			||||||
                        JSONObject t = data.getJSONObject(i);
 | 
					 | 
				
			||||||
                        Transaction tx = new Transaction(
 | 
					 | 
				
			||||||
                                t.getInt("id"),
 | 
					 | 
				
			||||||
                                t.getString("type"),
 | 
					 | 
				
			||||||
                                t.getString("channel_category"),
 | 
					 | 
				
			||||||
                                t.getString("channel_code"),
 | 
					 | 
				
			||||||
                                t.getString("reference_id"),
 | 
					 | 
				
			||||||
                                t.getString("amount"),
 | 
					 | 
				
			||||||
                                t.getString("cashflow"),
 | 
					 | 
				
			||||||
                                t.getString("status"),
 | 
					 | 
				
			||||||
                                t.getString("created_at"),
 | 
					 | 
				
			||||||
                                t.getString("merchant_name")
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                        result.add(tx);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    error = true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            } catch (IOException | JSONException | URISyntaxException e) {
 | 
					 | 
				
			||||||
                error = true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return result;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @Override
 | 
					 | 
				
			||||||
        protected void onPostExecute(List<Transaction> transactions) {
 | 
					 | 
				
			||||||
            isLoading = false;
 | 
					 | 
				
			||||||
            progressBar.setVisibility(View.GONE);
 | 
					 | 
				
			||||||
            if (error) {
 | 
					 | 
				
			||||||
                Toast.makeText(TransactionActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
 | 
					 | 
				
			||||||
                return;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (pageToLoad == 0) {
 | 
					 | 
				
			||||||
                transactionList.clear();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            transactionList.addAll(transactions);
 | 
					 | 
				
			||||||
            adapter.notifyDataSetChanged();
 | 
					 | 
				
			||||||
            page = pageToLoad;
 | 
					 | 
				
			||||||
            if (transactionList.size() >= total) {
 | 
					 | 
				
			||||||
                isLastPage = true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public boolean onOptionsItemSelected(android.view.MenuItem item) {
 | 
					 | 
				
			||||||
        if (item.getItemId() == android.R.id.home) {
 | 
					 | 
				
			||||||
            // Handle the back button click
 | 
					 | 
				
			||||||
            finish();
 | 
					 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return super.onOptionsItemSelected(item);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    static class Transaction {
 | 
					 | 
				
			||||||
        int id;
 | 
					 | 
				
			||||||
        String type;
 | 
					 | 
				
			||||||
        String channelCategory;
 | 
					 | 
				
			||||||
        String channelCode;
 | 
					 | 
				
			||||||
        String referenceId;
 | 
					 | 
				
			||||||
        String amount;
 | 
					 | 
				
			||||||
        String cashflow;
 | 
					 | 
				
			||||||
        String status;
 | 
					 | 
				
			||||||
        String createdAt;
 | 
					 | 
				
			||||||
        String merchantName;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Transaction(int id, String type, String channelCategory, String channelCode, String referenceId, String amount, String cashflow, String status, String createdAt, String merchantName) {
 | 
					 | 
				
			||||||
            this.id = id;
 | 
					 | 
				
			||||||
            this.type = type;
 | 
					 | 
				
			||||||
            this.channelCategory = channelCategory;
 | 
					 | 
				
			||||||
            this.channelCode = channelCode;
 | 
					 | 
				
			||||||
            this.referenceId = referenceId;
 | 
					 | 
				
			||||||
            this.amount = amount;
 | 
					 | 
				
			||||||
            this.cashflow = cashflow;
 | 
					 | 
				
			||||||
            this.status = status;
 | 
					 | 
				
			||||||
            this.createdAt = createdAt;
 | 
					 | 
				
			||||||
            this.merchantName = merchantName;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,64 +0,0 @@
 | 
				
			|||||||
package com.example.bdkipoc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import android.view.LayoutInflater;
 | 
					 | 
				
			||||||
import android.view.View;
 | 
					 | 
				
			||||||
import android.view.ViewGroup;
 | 
					 | 
				
			||||||
import android.widget.TextView;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import androidx.annotation.NonNull;
 | 
					 | 
				
			||||||
import androidx.recyclerview.widget.RecyclerView;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.util.List;
 | 
					 | 
				
			||||||
import java.text.NumberFormat;
 | 
					 | 
				
			||||||
import java.util.Locale;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.TransactionViewHolder> {
 | 
					 | 
				
			||||||
    private List<TransactionActivity.Transaction> transactionList;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public TransactionAdapter(List<TransactionActivity.Transaction> transactionList) {
 | 
					 | 
				
			||||||
        this.transactionList = transactionList;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @NonNull
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 | 
					 | 
				
			||||||
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false);
 | 
					 | 
				
			||||||
        return new TransactionViewHolder(view);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
 | 
					 | 
				
			||||||
        TransactionActivity.Transaction t = transactionList.get(position);
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // Format the amount as Indonesian Rupiah
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            double amountValue = Double.parseDouble(t.amount);
 | 
					 | 
				
			||||||
            NumberFormat rupiahFormat = NumberFormat.getCurrencyInstance(new Locale.Builder().setLanguage("id").setRegion("ID").build());
 | 
					 | 
				
			||||||
            holder.amount.setText(rupiahFormat.format(amountValue));
 | 
					 | 
				
			||||||
        } catch (NumberFormatException e) {
 | 
					 | 
				
			||||||
            holder.amount.setText("Rp " + t.amount);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        holder.status.setText(t.status);
 | 
					 | 
				
			||||||
        holder.referenceId.setText(t.referenceId);
 | 
					 | 
				
			||||||
        holder.merchantName.setText(t.merchantName);
 | 
					 | 
				
			||||||
        holder.createdAt.setText(t.createdAt.replace("T", " ").substring(0, 19));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public int getItemCount() {
 | 
					 | 
				
			||||||
        return transactionList.size();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    static class TransactionViewHolder extends RecyclerView.ViewHolder {
 | 
					 | 
				
			||||||
        TextView amount, status, referenceId, merchantName, createdAt;
 | 
					 | 
				
			||||||
        public TransactionViewHolder(@NonNull View itemView) {
 | 
					 | 
				
			||||||
            super(itemView);
 | 
					 | 
				
			||||||
            amount = itemView.findViewById(R.id.textAmount);
 | 
					 | 
				
			||||||
            status = itemView.findViewById(R.id.textStatus);
 | 
					 | 
				
			||||||
            referenceId = itemView.findViewById(R.id.textReferenceId);
 | 
					 | 
				
			||||||
            merchantName = itemView.findViewById(R.id.textMerchantName);
 | 
					 | 
				
			||||||
            createdAt = itemView.findViewById(R.id.textCreatedAt);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -0,0 +1,574 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.bantuan;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import android.graphics.Color;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.os.Handler;
 | 
				
			||||||
 | 
					import android.os.Looper;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					import android.widget.ScrollView;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					import androidx.core.content.ContextCompat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.LoginActivity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.json.JSONArray;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Collections;
 | 
				
			||||||
 | 
					import java.util.Comparator;
 | 
				
			||||||
 | 
					import java.util.Date;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					import java.util.concurrent.ExecutorService;
 | 
				
			||||||
 | 
					import java.util.concurrent.Executors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class BantuanActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // UI Components
 | 
				
			||||||
 | 
					    private LinearLayout tabUmum, tabRiwayat;
 | 
				
			||||||
 | 
					    private TextView textUmum, textRiwayat;
 | 
				
			||||||
 | 
					    private ScrollView contentUmum, contentRiwayat;
 | 
				
			||||||
 | 
					    private LinearLayout riwayatContainer;
 | 
				
			||||||
 | 
					    private LinearLayout btnForm, btnWhatsApp, backNavigation;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // State
 | 
				
			||||||
 | 
					    private boolean isUmumTabActive = true;
 | 
				
			||||||
 | 
					    private List<TicketData> ticketList = new ArrayList<>();
 | 
				
			||||||
 | 
					    private ExecutorService executor = Executors.newSingleThreadExecutor();
 | 
				
			||||||
 | 
					    private Handler mainHandler = new Handler(Looper.getMainLooper());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // ✅ Enhanced TicketData class with date parsing
 | 
				
			||||||
 | 
					    public static class TicketData {
 | 
				
			||||||
 | 
					        public String createdAt, ticketCode, issueName, status;
 | 
				
			||||||
 | 
					        public Date parsedDate; // Added for sorting
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public TicketData(String createdAt, String ticketCode, String issueName, String status) {
 | 
				
			||||||
 | 
					            this.createdAt = createdAt;
 | 
				
			||||||
 | 
					            this.ticketCode = ticketCode;
 | 
				
			||||||
 | 
					            this.issueName = issueName;
 | 
				
			||||||
 | 
					            this.status = status;
 | 
				
			||||||
 | 
					            this.parsedDate = parseDate(createdAt);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Parse date from ISO format to Date object
 | 
				
			||||||
 | 
					        private Date parseDate(String dateString) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                // Try different date formats
 | 
				
			||||||
 | 
					                SimpleDateFormat[] formats = {
 | 
				
			||||||
 | 
					                    new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()),
 | 
				
			||||||
 | 
					                    new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()),
 | 
				
			||||||
 | 
					                    new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()),
 | 
				
			||||||
 | 
					                    new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                for (SimpleDateFormat format : formats) {
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        return format.parse(dateString);
 | 
				
			||||||
 | 
					                    } catch (Exception e) {
 | 
				
			||||||
 | 
					                        // Continue to next format
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // If all parsing fails, return current date
 | 
				
			||||||
 | 
					                return new Date();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                return new Date(); // Fallback to current date
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // ✅ Check authentication
 | 
				
			||||||
 | 
					        if (!LoginActivity.isLoggedIn(this)) {
 | 
				
			||||||
 | 
					            LoginActivity.logout(this);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_bantuan);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        initViews();
 | 
				
			||||||
 | 
					        setupListeners();
 | 
				
			||||||
 | 
					        showUmumTab();
 | 
				
			||||||
 | 
					        loadTicketData();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void initViews() {
 | 
				
			||||||
 | 
					        // Tabs
 | 
				
			||||||
 | 
					        tabUmum = findViewById(R.id.tab_umum);
 | 
				
			||||||
 | 
					        tabRiwayat = findViewById(R.id.tab_riwayat);
 | 
				
			||||||
 | 
					        textUmum = findViewById(R.id.text_umum);
 | 
				
			||||||
 | 
					        textRiwayat = findViewById(R.id.text_riwayat);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Content
 | 
				
			||||||
 | 
					        contentUmum = findViewById(R.id.content_umum);
 | 
				
			||||||
 | 
					        contentRiwayat = findViewById(R.id.content_riwayat);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Riwayat container
 | 
				
			||||||
 | 
					        if (contentRiwayat != null && contentRiwayat.getChildCount() > 0) {
 | 
				
			||||||
 | 
					            View child = contentRiwayat.getChildAt(0);
 | 
				
			||||||
 | 
					            if (child instanceof LinearLayout) {
 | 
				
			||||||
 | 
					                riwayatContainer = (LinearLayout) child;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Buttons
 | 
				
			||||||
 | 
					        btnForm = findViewById(R.id.btn_form);
 | 
				
			||||||
 | 
					        btnWhatsApp = findViewById(R.id.btn_whatsapp);
 | 
				
			||||||
 | 
					        backNavigation = findViewById(R.id.back_navigation);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupListeners() {
 | 
				
			||||||
 | 
					        if (backNavigation != null) {
 | 
				
			||||||
 | 
					            backNavigation.setOnClickListener(v -> onBackPressed());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (tabUmum != null) {
 | 
				
			||||||
 | 
					            tabUmum.setOnClickListener(v -> showUmumTab());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (tabRiwayat != null) {
 | 
				
			||||||
 | 
					            tabRiwayat.setOnClickListener(v -> showRiwayatTab());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (btnForm != null) {
 | 
				
			||||||
 | 
					            btnForm.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                Intent intent = new Intent(this, BantuanFormActivity.class);
 | 
				
			||||||
 | 
					                startActivity(intent);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (btnWhatsApp != null) {
 | 
				
			||||||
 | 
					            btnWhatsApp.setOnClickListener(v -> openWhatsAppCS());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void showUmumTab() {
 | 
				
			||||||
 | 
					        if (isUmumTabActive) return;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        isUmumTabActive = true;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (contentUmum != null) contentUmum.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        if (contentRiwayat != null) contentRiwayat.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        updateTabAppearance();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void showRiwayatTab() {
 | 
				
			||||||
 | 
					        if (!isUmumTabActive) return;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        isUmumTabActive = false;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (contentUmum != null) contentUmum.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        if (contentRiwayat != null) contentRiwayat.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        updateTabAppearance();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Load data if available
 | 
				
			||||||
 | 
					        if (!ticketList.isEmpty()) {
 | 
				
			||||||
 | 
					            populateRiwayatContent();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            showLoadingMessage();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void updateTabAppearance() {
 | 
				
			||||||
 | 
					        if (isUmumTabActive) {
 | 
				
			||||||
 | 
					            // Umum active
 | 
				
			||||||
 | 
					            if (tabUmum != null) tabUmum.setBackgroundResource(R.drawable.tab_active_bg);
 | 
				
			||||||
 | 
					            if (textUmum != null) textUmum.setTextColor(ContextCompat.getColor(this, android.R.color.white));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Riwayat inactive
 | 
				
			||||||
 | 
					            if (tabRiwayat != null) tabRiwayat.setBackgroundResource(R.drawable.tab_inactive_bg);
 | 
				
			||||||
 | 
					            if (textRiwayat != null) textRiwayat.setTextColor(Color.parseColor("#DE0701"));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Riwayat active
 | 
				
			||||||
 | 
					            if (tabRiwayat != null) tabRiwayat.setBackgroundResource(R.drawable.tab_active_bg);
 | 
				
			||||||
 | 
					            if (textRiwayat != null) textRiwayat.setTextColor(ContextCompat.getColor(this, android.R.color.white));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Umum inactive
 | 
				
			||||||
 | 
					            if (tabUmum != null) tabUmum.setBackgroundResource(R.drawable.tab_inactive_bg);
 | 
				
			||||||
 | 
					            if (textUmum != null) textUmum.setTextColor(Color.parseColor("#DE0701"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // ✅ Simplified API call with new endpoint
 | 
				
			||||||
 | 
					    private void loadTicketData() {
 | 
				
			||||||
 | 
					        String authToken = LoginActivity.getToken(this);
 | 
				
			||||||
 | 
					        if (authToken == null || authToken.isEmpty()) {
 | 
				
			||||||
 | 
					            LoginActivity.logout(this);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        executor.execute(() -> {
 | 
				
			||||||
 | 
					            HttpURLConnection connection = null;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                // ✅ Updated API endpoint
 | 
				
			||||||
 | 
					                URL url = new URL("https://be-edc.msvc.app/tickets/list");
 | 
				
			||||||
 | 
					                connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                connection.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("accept", "application/json");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("Authorization", "Bearer " + authToken);
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(15000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(15000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (responseCode == 200) {
 | 
				
			||||||
 | 
					                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(line);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    reader.close();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    parseTicketData(response.toString());
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } else if (responseCode == 401) {
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        android.widget.Toast.makeText(this, "Session expired. Please login again.", 
 | 
				
			||||||
 | 
					                            android.widget.Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        LoginActivity.logout(this);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        android.widget.Toast.makeText(this, "Failed to load data. Error: " + responseCode, 
 | 
				
			||||||
 | 
					                            android.widget.Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                    android.widget.Toast.makeText(this, "Network error: " + e.getMessage(), 
 | 
				
			||||||
 | 
					                        android.widget.Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					                if (connection != null) {
 | 
				
			||||||
 | 
					                    connection.disconnect();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // ✅ Enhanced JSON parsing with sorting
 | 
				
			||||||
 | 
					    private void parseTicketData(String jsonResponse) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            JSONObject jsonObject = new JSONObject(jsonResponse);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Check for different possible response structures
 | 
				
			||||||
 | 
					            JSONArray dataArray = null;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (jsonObject.has("results") && jsonObject.getJSONObject("results").has("data")) {
 | 
				
			||||||
 | 
					                // Structure: { "results": { "data": [...] } }
 | 
				
			||||||
 | 
					                dataArray = jsonObject.getJSONObject("results").getJSONArray("data");
 | 
				
			||||||
 | 
					            } else if (jsonObject.has("data")) {
 | 
				
			||||||
 | 
					                // Structure: { "data": [...] }
 | 
				
			||||||
 | 
					                dataArray = jsonObject.getJSONArray("data");
 | 
				
			||||||
 | 
					            } else if (jsonObject.has("tickets")) {
 | 
				
			||||||
 | 
					                // Structure: { "tickets": [...] }
 | 
				
			||||||
 | 
					                dataArray = jsonObject.getJSONArray("tickets");
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Assume the response is directly an array
 | 
				
			||||||
 | 
					                dataArray = new JSONArray(jsonResponse);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            List<TicketData> newTicketList = new ArrayList<>();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for (int i = 0; i < dataArray.length(); i++) {
 | 
				
			||||||
 | 
					                JSONObject ticket = dataArray.getJSONObject(i);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String createdAt = ticket.optString("createdAt", ticket.optString("created_at", ""));
 | 
				
			||||||
 | 
					                String ticketCode = ticket.optString("ticket_code", ticket.optString("ticketCode", ""));
 | 
				
			||||||
 | 
					                String status = ticket.optString("status", "");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String issueName = "Tidak ada keterangan";
 | 
				
			||||||
 | 
					                if (ticket.has("issue") && !ticket.isNull("issue")) {
 | 
				
			||||||
 | 
					                    JSONObject issue = ticket.getJSONObject("issue");
 | 
				
			||||||
 | 
					                    issueName = issue.optString("name", issueName);
 | 
				
			||||||
 | 
					                } else if (ticket.has("issue_name")) {
 | 
				
			||||||
 | 
					                    issueName = ticket.optString("issue_name", issueName);
 | 
				
			||||||
 | 
					                } else if (ticket.has("title")) {
 | 
				
			||||||
 | 
					                    issueName = ticket.optString("title", issueName);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (!createdAt.isEmpty() && !ticketCode.isEmpty()) {
 | 
				
			||||||
 | 
					                    newTicketList.add(new TicketData(createdAt, ticketCode, issueName, status));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // ✅ Sort by date - newest first
 | 
				
			||||||
 | 
					            sortTicketsByDate(newTicketList);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                ticketList.clear();
 | 
				
			||||||
 | 
					                ticketList.addAll(newTicketList);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (!isUmumTabActive) {
 | 
				
			||||||
 | 
					                    populateRiwayatContent();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                android.widget.Toast.makeText(this, "Data riwayat berhasil dimuat (" + 
 | 
				
			||||||
 | 
					                    ticketList.size() + " item)", android.widget.Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                android.widget.Toast.makeText(this, "Error parsing data: " + e.getMessage(), 
 | 
				
			||||||
 | 
					                    android.widget.Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // ✅ New method to sort tickets by date (newest first)
 | 
				
			||||||
 | 
					    private void sortTicketsByDate(List<TicketData> tickets) {
 | 
				
			||||||
 | 
					        Collections.sort(tickets, new Comparator<TicketData>() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public int compare(TicketData ticket1, TicketData ticket2) {
 | 
				
			||||||
 | 
					                // Sort by parsedDate in descending order (newest first)
 | 
				
			||||||
 | 
					                if (ticket1.parsedDate == null && ticket2.parsedDate == null) {
 | 
				
			||||||
 | 
					                    return 0;
 | 
				
			||||||
 | 
					                } else if (ticket1.parsedDate == null) {
 | 
				
			||||||
 | 
					                    return 1; // null dates go to the end
 | 
				
			||||||
 | 
					                } else if (ticket2.parsedDate == null) {
 | 
				
			||||||
 | 
					                    return -1; // null dates go to the end
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // Compare dates in descending order (newest first)
 | 
				
			||||||
 | 
					                    return ticket2.parsedDate.compareTo(ticket1.parsedDate);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void populateRiwayatContent() {
 | 
				
			||||||
 | 
					        if (riwayatContainer == null) return;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        riwayatContainer.removeAllViews();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (ticketList.isEmpty()) {
 | 
				
			||||||
 | 
					            showEmptyMessage();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // ✅ Data is already sorted, just populate the UI
 | 
				
			||||||
 | 
					        for (int i = 0; i < ticketList.size(); i++) {
 | 
				
			||||||
 | 
					            TicketData ticket = ticketList.get(i);
 | 
				
			||||||
 | 
					            LinearLayout historyItem = createHistoryItem(ticket, i);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (historyItem != null) {
 | 
				
			||||||
 | 
					                riwayatContainer.addView(historyItem);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Add separator except for last item
 | 
				
			||||||
 | 
					                if (i < ticketList.size() - 1) {
 | 
				
			||||||
 | 
					                    View separator = new View(this);
 | 
				
			||||||
 | 
					                    separator.setLayoutParams(new LinearLayout.LayoutParams(
 | 
				
			||||||
 | 
					                        LinearLayout.LayoutParams.MATCH_PARENT, 1));
 | 
				
			||||||
 | 
					                    separator.setBackgroundColor(Color.parseColor("#e0e0e0"));
 | 
				
			||||||
 | 
					                    riwayatContainer.addView(separator);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // ✅ Enhanced createHistoryItem with position indicator
 | 
				
			||||||
 | 
					    private LinearLayout createHistoryItem(TicketData ticket, int position) {
 | 
				
			||||||
 | 
					        LinearLayout mainLayout = new LinearLayout(this);
 | 
				
			||||||
 | 
					        mainLayout.setLayoutParams(new LinearLayout.LayoutParams(
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.MATCH_PARENT,
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.WRAP_CONTENT));
 | 
				
			||||||
 | 
					        mainLayout.setOrientation(LinearLayout.VERTICAL);
 | 
				
			||||||
 | 
					        mainLayout.setPadding(dpToPx(16), dpToPx(16), dpToPx(16), dpToPx(16));
 | 
				
			||||||
 | 
					        mainLayout.setBackgroundColor(Color.WHITE);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Header (date and status)
 | 
				
			||||||
 | 
					        LinearLayout headerLayout = new LinearLayout(this);
 | 
				
			||||||
 | 
					        headerLayout.setLayoutParams(new LinearLayout.LayoutParams(
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.MATCH_PARENT,
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.WRAP_CONTENT));
 | 
				
			||||||
 | 
					        headerLayout.setOrientation(LinearLayout.HORIZONTAL);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Date with "Terbaru" indicator for first item
 | 
				
			||||||
 | 
					        TextView dateText = new TextView(this);
 | 
				
			||||||
 | 
					        LinearLayout.LayoutParams dateParams = new LinearLayout.LayoutParams(
 | 
				
			||||||
 | 
					            0, LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f);
 | 
				
			||||||
 | 
					        dateText.setLayoutParams(dateParams);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String dateDisplay = formatDate(ticket.createdAt);
 | 
				
			||||||
 | 
					        if (position == 0) {
 | 
				
			||||||
 | 
					            dateDisplay += " (Terbaru)";
 | 
				
			||||||
 | 
					            dateText.setTextColor(Color.parseColor("#DE0701"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        dateText.setText(dateDisplay);
 | 
				
			||||||
 | 
					        dateText.setTextSize(16);
 | 
				
			||||||
 | 
					        dateText.setTypeface(null, android.graphics.Typeface.BOLD);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Status
 | 
				
			||||||
 | 
					        TextView statusText = new TextView(this);
 | 
				
			||||||
 | 
					        statusText.setLayoutParams(new LinearLayout.LayoutParams(
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.WRAP_CONTENT,
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.WRAP_CONTENT));
 | 
				
			||||||
 | 
					        statusText.setText(formatStatus(ticket.status));
 | 
				
			||||||
 | 
					        statusText.setTextSize(14);
 | 
				
			||||||
 | 
					        statusText.setTextColor(getStatusColor(ticket.status));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        headerLayout.addView(dateText);
 | 
				
			||||||
 | 
					        headerLayout.addView(statusText);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Ticket code
 | 
				
			||||||
 | 
					        TextView ticketCodeText = new TextView(this);
 | 
				
			||||||
 | 
					        LinearLayout.LayoutParams ticketParams = new LinearLayout.LayoutParams(
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.MATCH_PARENT,
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.WRAP_CONTENT);
 | 
				
			||||||
 | 
					        ticketParams.setMargins(0, dpToPx(4), 0, 0);
 | 
				
			||||||
 | 
					        ticketCodeText.setLayoutParams(ticketParams);
 | 
				
			||||||
 | 
					        ticketCodeText.setText("Nomor tiket: " + ticket.ticketCode);
 | 
				
			||||||
 | 
					        ticketCodeText.setTextSize(14);
 | 
				
			||||||
 | 
					        ticketCodeText.setTextColor(ContextCompat.getColor(this, android.R.color.darker_gray));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Issue name
 | 
				
			||||||
 | 
					        TextView issueText = new TextView(this);
 | 
				
			||||||
 | 
					        LinearLayout.LayoutParams issueParams = new LinearLayout.LayoutParams(
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.MATCH_PARENT,
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.WRAP_CONTENT);
 | 
				
			||||||
 | 
					        issueParams.setMargins(0, dpToPx(8), 0, 0);
 | 
				
			||||||
 | 
					        issueText.setLayoutParams(issueParams);
 | 
				
			||||||
 | 
					        issueText.setText(ticket.issueName);
 | 
				
			||||||
 | 
					        issueText.setTextSize(16);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        mainLayout.addView(headerLayout);
 | 
				
			||||||
 | 
					        mainLayout.addView(ticketCodeText);
 | 
				
			||||||
 | 
					        mainLayout.addView(issueText);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return mainLayout;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void showLoadingMessage() {
 | 
				
			||||||
 | 
					        if (riwayatContainer == null) return;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        riwayatContainer.removeAllViews();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        TextView loadingText = new TextView(this);
 | 
				
			||||||
 | 
					        loadingText.setLayoutParams(new LinearLayout.LayoutParams(
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.MATCH_PARENT,
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.WRAP_CONTENT));
 | 
				
			||||||
 | 
					        loadingText.setText("Memuat data...");
 | 
				
			||||||
 | 
					        loadingText.setTextSize(16);
 | 
				
			||||||
 | 
					        loadingText.setGravity(android.view.Gravity.CENTER);
 | 
				
			||||||
 | 
					        loadingText.setPadding(dpToPx(16), dpToPx(32), dpToPx(16), dpToPx(32));
 | 
				
			||||||
 | 
					        loadingText.setTextColor(ContextCompat.getColor(this, android.R.color.darker_gray));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        riwayatContainer.addView(loadingText);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void showEmptyMessage() {
 | 
				
			||||||
 | 
					        TextView emptyText = new TextView(this);
 | 
				
			||||||
 | 
					        emptyText.setLayoutParams(new LinearLayout.LayoutParams(
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.MATCH_PARENT,
 | 
				
			||||||
 | 
					            LinearLayout.LayoutParams.WRAP_CONTENT));
 | 
				
			||||||
 | 
					        emptyText.setText("Belum ada data riwayat");
 | 
				
			||||||
 | 
					        emptyText.setTextSize(16);
 | 
				
			||||||
 | 
					        emptyText.setGravity(android.view.Gravity.CENTER);
 | 
				
			||||||
 | 
					        emptyText.setPadding(dpToPx(16), dpToPx(32), dpToPx(16), dpToPx(32));
 | 
				
			||||||
 | 
					        emptyText.setTextColor(ContextCompat.getColor(this, android.R.color.darker_gray));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        riwayatContainer.addView(emptyText);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void openWhatsAppCS() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            JSONObject userData = LoginActivity.getUserDataAsJson(this);
 | 
				
			||||||
 | 
					            String userName = "User";
 | 
				
			||||||
 | 
					            String userEmail = "";
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (userData != null) {
 | 
				
			||||||
 | 
					                userName = userData.optString("name", "User");
 | 
				
			||||||
 | 
					                userEmail = userData.optString("email", "");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            String phoneNumber = "+6281234567890"; // Update with actual CS number
 | 
				
			||||||
 | 
					            String message = "Halo, saya " + userName;
 | 
				
			||||||
 | 
					            if (!userEmail.isEmpty()) {
 | 
				
			||||||
 | 
					                message += " (" + userEmail + ")";
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            message += " butuh bantuan terkait Payvora PRO";
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Intent intent = new Intent(Intent.ACTION_VIEW);
 | 
				
			||||||
 | 
					            intent.setData(android.net.Uri.parse("https://wa.me/" + phoneNumber + "?text=" + 
 | 
				
			||||||
 | 
					                android.net.Uri.encode(message)));
 | 
				
			||||||
 | 
					            startActivity(intent);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            android.widget.Toast.makeText(this, "Error opening WhatsApp: " + e.getMessage(), 
 | 
				
			||||||
 | 
					                android.widget.Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // ✅ Utility methods
 | 
				
			||||||
 | 
					    private String formatDate(String isoDate) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
 | 
				
			||||||
 | 
					            SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
 | 
				
			||||||
 | 
					            Date date = inputFormat.parse(isoDate);
 | 
				
			||||||
 | 
					            return outputFormat.format(date);
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            return isoDate.length() > 10 ? isoDate.substring(0, 10) : isoDate;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatStatus(String status) {
 | 
				
			||||||
 | 
					        switch (status.toLowerCase()) {
 | 
				
			||||||
 | 
					            case "new": return "Pengajuan";
 | 
				
			||||||
 | 
					            case "on_progres": return "Proses";
 | 
				
			||||||
 | 
					            case "done": return "Selesai";
 | 
				
			||||||
 | 
					            case "cancel": return "Cancel";
 | 
				
			||||||
 | 
					            default: return status;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private int getStatusColor(String status) {
 | 
				
			||||||
 | 
					        switch (status.toLowerCase()) {
 | 
				
			||||||
 | 
					            case "new": return ContextCompat.getColor(this, android.R.color.holo_blue_light);
 | 
				
			||||||
 | 
					            case "on_progres": return ContextCompat.getColor(this, android.R.color.holo_orange_light);
 | 
				
			||||||
 | 
					            case "done": return ContextCompat.getColor(this, android.R.color.holo_green_dark);
 | 
				
			||||||
 | 
					            case "cancel": return ContextCompat.getColor(this, android.R.color.holo_red_dark);
 | 
				
			||||||
 | 
					            default: return ContextCompat.getColor(this, android.R.color.black);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private int dpToPx(int dp) {
 | 
				
			||||||
 | 
					        float density = getResources().getDisplayMetrics().density;
 | 
				
			||||||
 | 
					        return Math.round(dp * density);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onResume() {
 | 
				
			||||||
 | 
					        super.onResume();
 | 
				
			||||||
 | 
					        if (!LoginActivity.isLoggedIn(this)) {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // ✅ Refresh data when returning from form
 | 
				
			||||||
 | 
					        loadTicketData();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onDestroy() {
 | 
				
			||||||
 | 
					        super.onDestroy();
 | 
				
			||||||
 | 
					        if (executor != null && !executor.isShutdown()) {
 | 
				
			||||||
 | 
					            executor.shutdown();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,950 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.bantuan;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.app.DatePickerDialog;
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import android.net.Uri;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.os.Handler;
 | 
				
			||||||
 | 
					import android.os.Looper;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.widget.ArrayAdapter;
 | 
				
			||||||
 | 
					import android.widget.Button;
 | 
				
			||||||
 | 
					import android.widget.EditText;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					import android.widget.Spinner;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					import androidx.activity.result.ActivityResultLauncher;
 | 
				
			||||||
 | 
					import androidx.activity.result.contract.ActivityResultContracts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.LoginActivity;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.json.JSONArray;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.io.OutputStream;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Calendar;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					import java.util.concurrent.ExecutorService;
 | 
				
			||||||
 | 
					import java.util.concurrent.Executors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class BantuanFormActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private static final String TAG = "BantuanFormActivity";
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // UI Components
 | 
				
			||||||
 | 
					    private EditText etTicketCode;
 | 
				
			||||||
 | 
					    private Spinner spinnerSource, spinnerIssue, spinnerMerchant, spinnerAssign;
 | 
				
			||||||
 | 
					    private TextView tvStatus, tvResolvedDate;
 | 
				
			||||||
 | 
					    private LinearLayout llResolvedDate;
 | 
				
			||||||
 | 
					    private Button btnKirim;
 | 
				
			||||||
 | 
					    private LinearLayout backNavigation;
 | 
				
			||||||
 | 
					    private LinearLayout successScreen;
 | 
				
			||||||
 | 
					    private LinearLayout mainContent;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Data
 | 
				
			||||||
 | 
					    private String selectedResolvedDate = "";
 | 
				
			||||||
 | 
					    private ExecutorService executor = Executors.newSingleThreadExecutor();
 | 
				
			||||||
 | 
					    private Handler mainHandler = new Handler(Looper.getMainLooper());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Dynamic data lists
 | 
				
			||||||
 | 
					    private List<ParameterDetail> sourceList = new ArrayList<>();
 | 
				
			||||||
 | 
					    private List<ParameterDetail> issueList = new ArrayList<>();
 | 
				
			||||||
 | 
					    private List<ParameterDetail> assignList = new ArrayList<>();
 | 
				
			||||||
 | 
					    private List<ParameterDetail> merchantList = new ArrayList<>();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Inner class for parameter details
 | 
				
			||||||
 | 
					    public static class ParameterDetail {
 | 
				
			||||||
 | 
					        public int id;
 | 
				
			||||||
 | 
					        public String name;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public ParameterDetail(int id, String name) {
 | 
				
			||||||
 | 
					            this.id = id;
 | 
				
			||||||
 | 
					            this.name = name;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public String toString() {
 | 
				
			||||||
 | 
					            return name;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check authentication
 | 
				
			||||||
 | 
					        if (!LoginActivity.isLoggedIn(this)) {
 | 
				
			||||||
 | 
					            LoginActivity.logout(this);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_bantuan_form);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        initViews();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Load dynamic data first, then setup spinners
 | 
				
			||||||
 | 
					        loadHeaderParams();
 | 
				
			||||||
 | 
					        loadUsers();
 | 
				
			||||||
 | 
					        loadMerchants();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        setupListeners();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Initialize button state
 | 
				
			||||||
 | 
					        updateButtonState();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void initViews() {
 | 
				
			||||||
 | 
					        // Main content containers
 | 
				
			||||||
 | 
					        mainContent = findViewById(R.id.main_content);
 | 
				
			||||||
 | 
					        successScreen = findViewById(R.id.success_screen);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Form fields
 | 
				
			||||||
 | 
					        etTicketCode = findViewById(R.id.et_ticket_code);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Spinners
 | 
				
			||||||
 | 
					        spinnerSource = findViewById(R.id.spinner_source);
 | 
				
			||||||
 | 
					        spinnerIssue = findViewById(R.id.spinner_issue);
 | 
				
			||||||
 | 
					        spinnerMerchant = findViewById(R.id.spinner_merchant);
 | 
				
			||||||
 | 
					        spinnerAssign = findViewById(R.id.spinner_assign);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Resolved Date components
 | 
				
			||||||
 | 
					        llResolvedDate = findViewById(R.id.ll_resolved_date);
 | 
				
			||||||
 | 
					        tvResolvedDate = findViewById(R.id.tv_resolved_date);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Status (read-only)
 | 
				
			||||||
 | 
					        tvStatus = findViewById(R.id.tv_status);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Buttons
 | 
				
			||||||
 | 
					        btnKirim = findViewById(R.id.btn_kirim);
 | 
				
			||||||
 | 
					        backNavigation = findViewById(R.id.back_navigation);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void loadHeaderParams() {
 | 
				
			||||||
 | 
					        String authToken = LoginActivity.getToken(this);
 | 
				
			||||||
 | 
					        if (authToken == null || authToken.isEmpty()) {
 | 
				
			||||||
 | 
					            LoginActivity.logout(this);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        executor.execute(() -> {
 | 
				
			||||||
 | 
					            HttpURLConnection connection = null;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                URL url = new URL("https://be-edc.msvc.app/header-params/list");
 | 
				
			||||||
 | 
					                connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                connection.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("Authorization", "Bearer " + authToken);
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(15000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(15000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (responseCode == 200) {
 | 
				
			||||||
 | 
					                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(line);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    reader.close();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Parse response
 | 
				
			||||||
 | 
					                    JSONObject jsonResponse = new JSONObject(response.toString());
 | 
				
			||||||
 | 
					                    JSONArray dataArray = jsonResponse.getJSONArray("data");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Clear existing lists
 | 
				
			||||||
 | 
					                    sourceList.clear();
 | 
				
			||||||
 | 
					                    issueList.clear();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Process each header parameter
 | 
				
			||||||
 | 
					                    for (int i = 0; i < dataArray.length(); i++) {
 | 
				
			||||||
 | 
					                        JSONObject headerParam = dataArray.getJSONObject(i);
 | 
				
			||||||
 | 
					                        String code = headerParam.getString("code");
 | 
				
			||||||
 | 
					                        String name = headerParam.getString("name");
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Check if this is ticket_sources (code "01")
 | 
				
			||||||
 | 
					                        if ("01".equals(code) && "ticket_sources".equals(name)) {
 | 
				
			||||||
 | 
					                            JSONArray details = headerParam.getJSONArray("details");
 | 
				
			||||||
 | 
					                            for (int j = 0; j < details.length(); j++) {
 | 
				
			||||||
 | 
					                                JSONObject detail = details.getJSONObject(j);
 | 
				
			||||||
 | 
					                                int id = detail.getInt("id");
 | 
				
			||||||
 | 
					                                String detailName = detail.getString("name");
 | 
				
			||||||
 | 
					                                String status = detail.getString("status");
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                // Only add active items
 | 
				
			||||||
 | 
					                                if ("1".equals(status)) {
 | 
				
			||||||
 | 
					                                    sourceList.add(new ParameterDetail(id, detailName));
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        // Check if this is issue_categories (code "02")
 | 
				
			||||||
 | 
					                        else if ("02".equals(code) && "issue_categories".equals(name)) {
 | 
				
			||||||
 | 
					                            JSONArray details = headerParam.getJSONArray("details");
 | 
				
			||||||
 | 
					                            for (int j = 0; j < details.length(); j++) {
 | 
				
			||||||
 | 
					                                JSONObject detail = details.getJSONObject(j);
 | 
				
			||||||
 | 
					                                int id = detail.getInt("id");
 | 
				
			||||||
 | 
					                                String detailName = detail.getString("name");
 | 
				
			||||||
 | 
					                                String status = detail.getString("status");
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                // Only add active items
 | 
				
			||||||
 | 
					                                if ("1".equals(status)) {
 | 
				
			||||||
 | 
					                                    issueList.add(new ParameterDetail(id, detailName));
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Update UI on main thread
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        setupSpinners();
 | 
				
			||||||
 | 
					                        updateButtonState();
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } else if (responseCode == 401) {
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Session expired. Please login again.", Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        LoginActivity.logout(this);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Failed to load data. Error: " + responseCode, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        // Setup with empty data
 | 
				
			||||||
 | 
					                        setupSpinners();
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                    Toast.makeText(this, "Network error: " + e.getMessage(), Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                    // Setup with empty data
 | 
				
			||||||
 | 
					                    setupSpinners();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					                if (connection != null) {
 | 
				
			||||||
 | 
					                    connection.disconnect();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void loadMerchants() {
 | 
				
			||||||
 | 
					        String authToken = LoginActivity.getToken(this);
 | 
				
			||||||
 | 
					        if (authToken == null || authToken.isEmpty()) {
 | 
				
			||||||
 | 
					            LoginActivity.logout(this);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        executor.execute(() -> {
 | 
				
			||||||
 | 
					            HttpURLConnection connection = null;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                URL url = new URL("https://be-edc.msvc.app/merchants/list?location_id=0");
 | 
				
			||||||
 | 
					                connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                connection.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("Authorization", "Bearer " + authToken);
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(15000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(15000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (responseCode == 200) {
 | 
				
			||||||
 | 
					                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(line);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    reader.close();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Parse response
 | 
				
			||||||
 | 
					                    JSONObject jsonResponse = new JSONObject(response.toString());
 | 
				
			||||||
 | 
					                    JSONArray dataArray = jsonResponse.getJSONArray("data");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Clear existing list
 | 
				
			||||||
 | 
					                    merchantList.clear();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Process each merchant
 | 
				
			||||||
 | 
					                    for (int i = 0; i < dataArray.length(); i++) {
 | 
				
			||||||
 | 
					                        JSONObject merchant = dataArray.getJSONObject(i);
 | 
				
			||||||
 | 
					                        String status = merchant.getString("status");
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Only add active merchants
 | 
				
			||||||
 | 
					                        if ("1".equals(status)) {
 | 
				
			||||||
 | 
					                            int id = merchant.getInt("id");
 | 
				
			||||||
 | 
					                            String name = merchant.getString("name");
 | 
				
			||||||
 | 
					                            String merchantCode = merchant.optString("merchant_code", "");
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // Only add merchants that have merchant_code
 | 
				
			||||||
 | 
					                            if (!merchantCode.isEmpty()) {
 | 
				
			||||||
 | 
					                                merchantList.add(new ParameterDetail(id, name));
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Update UI on main thread
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        setupSpinners();
 | 
				
			||||||
 | 
					                        updateButtonState();
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } else if (responseCode == 401) {
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Session expired. Please login again.", Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        LoginActivity.logout(this);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Failed to load merchants. Error: " + responseCode, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        // Setup with empty data
 | 
				
			||||||
 | 
					                        setupSpinners();
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                    Toast.makeText(this, "Network error loading merchants: " + e.getMessage(), Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                    // Setup with empty data
 | 
				
			||||||
 | 
					                    setupSpinners();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					                if (connection != null) {
 | 
				
			||||||
 | 
					                    connection.disconnect();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void showSuccessScreen() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "Showing success screen");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Hide main content and show success screen
 | 
				
			||||||
 | 
					        if (mainContent != null) {
 | 
				
			||||||
 | 
					            mainContent.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (successScreen != null) {
 | 
				
			||||||
 | 
					            successScreen.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Navigate back to BantuanActivity after 2 seconds
 | 
				
			||||||
 | 
					        new Handler(Looper.getMainLooper()).postDelayed(() -> {
 | 
				
			||||||
 | 
					            navigateToBantuanActivity();
 | 
				
			||||||
 | 
					        }, 2000);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void navigateToBantuanActivity() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "Navigating back to BantuanActivity");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Create intent to BantuanActivity
 | 
				
			||||||
 | 
					            Intent intent = new Intent(this, Class.forName("com.example.bdkipoc.bantuan.BantuanActivity"));
 | 
				
			||||||
 | 
					            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
 | 
				
			||||||
 | 
					            startActivity(intent);
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					        } catch (ClassNotFoundException e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "BantuanActivity class not found", e);
 | 
				
			||||||
 | 
					            // Fallback: just finish current activity
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void loadUsers() {
 | 
				
			||||||
 | 
					        String authToken = LoginActivity.getToken(this);
 | 
				
			||||||
 | 
					        if (authToken == null || authToken.isEmpty()) {
 | 
				
			||||||
 | 
					            LoginActivity.logout(this);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        executor.execute(() -> {
 | 
				
			||||||
 | 
					            HttpURLConnection connection = null;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                URL url = new URL("https://be-edc.msvc.app/users");
 | 
				
			||||||
 | 
					                connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                connection.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("Authorization", "Bearer " + authToken);
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(15000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(15000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (responseCode == 200) {
 | 
				
			||||||
 | 
					                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(line);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    reader.close();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Parse response
 | 
				
			||||||
 | 
					                    JSONArray usersArray = new JSONArray(response.toString());
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Clear existing list
 | 
				
			||||||
 | 
					                    assignList.clear();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Process each user
 | 
				
			||||||
 | 
					                    for (int i = 0; i < usersArray.length(); i++) {
 | 
				
			||||||
 | 
					                        JSONObject user = usersArray.getJSONObject(i);
 | 
				
			||||||
 | 
					                        String role = user.getString("role");
 | 
				
			||||||
 | 
					                        boolean isActive = user.getBoolean("is_active");
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Only add superadmin users who are active
 | 
				
			||||||
 | 
					                        if ("superadmin".equals(role) && isActive) {
 | 
				
			||||||
 | 
					                            int id = user.getInt("id");
 | 
				
			||||||
 | 
					                            String name = user.getString("name");
 | 
				
			||||||
 | 
					                            assignList.add(new ParameterDetail(id, name));
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Update UI on main thread
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        setupSpinners();
 | 
				
			||||||
 | 
					                        updateButtonState();
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } else if (responseCode == 401) {
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Session expired. Please login again.", Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        LoginActivity.logout(this);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Failed to load users. Error: " + responseCode, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        // Setup with empty data
 | 
				
			||||||
 | 
					                        setupSpinners();
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                    Toast.makeText(this, "Network error loading users: " + e.getMessage(), Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                    // Setup with empty data
 | 
				
			||||||
 | 
					                    setupSpinners();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					                if (connection != null) {
 | 
				
			||||||
 | 
					                    connection.disconnect();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupSpinners() {
 | 
				
			||||||
 | 
					        // Setup Source Spinner (Dynamic)
 | 
				
			||||||
 | 
					        List<String> sourceOptions = new ArrayList<>();
 | 
				
			||||||
 | 
					        sourceOptions.add("Pilih Source");
 | 
				
			||||||
 | 
					        for (ParameterDetail source : sourceList) {
 | 
				
			||||||
 | 
					            sourceOptions.add(source.name);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ArrayAdapter<String> sourceAdapter = new ArrayAdapter<>(this, 
 | 
				
			||||||
 | 
					            android.R.layout.simple_spinner_item, sourceOptions);
 | 
				
			||||||
 | 
					        sourceAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
 | 
				
			||||||
 | 
					        spinnerSource.setAdapter(sourceAdapter);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Setup Issue Spinner (Dynamic)
 | 
				
			||||||
 | 
					        List<String> issueOptions = new ArrayList<>();
 | 
				
			||||||
 | 
					        issueOptions.add("Pilih Issue");
 | 
				
			||||||
 | 
					        for (ParameterDetail issue : issueList) {
 | 
				
			||||||
 | 
					            issueOptions.add(issue.name);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ArrayAdapter<String> issueAdapter = new ArrayAdapter<>(this,
 | 
				
			||||||
 | 
					            android.R.layout.simple_spinner_item, issueOptions);
 | 
				
			||||||
 | 
					        issueAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
 | 
				
			||||||
 | 
					        spinnerIssue.setAdapter(issueAdapter);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Setup Merchant Spinner (Dynamic)
 | 
				
			||||||
 | 
					        List<String> merchantOptions = new ArrayList<>();
 | 
				
			||||||
 | 
					        merchantOptions.add("Pilih Merchant");
 | 
				
			||||||
 | 
					        for (ParameterDetail merchant : merchantList) {
 | 
				
			||||||
 | 
					            merchantOptions.add(merchant.name);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ArrayAdapter<String> merchantAdapter = new ArrayAdapter<>(this,
 | 
				
			||||||
 | 
					            android.R.layout.simple_spinner_item, merchantOptions);
 | 
				
			||||||
 | 
					        merchantAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
 | 
				
			||||||
 | 
					        spinnerMerchant.setAdapter(merchantAdapter);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Setup Assign Spinner (Dynamic)
 | 
				
			||||||
 | 
					        List<String> assignOptions = new ArrayList<>();
 | 
				
			||||||
 | 
					        assignOptions.add("Pilih Assign");
 | 
				
			||||||
 | 
					        for (ParameterDetail assign : assignList) {
 | 
				
			||||||
 | 
					            assignOptions.add(assign.name);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ArrayAdapter<String> assignAdapter = new ArrayAdapter<>(this,
 | 
				
			||||||
 | 
					            android.R.layout.simple_spinner_item, assignOptions);
 | 
				
			||||||
 | 
					        assignAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
 | 
				
			||||||
 | 
					        spinnerAssign.setAdapter(assignAdapter);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Setup Resolved Date components (no spinner setup needed)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set status to fixed value
 | 
				
			||||||
 | 
					        if (tvStatus != null) {
 | 
				
			||||||
 | 
					            tvStatus.setText("Pengajuan");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupListeners() {
 | 
				
			||||||
 | 
					        // Back navigation
 | 
				
			||||||
 | 
					        if (backNavigation != null) {
 | 
				
			||||||
 | 
					            backNavigation.setOnClickListener(v -> onBackPressed());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Source spinner listener
 | 
				
			||||||
 | 
					        spinnerSource.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
 | 
				
			||||||
 | 
					                updateButtonState();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onNothingSelected(android.widget.AdapterView<?> parent) {}
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Issue spinner listener
 | 
				
			||||||
 | 
					        spinnerIssue.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
 | 
				
			||||||
 | 
					                updateButtonState();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onNothingSelected(android.widget.AdapterView<?> parent) {}
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Merchant spinner listener
 | 
				
			||||||
 | 
					        spinnerMerchant.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
 | 
				
			||||||
 | 
					                updateButtonState();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onNothingSelected(android.widget.AdapterView<?> parent) {}
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Assign spinner listener
 | 
				
			||||||
 | 
					        spinnerAssign.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
 | 
				
			||||||
 | 
					                updateButtonState();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onNothingSelected(android.widget.AdapterView<?> parent) {}
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Resolved Date click listener
 | 
				
			||||||
 | 
					        if (llResolvedDate != null) {
 | 
				
			||||||
 | 
					            llResolvedDate.setOnClickListener(v -> showDatePicker());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Text watcher for EditText field
 | 
				
			||||||
 | 
					        setupTextWatcher();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Submit button listener
 | 
				
			||||||
 | 
					        if (btnKirim != null) {
 | 
				
			||||||
 | 
					            btnKirim.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                if (btnKirim.isEnabled()) {
 | 
				
			||||||
 | 
					                    submitForm();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupTextWatcher() {
 | 
				
			||||||
 | 
					        android.text.TextWatcher textWatcher = new android.text.TextWatcher() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onTextChanged(CharSequence s, int start, int before, int count) {}
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void afterTextChanged(android.text.Editable s) {
 | 
				
			||||||
 | 
					                updateButtonState();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (etTicketCode != null) {
 | 
				
			||||||
 | 
					            etTicketCode.addTextChangedListener(textWatcher);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void updateButtonState() {
 | 
				
			||||||
 | 
					        if (btnKirim == null) return;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        boolean isFormValid = checkFormValidity();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (isFormValid) {
 | 
				
			||||||
 | 
					            // Active state - Red background
 | 
				
			||||||
 | 
					            btnKirim.setBackgroundColor(0xFFDE0701); // Red color matching theme
 | 
				
			||||||
 | 
					            btnKirim.setTextColor(getResources().getColor(android.R.color.white));
 | 
				
			||||||
 | 
					            btnKirim.setEnabled(true);
 | 
				
			||||||
 | 
					            btnKirim.setAlpha(1.0f);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Inactive state - Gray background
 | 
				
			||||||
 | 
					            btnKirim.setBackgroundColor(getResources().getColor(android.R.color.darker_gray));
 | 
				
			||||||
 | 
					            btnKirim.setTextColor(getResources().getColor(android.R.color.white));
 | 
				
			||||||
 | 
					            btnKirim.setEnabled(false);
 | 
				
			||||||
 | 
					            btnKirim.setAlpha(0.6f);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private boolean checkFormValidity() {
 | 
				
			||||||
 | 
					        // Check if all required fields have values
 | 
				
			||||||
 | 
					        boolean hasTicketCode = etTicketCode != null && !etTicketCode.getText().toString().trim().isEmpty();
 | 
				
			||||||
 | 
					        boolean hasSource = spinnerSource != null && spinnerSource.getSelectedItemPosition() > 0;
 | 
				
			||||||
 | 
					        boolean hasIssue = spinnerIssue != null && spinnerIssue.getSelectedItemPosition() > 0;
 | 
				
			||||||
 | 
					        boolean hasMerchant = spinnerMerchant != null && spinnerMerchant.getSelectedItemPosition() > 0;
 | 
				
			||||||
 | 
					        boolean hasAssign = spinnerAssign != null && spinnerAssign.getSelectedItemPosition() > 0;
 | 
				
			||||||
 | 
					        boolean hasResolvedDate = !selectedResolvedDate.isEmpty();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return hasTicketCode && hasSource && hasIssue && hasMerchant && hasAssign && hasResolvedDate;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void showDatePicker() {
 | 
				
			||||||
 | 
					        Calendar calendar = Calendar.getInstance();
 | 
				
			||||||
 | 
					        DatePickerDialog datePickerDialog = new DatePickerDialog(
 | 
				
			||||||
 | 
					            this,
 | 
				
			||||||
 | 
					            (view, year, month, dayOfMonth) -> {
 | 
				
			||||||
 | 
					                Calendar selectedCalendar = Calendar.getInstance();
 | 
				
			||||||
 | 
					                selectedCalendar.set(year, month, dayOfMonth);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Format for API (ISO 8601)
 | 
				
			||||||
 | 
					                SimpleDateFormat apiFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault());
 | 
				
			||||||
 | 
					                selectedResolvedDate = apiFormat.format(selectedCalendar.getTime());
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Format for display
 | 
				
			||||||
 | 
					                SimpleDateFormat displayFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
 | 
				
			||||||
 | 
					                String displayDate = displayFormat.format(selectedCalendar.getTime());
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Update TextView to show selected date
 | 
				
			||||||
 | 
					                if (tvResolvedDate != null) {
 | 
				
			||||||
 | 
					                    tvResolvedDate.setText(displayDate);
 | 
				
			||||||
 | 
					                    tvResolvedDate.setTextColor(0xFF000000); // Black color for selected date
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Log.d(TAG, "Date selected: " + displayDate + " (API format: " + selectedResolvedDate + ")");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Update button state after date selection
 | 
				
			||||||
 | 
					                updateButtonState();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            calendar.get(Calendar.YEAR),
 | 
				
			||||||
 | 
					            calendar.get(Calendar.MONTH),
 | 
				
			||||||
 | 
					            calendar.get(Calendar.DAY_OF_MONTH)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set minimum date to today to prevent past dates
 | 
				
			||||||
 | 
					        datePickerDialog.getDatePicker().setMinDate(System.currentTimeMillis());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        datePickerDialog.show();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private int getSourceId(int position) {
 | 
				
			||||||
 | 
					        if (position > 0 && position <= sourceList.size()) {
 | 
				
			||||||
 | 
					            return sourceList.get(position - 1).id;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private int getIssueId(int position) {
 | 
				
			||||||
 | 
					        if (position > 0 && position <= issueList.size()) {
 | 
				
			||||||
 | 
					            return issueList.get(position - 1).id;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private int getMerchantId(int position) {
 | 
				
			||||||
 | 
					        if (position > 0 && position <= merchantList.size()) {
 | 
				
			||||||
 | 
					            return merchantList.get(position - 1).id;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private int getAssignId(int position) {
 | 
				
			||||||
 | 
					        if (position > 0 && position <= assignList.size()) {
 | 
				
			||||||
 | 
					            return assignList.get(position - 1).id;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void submitForm() {
 | 
				
			||||||
 | 
					        if (!validateForm()) {
 | 
				
			||||||
 | 
					            Log.w(TAG, "Form validation failed");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "Form validation passed, preparing data...");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Disable button and show loading
 | 
				
			||||||
 | 
					        btnKirim.setEnabled(false);
 | 
				
			||||||
 | 
					        btnKirim.setText("Mengirim...");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Prepare form data
 | 
				
			||||||
 | 
					        String ticketCode = etTicketCode.getText().toString().trim();
 | 
				
			||||||
 | 
					        int sourceId = getSourceId(spinnerSource.getSelectedItemPosition());
 | 
				
			||||||
 | 
					        int issueId = getIssueId(spinnerIssue.getSelectedItemPosition());
 | 
				
			||||||
 | 
					        int merchantId = getMerchantId(spinnerMerchant.getSelectedItemPosition());
 | 
				
			||||||
 | 
					        int assignId = getAssignId(spinnerAssign.getSelectedItemPosition());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Validate IDs
 | 
				
			||||||
 | 
					        if (sourceId == 0) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Invalid source ID: " + sourceId + ", position: " + spinnerSource.getSelectedItemPosition());
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Error: Source tidak valid", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            btnKirim.setEnabled(true);
 | 
				
			||||||
 | 
					            btnKirim.setText("Kirim Sekarang");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (issueId == 0) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Invalid issue ID: " + issueId + ", position: " + spinnerIssue.getSelectedItemPosition());
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Error: Issue tidak valid", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            btnKirim.setEnabled(true);
 | 
				
			||||||
 | 
					            btnKirim.setText("Kirim Sekarang");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (merchantId == 0) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Invalid merchant ID: " + merchantId + ", position: " + spinnerMerchant.getSelectedItemPosition());
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Error: Merchant tidak valid", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            btnKirim.setEnabled(true);
 | 
				
			||||||
 | 
					            btnKirim.setText("Kirim Sekarang");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (assignId == 0) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Invalid assign ID: " + assignId + ", position: " + spinnerAssign.getSelectedItemPosition());
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Error: Assign To tidak valid", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            btnKirim.setEnabled(true);
 | 
				
			||||||
 | 
					            btnKirim.setText("Kirim Sekarang");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "All IDs validated successfully");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        submitToAPI(ticketCode, sourceId, issueId, merchantId, assignId, selectedResolvedDate);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private boolean validateForm() {
 | 
				
			||||||
 | 
					        boolean isValid = true;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Validate Ticket Code
 | 
				
			||||||
 | 
					        if (etTicketCode.getText().toString().trim().isEmpty()) {
 | 
				
			||||||
 | 
					            etTicketCode.setError("Ticket Code wajib diisi");
 | 
				
			||||||
 | 
					            isValid = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Validate Source
 | 
				
			||||||
 | 
					        if (spinnerSource.getSelectedItemPosition() == 0) {
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Pilih Source", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            isValid = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Validate Issue
 | 
				
			||||||
 | 
					        if (spinnerIssue.getSelectedItemPosition() == 0) {
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Pilih Issue", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            isValid = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Validate Merchant
 | 
				
			||||||
 | 
					        if (spinnerMerchant.getSelectedItemPosition() == 0) {
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Pilih Merchant", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            isValid = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Validate Assign
 | 
				
			||||||
 | 
					        if (spinnerAssign.getSelectedItemPosition() == 0) {
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Pilih Assign", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            isValid = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Validate Resolved Date
 | 
				
			||||||
 | 
					        if (selectedResolvedDate.isEmpty()) {
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Pilih tanggal resolved", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            isValid = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return isValid;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void submitToAPI(String ticketCode, int sourceId, int issueId, int merchantId, 
 | 
				
			||||||
 | 
					                           int assignId, String resolvedAt) {
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "Starting API submission...");
 | 
				
			||||||
 | 
					        Log.d(TAG, "Ticket Code: " + ticketCode);
 | 
				
			||||||
 | 
					        Log.d(TAG, "Source ID: " + sourceId);
 | 
				
			||||||
 | 
					        Log.d(TAG, "Issue ID: " + issueId);
 | 
				
			||||||
 | 
					        Log.d(TAG, "Merchant ID: " + merchantId);
 | 
				
			||||||
 | 
					        Log.d(TAG, "Assign ID: " + assignId);
 | 
				
			||||||
 | 
					        Log.d(TAG, "Resolved At: " + resolvedAt);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String authToken = LoginActivity.getToken(this);
 | 
				
			||||||
 | 
					        if (authToken == null || authToken.isEmpty()) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Auth token is null or empty");
 | 
				
			||||||
 | 
					            mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                btnKirim.setEnabled(true);
 | 
				
			||||||
 | 
					                btnKirim.setText("Kirim Sekarang");
 | 
				
			||||||
 | 
					                LoginActivity.logout(this);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "Auth token obtained: " + authToken.substring(0, Math.min(authToken.length(), 10)) + "...");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        executor.execute(() -> {
 | 
				
			||||||
 | 
					            HttpURLConnection connection = null;
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                URL url = new URL("https://be-edc.msvc.app/tickets");
 | 
				
			||||||
 | 
					                connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                connection.setRequestMethod("POST");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("Content-Type", "application/json");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("Authorization", "Bearer " + authToken);
 | 
				
			||||||
 | 
					                connection.setDoOutput(true);
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(15000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(15000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Create JSON payload
 | 
				
			||||||
 | 
					                JSONObject payload = new JSONObject();
 | 
				
			||||||
 | 
					                payload.put("ticket_code", ticketCode);
 | 
				
			||||||
 | 
					                payload.put("source_id", sourceId);
 | 
				
			||||||
 | 
					                payload.put("issue_id", issueId);
 | 
				
			||||||
 | 
					                payload.put("merchant_id", merchantId);
 | 
				
			||||||
 | 
					                payload.put("status", "new");
 | 
				
			||||||
 | 
					                payload.put("is_sla_violated", true);
 | 
				
			||||||
 | 
					                payload.put("assigned_to", assignId);
 | 
				
			||||||
 | 
					                payload.put("resolved_at", resolvedAt);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String jsonPayload = payload.toString();
 | 
				
			||||||
 | 
					                Log.d(TAG, "JSON Payload: " + jsonPayload);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Send request
 | 
				
			||||||
 | 
					                try (OutputStream os = connection.getOutputStream()) {
 | 
				
			||||||
 | 
					                    byte[] input = jsonPayload.getBytes("utf-8");
 | 
				
			||||||
 | 
					                    os.write(input, 0, input.length);
 | 
				
			||||||
 | 
					                    Log.d(TAG, "Request sent successfully");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                Log.d(TAG, "Response Code: " + responseCode);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Read response
 | 
				
			||||||
 | 
					                BufferedReader reader;
 | 
				
			||||||
 | 
					                if (responseCode >= 200 && responseCode < 300) {
 | 
				
			||||||
 | 
					                    reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    reader = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                String line;
 | 
				
			||||||
 | 
					                while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                    response.append(line);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                reader.close();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String responseBody = response.toString();
 | 
				
			||||||
 | 
					                Log.d(TAG, "Response Body: " + responseBody);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Handle response
 | 
				
			||||||
 | 
					                mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                    btnKirim.setEnabled(true);
 | 
				
			||||||
 | 
					                    btnKirim.setText("Kirim Sekarang");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if (responseCode >= 200 && responseCode < 300) {
 | 
				
			||||||
 | 
					                        // Success
 | 
				
			||||||
 | 
					                        Log.d(TAG, "Request successful");
 | 
				
			||||||
 | 
					                        try {
 | 
				
			||||||
 | 
					                            JSONObject responseJson = new JSONObject(responseBody);
 | 
				
			||||||
 | 
					                            String message = responseJson.optString("message", "Tiket berhasil dibuat");
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // Show success screen instead of toast
 | 
				
			||||||
 | 
					                            showSuccessScreen();
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                        } catch (Exception e) {
 | 
				
			||||||
 | 
					                            Log.e(TAG, "Error parsing success response", e);
 | 
				
			||||||
 | 
					                            // Show success screen even if parsing fails
 | 
				
			||||||
 | 
					                            showSuccessScreen();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else if (responseCode == 401) {
 | 
				
			||||||
 | 
					                        Log.e(TAG, "Unauthorized - token expired");
 | 
				
			||||||
 | 
					                        Toast.makeText(this, "Session expired. Please login again.", 
 | 
				
			||||||
 | 
					                            Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        LoginActivity.logout(this);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        // Error
 | 
				
			||||||
 | 
					                        Log.e(TAG, "Request failed with code: " + responseCode);
 | 
				
			||||||
 | 
					                        Log.e(TAG, "Error response: " + responseBody);
 | 
				
			||||||
 | 
					                        try {
 | 
				
			||||||
 | 
					                            JSONObject errorJson = new JSONObject(responseBody);
 | 
				
			||||||
 | 
					                            String errorMessage = errorJson.optString("message", "Gagal mengirim tiket");
 | 
				
			||||||
 | 
					                            Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        } catch (Exception e) {
 | 
				
			||||||
 | 
					                            Log.e(TAG, "Error parsing error response", e);
 | 
				
			||||||
 | 
					                            Toast.makeText(this, "Gagal mengirim tiket. Error: " + responseCode, 
 | 
				
			||||||
 | 
					                                Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "Network error during submission", e);
 | 
				
			||||||
 | 
					                mainHandler.post(() -> {
 | 
				
			||||||
 | 
					                    btnKirim.setEnabled(true);
 | 
				
			||||||
 | 
					                    btnKirim.setText("Kirim Sekarang");
 | 
				
			||||||
 | 
					                    Toast.makeText(this, "Network error: " + e.getMessage(), 
 | 
				
			||||||
 | 
					                        Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } finally {
 | 
				
			||||||
 | 
					                if (connection != null) {
 | 
				
			||||||
 | 
					                    connection.disconnect();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void clearForm() {
 | 
				
			||||||
 | 
					        if (etTicketCode != null) etTicketCode.setText("");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (spinnerSource != null) spinnerSource.setSelection(0);
 | 
				
			||||||
 | 
					        if (spinnerIssue != null) spinnerIssue.setSelection(0);
 | 
				
			||||||
 | 
					        if (spinnerMerchant != null) spinnerMerchant.setSelection(0);
 | 
				
			||||||
 | 
					        if (spinnerAssign != null) spinnerAssign.setSelection(0);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Reset resolved date
 | 
				
			||||||
 | 
					        if (tvResolvedDate != null) {
 | 
				
			||||||
 | 
					            tvResolvedDate.setText("Pilih Tanggal Resolved");
 | 
				
			||||||
 | 
					            tvResolvedDate.setTextColor(0xFFAAAAAA); // Light gray color
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        selectedResolvedDate = "";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Update button state after clearing form
 | 
				
			||||||
 | 
					        updateButtonState();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onResume() {
 | 
				
			||||||
 | 
					        super.onResume();
 | 
				
			||||||
 | 
					        if (!LoginActivity.isLoggedIn(this)) {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onDestroy() {
 | 
				
			||||||
 | 
					        super.onDestroy();
 | 
				
			||||||
 | 
					        if (executor != null && !executor.isShutdown()) {
 | 
				
			||||||
 | 
					            executor.shutdown();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,672 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.cetakulang;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import android.view.LayoutInflater;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.view.ViewGroup;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.annotation.NonNull;
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||||
 | 
					import androidx.core.content.ContextCompat;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.io.OutputStream; // ✅ ADDED: Missing import
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat;
 | 
				
			||||||
 | 
					import java.util.Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.json.JSONArray;
 | 
				
			||||||
 | 
					import org.json.JSONException;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.StyleHelper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ReprintAdapterActivity extends RecyclerView.Adapter<ReprintAdapterActivity.TransactionViewHolder> {
 | 
				
			||||||
 | 
					    private List<ReprintActivity.Transaction> transactionList;
 | 
				
			||||||
 | 
					    private OnPrintClickListener printClickListener;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public interface OnPrintClickListener {
 | 
				
			||||||
 | 
					        void onPrintClick(ReprintActivity.Transaction transaction);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ReprintAdapterActivity(List<ReprintActivity.Transaction> transactionList) {
 | 
				
			||||||
 | 
					        this.transactionList = transactionList;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void setPrintClickListener(OnPrintClickListener listener) {
 | 
				
			||||||
 | 
					        this.printClickListener = listener;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update data without numbering (removed as per request)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void updateData(List<ReprintActivity.Transaction> newData, int startIndex) {
 | 
				
			||||||
 | 
					        this.transactionList = newData;
 | 
				
			||||||
 | 
					        notifyDataSetChanged();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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_reprint, parent, false);
 | 
				
			||||||
 | 
					        return new TransactionViewHolder(view);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
 | 
				
			||||||
 | 
					        ReprintActivity.Transaction t = transactionList.get(position);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // ✅ STRIPE TABLE: Set alternating row colors
 | 
				
			||||||
 | 
					        LinearLayout itemContainer = holder.itemView.findViewById(R.id.itemContainer);
 | 
				
			||||||
 | 
					        if (position % 2 == 0) {
 | 
				
			||||||
 | 
					            // Even rows - white background
 | 
				
			||||||
 | 
					            itemContainer.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.white));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Odd rows - light gray background  
 | 
				
			||||||
 | 
					            itemContainer.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.background_light));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "📋 Binding transaction " + position + ":");
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "   Reference: " + t.referenceId);
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "   Status: " + t.status);
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "   Amount: " + t.amount);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set reference ID
 | 
				
			||||||
 | 
					        holder.referenceId.setText(t.referenceId);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Format the amount as Indonesian Rupiah
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            String cleanAmount = cleanAmountString(t.amount);
 | 
				
			||||||
 | 
					            long amountValue = Long.parseLong(cleanAmount);
 | 
				
			||||||
 | 
					            String formattedAmount = formatRupiah(amountValue);
 | 
				
			||||||
 | 
					            holder.amount.setText(formattedAmount);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Log.d("ReprintAdapterActivity", "💰 Amount processed: '" + t.amount + "' -> '" + formattedAmount + "'");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (NumberFormatException 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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ✅ ENHANCED STATUS HANDLING dengan comprehensive checking
 | 
				
			||||||
 | 
					        String displayStatus = t.status;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "🔍 Checking status for: " + t.referenceId + " (current: " + displayStatus + ")");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Jika status adalah INIT atau PENDING, lakukan comprehensive check
 | 
				
			||||||
 | 
					        if ("INIT".equalsIgnoreCase(t.status) || "PENDING".equalsIgnoreCase(t.status)) {
 | 
				
			||||||
 | 
					            if (t.referenceId != null && !t.referenceId.isEmpty()) {
 | 
				
			||||||
 | 
					                // Show checking state
 | 
				
			||||||
 | 
					                holder.status.setText("CHECKING...");
 | 
				
			||||||
 | 
					                StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), "CHECKING");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Log.d("ReprintAdapterActivity", "🔄 Starting comprehensive check for: " + t.referenceId);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Check real status dari semua kemungkinan sources
 | 
				
			||||||
 | 
					                checkMidtransStatus(t.referenceId, holder.status);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // No reference ID to check
 | 
				
			||||||
 | 
					                holder.status.setText(displayStatus.toUpperCase());
 | 
				
			||||||
 | 
					                StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), displayStatus);
 | 
				
			||||||
 | 
					                Log.w("ReprintAdapterActivity", "⚠️ No reference ID for status check");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Use existing status yang sudah confirmed
 | 
				
			||||||
 | 
					            holder.status.setText(displayStatus.toUpperCase());
 | 
				
			||||||
 | 
					            StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), displayStatus);
 | 
				
			||||||
 | 
					            Log.d("ReprintAdapterActivity", "✅ Using confirmed status: " + displayStatus);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set payment method
 | 
				
			||||||
 | 
					        String paymentMethod = getPaymentMethodName(t.channelCode, t.channelCategory);
 | 
				
			||||||
 | 
					        holder.paymentMethod.setText(paymentMethod);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ✅ FORMAT AND DISPLAY CREATED AT
 | 
				
			||||||
 | 
					        String formattedDate = formatCreatedAtDate(t.createdAt);
 | 
				
			||||||
 | 
					        holder.createdAt.setText(formattedDate);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "📅 Created at: " + t.createdAt + " -> " + formattedDate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set click listeners
 | 
				
			||||||
 | 
					        holder.itemView.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					            if (printClickListener != null) {
 | 
				
			||||||
 | 
					                printClickListener.onPrintClick(t);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        holder.printSection.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					            if (printClickListener != null) {
 | 
				
			||||||
 | 
					                printClickListener.onPrintClick(t);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "✅ Transaction binding complete for: " + t.referenceId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String cleanAmountString(String amount) {
 | 
				
			||||||
 | 
					        if (amount == null || amount.isEmpty()) {
 | 
				
			||||||
 | 
					            return "0";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "Cleaning amount: '" + amount + "'");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Remove currency symbols and spaces
 | 
				
			||||||
 | 
					        String cleaned = amount
 | 
				
			||||||
 | 
					                .replace("Rp. ", "")
 | 
				
			||||||
 | 
					                .replace("Rp ", "")
 | 
				
			||||||
 | 
					                .replace("IDR ", "")
 | 
				
			||||||
 | 
					                .replace(" ", "")
 | 
				
			||||||
 | 
					                .trim();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Handle dots properly
 | 
				
			||||||
 | 
					        if (cleaned.contains(".")) {
 | 
				
			||||||
 | 
					            // Split by dots
 | 
				
			||||||
 | 
					            String[] parts = cleaned.split("\\.");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (parts.length == 2) {
 | 
				
			||||||
 | 
					                String beforeDot = parts[0];
 | 
				
			||||||
 | 
					                String afterDot = parts[1];
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Check if it's decimal format (like "1000.00") or thousand separator (like "1.000")
 | 
				
			||||||
 | 
					                if (afterDot.length() <= 2 && (afterDot.equals("00") || afterDot.equals("0"))) {
 | 
				
			||||||
 | 
					                    // It's decimal format - keep only the integer part
 | 
				
			||||||
 | 
					                    cleaned = beforeDot;
 | 
				
			||||||
 | 
					                } else if (afterDot.length() == 3) {
 | 
				
			||||||
 | 
					                    // It's thousand separator format - combine parts
 | 
				
			||||||
 | 
					                    cleaned = beforeDot + afterDot;
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // Ambiguous case - assume thousand separator if beforeDot is short
 | 
				
			||||||
 | 
					                    if (beforeDot.length() <= 3) {
 | 
				
			||||||
 | 
					                        cleaned = beforeDot + afterDot;
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        cleaned = beforeDot; // Assume decimal
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else if (parts.length > 2) {
 | 
				
			||||||
 | 
					                // Multiple dots - assume all are thousand separators
 | 
				
			||||||
 | 
					                cleaned = String.join("", parts);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Remove any commas
 | 
				
			||||||
 | 
					        cleaned = cleaned.replace(",", "");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "Cleaned result: '" + cleaned + "'");
 | 
				
			||||||
 | 
					        return cleaned;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Format long amount to Indonesian Rupiah format
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private String formatRupiah(long amount) {
 | 
				
			||||||
 | 
					        // Use dots as thousand separators (Indonesian format)
 | 
				
			||||||
 | 
					        String formatted = String.format("%,d", amount).replace(',', '.');
 | 
				
			||||||
 | 
					        return "Rp. " + formatted;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ✅ FIXED: Enhanced status checking with comprehensive search
 | 
				
			||||||
 | 
					    private void checkMidtransStatus(String referenceId, TextView statusTextView) {
 | 
				
			||||||
 | 
					        new Thread(() -> {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                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";
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                URL url = new URL(queryUrl);
 | 
				
			||||||
 | 
					                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                conn.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                conn.setRequestProperty("Accept", "application/json");
 | 
				
			||||||
 | 
					                conn.setConnectTimeout(10000);
 | 
				
			||||||
 | 
					                conn.setReadTimeout(10000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                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");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    String finalStatus = "INIT"; // Default
 | 
				
			||||||
 | 
					                    String foundOrderId = null;
 | 
				
			||||||
 | 
					                    String foundAcquirer = null;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if (results != null && results.length() > 0) {
 | 
				
			||||||
 | 
					                        Log.d("ReprintAdapterActivity", "📊 Processing " + results.length() + " log entries");
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // STEP 2: Comprehensive search dengan multiple matching strategies
 | 
				
			||||||
 | 
					                        for (int i = 0; i < results.length(); i++) {
 | 
				
			||||||
 | 
					                            JSONObject log = results.getJSONObject(i);
 | 
				
			||||||
 | 
					                            JSONObject reqBody = log.optJSONObject("request_body");
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            if (reqBody != null) {
 | 
				
			||||||
 | 
					                                String logOrderId = reqBody.optString("order_id", "");
 | 
				
			||||||
 | 
					                                String logTransactionStatus = reqBody.optString("transaction_status", "");
 | 
				
			||||||
 | 
					                                String logReferenceId = reqBody.optString("reference_id", "");
 | 
				
			||||||
 | 
					                                String logAcquirer = reqBody.optString("acquirer", "");
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                // ✅ METHOD 1: Direct reference_id match
 | 
				
			||||||
 | 
					                                boolean isDirectMatch = referenceId.equals(logReferenceId);
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                // ✅ METHOD 2: Check custom_field1 untuk QR refresh tracking
 | 
				
			||||||
 | 
					                                boolean isRefreshMatch = false;
 | 
				
			||||||
 | 
					                                String customField1 = reqBody.optString("custom_field1", "");
 | 
				
			||||||
 | 
					                                if (!customField1.isEmpty()) {
 | 
				
			||||||
 | 
					                                    try {
 | 
				
			||||||
 | 
					                                        JSONObject customData = new JSONObject(customField1);
 | 
				
			||||||
 | 
					                                        String originalReference = customData.optString("original_reference", "");
 | 
				
			||||||
 | 
					                                        String appReferenceId = customData.optString("app_reference_id", "");
 | 
				
			||||||
 | 
					                                        if (referenceId.equals(originalReference) || referenceId.equals(appReferenceId)) {
 | 
				
			||||||
 | 
					                                            isRefreshMatch = true;
 | 
				
			||||||
 | 
					                                            Log.d("ReprintAdapterActivity", "🔄 Found refresh match: " + logOrderId);
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    } catch (JSONException e) {
 | 
				
			||||||
 | 
					                                        // Ignore custom field parsing errors
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                // ✅ METHOD 3: Check item details untuk backup tracking
 | 
				
			||||||
 | 
					                                boolean isItemMatch = false;
 | 
				
			||||||
 | 
					                                JSONArray itemDetails = reqBody.optJSONArray("item_details");
 | 
				
			||||||
 | 
					                                if (itemDetails != null && itemDetails.length() > 0) {
 | 
				
			||||||
 | 
					                                    for (int j = 0; j < itemDetails.length(); j++) {
 | 
				
			||||||
 | 
					                                        JSONObject item = itemDetails.optJSONObject(j);
 | 
				
			||||||
 | 
					                                        if (item != null) {
 | 
				
			||||||
 | 
					                                            String itemName = item.optString("name", "");
 | 
				
			||||||
 | 
					                                            if (itemName.contains("(Ref: " + referenceId + ")") || 
 | 
				
			||||||
 | 
					                                                itemName.contains("- " + referenceId)) {
 | 
				
			||||||
 | 
					                                                isItemMatch = true;
 | 
				
			||||||
 | 
					                                                Log.d("ReprintAdapterActivity", "📦 Found item match: " + logOrderId);
 | 
				
			||||||
 | 
					                                                break;
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                // ✅ COMPREHENSIVE MATCH: Any of the three methods
 | 
				
			||||||
 | 
					                                boolean isRelatedTransaction = isDirectMatch || isRefreshMatch || isItemMatch;
 | 
				
			||||||
 | 
					                                
 | 
				
			||||||
 | 
					                                if (isRelatedTransaction) {
 | 
				
			||||||
 | 
					                                    Log.d("ReprintAdapterActivity", "🎯 MATCH FOUND!");
 | 
				
			||||||
 | 
					                                    Log.d("ReprintAdapterActivity", "   Order ID: " + logOrderId);
 | 
				
			||||||
 | 
					                                    Log.d("ReprintAdapterActivity", "   Status: " + logTransactionStatus);
 | 
				
			||||||
 | 
					                                    Log.d("ReprintAdapterActivity", "   Acquirer: " + logAcquirer);
 | 
				
			||||||
 | 
					                                    Log.d("ReprintAdapterActivity", "   Match Type: " + 
 | 
				
			||||||
 | 
					                                        (isDirectMatch ? "DIRECT " : "") + 
 | 
				
			||||||
 | 
					                                        (isRefreshMatch ? "REFRESH " : "") + 
 | 
				
			||||||
 | 
					                                        (isItemMatch ? "ITEM" : ""));
 | 
				
			||||||
 | 
					                                    
 | 
				
			||||||
 | 
					                                    // ✅ PRIORITY SYSTEM: settlement > capture > success > pending > init
 | 
				
			||||||
 | 
					                                    if (logTransactionStatus.equals("settlement") || 
 | 
				
			||||||
 | 
					                                        logTransactionStatus.equals("capture") ||
 | 
				
			||||||
 | 
					                                        logTransactionStatus.equals("success")) {
 | 
				
			||||||
 | 
					                                        finalStatus = "PAID";
 | 
				
			||||||
 | 
					                                        foundOrderId = logOrderId;
 | 
				
			||||||
 | 
					                                        foundAcquirer = logAcquirer;
 | 
				
			||||||
 | 
					                                        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("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("ReprintAdapterActivity", "❌ FAILED status: " + logOrderId + " -> " + logTransactionStatus);
 | 
				
			||||||
 | 
					                                        }
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        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
 | 
				
			||||||
 | 
					                    final String displayStatus = finalStatus;
 | 
				
			||||||
 | 
					                    final String detectedAcquirer = foundAcquirer;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    statusTextView.post(() -> {
 | 
				
			||||||
 | 
					                        statusTextView.setText(displayStatus);
 | 
				
			||||||
 | 
					                        StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), displayStatus);
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        Log.d("ReprintAdapterActivity", "🎨 UI UPDATED:");
 | 
				
			||||||
 | 
					                        Log.d("ReprintAdapterActivity", "   Reference: " + referenceId);
 | 
				
			||||||
 | 
					                        Log.d("ReprintAdapterActivity", "   Display Status: " + displayStatus);
 | 
				
			||||||
 | 
					                        Log.d("ReprintAdapterActivity", "   Detected Acquirer: " + (detectedAcquirer != null ? detectedAcquirer : "Unknown"));
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // ✅ BONUS: Update backend jika status berubah ke PAID
 | 
				
			||||||
 | 
					                    if (finalStatus.equals("PAID")) {
 | 
				
			||||||
 | 
					                        updateBackendTransactionStatus(referenceId, finalStatus, foundOrderId, detectedAcquirer);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Log.w("ReprintAdapterActivity", "⚠️ API call failed with code: " + conn.getResponseCode());
 | 
				
			||||||
 | 
					                    statusTextView.post(() -> {
 | 
				
			||||||
 | 
					                        statusTextView.setText("ERROR");
 | 
				
			||||||
 | 
					                        StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "ERROR");
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (IOException | JSONException e) {
 | 
				
			||||||
 | 
					                Log.e("ReprintAdapterActivity", "❌ Comprehensive status check error: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                statusTextView.post(() -> {
 | 
				
			||||||
 | 
					                    statusTextView.setText("INIT");
 | 
				
			||||||
 | 
					                    StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "INIT");
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).start();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * ✅ NEW METHOD: Update backend transaction status when payment confirmed
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void updateBackendTransactionStatus(String referenceId, String status, String orderId, String acquirer) {
 | 
				
			||||||
 | 
					        new Thread(() -> {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                Log.d("ReprintAdapterActivity", "🔄 Updating backend status for reference: " + referenceId);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                JSONObject updatePayload = new JSONObject();
 | 
				
			||||||
 | 
					                updatePayload.put("status", status);
 | 
				
			||||||
 | 
					                updatePayload.put("payment_status", status);
 | 
				
			||||||
 | 
					                updatePayload.put("paid_order_id", orderId);
 | 
				
			||||||
 | 
					                updatePayload.put("detected_acquirer", acquirer);
 | 
				
			||||||
 | 
					                updatePayload.put("updated_at", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new Date()));
 | 
				
			||||||
 | 
					                updatePayload.put("settlement_time", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new Date()));
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String updateUrl = "https://be-edc.msvc.app/transactions/update-by-reference";
 | 
				
			||||||
 | 
					                URL url = new URL(updateUrl);
 | 
				
			||||||
 | 
					                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);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                JSONObject requestBody = new JSONObject();
 | 
				
			||||||
 | 
					                requestBody.put("reference_id", referenceId);
 | 
				
			||||||
 | 
					                requestBody.put("update_data", updatePayload);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                try (OutputStream os = conn.getOutputStream()) {
 | 
				
			||||||
 | 
					                    byte[] input = requestBody.toString().getBytes("utf-8");
 | 
				
			||||||
 | 
					                    os.write(input, 0, input.length);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = conn.getResponseCode();
 | 
				
			||||||
 | 
					                Log.d("ReprintAdapterActivity", "📥 Backend update response: " + responseCode);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (responseCode == 200 || responseCode == 201) {
 | 
				
			||||||
 | 
					                    Log.d("ReprintAdapterActivity", "✅ Backend status updated successfully");
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Log.e("ReprintAdapterActivity", "❌ Backend update failed: " + responseCode);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e("ReprintAdapterActivity", "❌ Backend update error: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).start();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Format created_at date to readable format
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private String formatCreatedAtDate(String rawDate) {
 | 
				
			||||||
 | 
					        if (rawDate == null || rawDate.isEmpty()) {
 | 
				
			||||||
 | 
					            return "N/A";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "📅 Input date: '" + rawDate + "'");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Handle different possible input formats from API
 | 
				
			||||||
 | 
					            SimpleDateFormat inputFormat;
 | 
				
			||||||
 | 
					            String cleanedDate = rawDate;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (rawDate.contains("T")) {
 | 
				
			||||||
 | 
					                // ISO format: "2025-06-10T04:31:19.565Z"
 | 
				
			||||||
 | 
					                cleanedDate = rawDate.replace("T", " ").replace("Z", "");
 | 
				
			||||||
 | 
					                // Remove microseconds if present
 | 
				
			||||||
 | 
					                if (cleanedDate.contains(".")) {
 | 
				
			||||||
 | 
					                    cleanedDate = cleanedDate.substring(0, cleanedDate.indexOf("."));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
 | 
				
			||||||
 | 
					            } else if (rawDate.length() > 19 && rawDate.contains(".")) {
 | 
				
			||||||
 | 
					                // Format with microseconds: "2025-06-10 04:31:19.565"
 | 
				
			||||||
 | 
					                cleanedDate = rawDate.substring(0, 19); // Cut off microseconds
 | 
				
			||||||
 | 
					                inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Standard format: "2025-06-10 04:31:19"
 | 
				
			||||||
 | 
					                inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Log.d("ReprintAdapterActivity", "📅 Cleaned date: '" + cleanedDate + "'");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Output format: d/M/yyyy H:mm:ss
 | 
				
			||||||
 | 
					            SimpleDateFormat outputFormat = new SimpleDateFormat("d/M/yyyy H:mm:ss", Locale.getDefault());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Date date = inputFormat.parse(cleanedDate);
 | 
				
			||||||
 | 
					            if (date != null) {
 | 
				
			||||||
 | 
					                String formatted = outputFormat.format(date);
 | 
				
			||||||
 | 
					                Log.d("ReprintAdapterActivity", "📅 Date formatted: " + rawDate + " -> " + formatted);
 | 
				
			||||||
 | 
					                return formatted;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e("ReprintAdapterActivity", "❌ Date formatting error for: " + rawDate, e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Fallback: Manual parsing
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Handle format like "2025-06-10T04:31:19.565Z" manually
 | 
				
			||||||
 | 
					            String workingDate = rawDate.replace("T", " ").replace("Z", "");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Remove microseconds if present
 | 
				
			||||||
 | 
					            if (workingDate.contains(".")) {
 | 
				
			||||||
 | 
					                workingDate = workingDate.substring(0, workingDate.indexOf("."));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Log.d("ReprintAdapterActivity", "📅 Manual parsing attempt: '" + workingDate + "'");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Split into date and time parts
 | 
				
			||||||
 | 
					            String[] parts = workingDate.split(" ");
 | 
				
			||||||
 | 
					            if (parts.length >= 2) {
 | 
				
			||||||
 | 
					                String datePart = parts[0]; // "2025-06-10"
 | 
				
			||||||
 | 
					                String timePart = parts[1]; // "04:31:19"
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String[] dateComponents = datePart.split("-");
 | 
				
			||||||
 | 
					                if (dateComponents.length == 3) {
 | 
				
			||||||
 | 
					                    String year = dateComponents[0];
 | 
				
			||||||
 | 
					                    String month = dateComponents[1];
 | 
				
			||||||
 | 
					                    String day = dateComponents[2];
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Remove leading zeros and format as d/M/yyyy H:mm:ss
 | 
				
			||||||
 | 
					                    int dayInt = Integer.parseInt(day);
 | 
				
			||||||
 | 
					                    int monthInt = Integer.parseInt(month);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Parse time to remove leading zeros from hour
 | 
				
			||||||
 | 
					                    String[] timeComponents = timePart.split(":");
 | 
				
			||||||
 | 
					                    if (timeComponents.length >= 3) {
 | 
				
			||||||
 | 
					                        int hour = Integer.parseInt(timeComponents[0]);
 | 
				
			||||||
 | 
					                        String minute = timeComponents[1];
 | 
				
			||||||
 | 
					                        String second = timeComponents[2];
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        String result = dayInt + "/" + monthInt + "/" + year + " " + hour + ":" + minute + ":" + second;
 | 
				
			||||||
 | 
					                        Log.d("ReprintAdapterActivity", "📅 Manual format result: " + result);
 | 
				
			||||||
 | 
					                        return result;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.w("ReprintAdapterActivity", "❌ Manual date formatting failed: " + e.getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.w("ReprintAdapterActivity", "📅 Using fallback - returning original date: " + rawDate);
 | 
				
			||||||
 | 
					        return rawDate;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String getPaymentMethodName(String channelCode, String channelCategory) {
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "🔍 Mapping payment method - channelCode: " + channelCode + ", channelCategory: " + channelCategory);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Priority 1: Use channelCode for specific mapping
 | 
				
			||||||
 | 
					        if (channelCode != null && !channelCode.isEmpty()) {
 | 
				
			||||||
 | 
					            String code = channelCode.toUpperCase().trim();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            switch (code) {
 | 
				
			||||||
 | 
					                case "QRIS":
 | 
				
			||||||
 | 
					                    return "QRIS";
 | 
				
			||||||
 | 
					                case "DEBIT":
 | 
				
			||||||
 | 
					                case "DEBIT_CARD":
 | 
				
			||||||
 | 
					                    return "Kartu Debit";
 | 
				
			||||||
 | 
					                case "CREDIT":
 | 
				
			||||||
 | 
					                case "CREDIT_CARD":
 | 
				
			||||||
 | 
					                    return "Kartu Kredit";
 | 
				
			||||||
 | 
					                case "BCA":
 | 
				
			||||||
 | 
					                    return "BCA";
 | 
				
			||||||
 | 
					                case "MANDIRI":
 | 
				
			||||||
 | 
					                    return "Mandiri";
 | 
				
			||||||
 | 
					                case "BNI":
 | 
				
			||||||
 | 
					                    return "BNI";
 | 
				
			||||||
 | 
					                case "BRI":
 | 
				
			||||||
 | 
					                    return "BRI";
 | 
				
			||||||
 | 
					                case "PERMATA":
 | 
				
			||||||
 | 
					                    return "Permata";
 | 
				
			||||||
 | 
					                case "CIMB":
 | 
				
			||||||
 | 
					                    return "CIMB Niaga";
 | 
				
			||||||
 | 
					                case "DANAMON":
 | 
				
			||||||
 | 
					                    return "Danamon";
 | 
				
			||||||
 | 
					                case "BSI":
 | 
				
			||||||
 | 
					                    return "BSI";
 | 
				
			||||||
 | 
					                case "CASH":
 | 
				
			||||||
 | 
					                    return "Tunai";
 | 
				
			||||||
 | 
					                case "EDC":
 | 
				
			||||||
 | 
					                    return "EDC";
 | 
				
			||||||
 | 
					                case "RETAIL_OUTLET":
 | 
				
			||||||
 | 
					                    // ✅ SPECIAL HANDLING: For RETAIL_OUTLET, determine by context
 | 
				
			||||||
 | 
					                    return determinePaymentMethodFromCategory(channelCategory);
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    Log.d("ReprintAdapterActivity", "🔍 Unknown channelCode: " + code + ", trying channelCategory");
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Priority 2: Use channelCategory as fallback
 | 
				
			||||||
 | 
					        if (channelCategory != null && !channelCategory.isEmpty()) {
 | 
				
			||||||
 | 
					            return mapChannelCategoryToPaymentMethod(channelCategory);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Final fallback
 | 
				
			||||||
 | 
					        Log.w("ReprintAdapterActivity", "⚠️ No valid payment method found, using default");
 | 
				
			||||||
 | 
					        return "Unknown";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String determinePaymentMethodFromCategory(String channelCategory) {
 | 
				
			||||||
 | 
					        if (channelCategory == null || channelCategory.isEmpty()) {
 | 
				
			||||||
 | 
					            return "QRIS"; // Default assumption for RETAIL_OUTLET
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String category = channelCategory.toUpperCase().trim();
 | 
				
			||||||
 | 
					        Log.d("ReprintAdapterActivity", "🔍 Mapping channelCategory: " + category);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        switch (category) {
 | 
				
			||||||
 | 
					            case "RETAIL_OUTLET":
 | 
				
			||||||
 | 
					                return "QRIS"; // Most RETAIL_OUTLET transactions are QRIS
 | 
				
			||||||
 | 
					            case "DEBIT":
 | 
				
			||||||
 | 
					            case "DEBIT_CARD":
 | 
				
			||||||
 | 
					                return "Kartu Debit";
 | 
				
			||||||
 | 
					            case "CREDIT":
 | 
				
			||||||
 | 
					            case "CREDIT_CARD":
 | 
				
			||||||
 | 
					                return "Kartu Kredit";
 | 
				
			||||||
 | 
					            case "E_MONEY":
 | 
				
			||||||
 | 
					            case "EMONEY":
 | 
				
			||||||
 | 
					                return "E-Money";
 | 
				
			||||||
 | 
					            case "BANK_TRANSFER":
 | 
				
			||||||
 | 
					                return "Transfer Bank";
 | 
				
			||||||
 | 
					            case "VIRTUAL_ACCOUNT":
 | 
				
			||||||
 | 
					                return "Virtual Account";
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                Log.d("ReprintAdapterActivity", "🔍 Unknown channelCategory: " + category + ", defaulting to QRIS");
 | 
				
			||||||
 | 
					                return "QRIS";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String mapChannelCategoryToPaymentMethod(String channelCategory) {
 | 
				
			||||||
 | 
					        if (channelCategory == null || channelCategory.isEmpty()) {
 | 
				
			||||||
 | 
					            return "Unknown";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String category = channelCategory.toUpperCase().trim();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        switch (category) {
 | 
				
			||||||
 | 
					            case "RETAIL_OUTLET":
 | 
				
			||||||
 | 
					                return "QRIS";
 | 
				
			||||||
 | 
					            case "DEBIT":
 | 
				
			||||||
 | 
					            case "DEBIT_CARD":
 | 
				
			||||||
 | 
					                return "Kartu Debit";
 | 
				
			||||||
 | 
					            case "CREDIT":
 | 
				
			||||||
 | 
					            case "CREDIT_CARD":
 | 
				
			||||||
 | 
					                return "Kartu Kredit";
 | 
				
			||||||
 | 
					            case "E_MONEY":
 | 
				
			||||||
 | 
					            case "EMONEY":
 | 
				
			||||||
 | 
					                return "E-Money";
 | 
				
			||||||
 | 
					            case "BANK_TRANSFER":
 | 
				
			||||||
 | 
					                return "Transfer Bank";
 | 
				
			||||||
 | 
					            case "VIRTUAL_ACCOUNT":
 | 
				
			||||||
 | 
					                return "Virtual Account";
 | 
				
			||||||
 | 
					            case "QRIS":
 | 
				
			||||||
 | 
					                return "QRIS";
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                // Capitalize first letter for unknown categories
 | 
				
			||||||
 | 
					                return capitalizeFirstLetter(channelCategory);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String capitalizeFirstLetter(String text) {
 | 
				
			||||||
 | 
					        if (text == null || text.isEmpty()) {
 | 
				
			||||||
 | 
					            return text;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public int getItemCount() {
 | 
				
			||||||
 | 
					        return transactionList.size();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    static class TransactionViewHolder extends RecyclerView.ViewHolder {
 | 
				
			||||||
 | 
					        TextView amount, referenceId, status, paymentMethod, createdAt; // ✅ Added createdAt
 | 
				
			||||||
 | 
					        LinearLayout printSection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public TransactionViewHolder(@NonNull View itemView) {
 | 
				
			||||||
 | 
					            super(itemView);
 | 
				
			||||||
 | 
					            amount = itemView.findViewById(R.id.textAmount);
 | 
				
			||||||
 | 
					            referenceId = itemView.findViewById(R.id.textReferenceId);
 | 
				
			||||||
 | 
					            status = itemView.findViewById(R.id.textStatus);
 | 
				
			||||||
 | 
					            paymentMethod = itemView.findViewById(R.id.textPaymentMethod);
 | 
				
			||||||
 | 
					            createdAt = itemView.findViewById(R.id.textCreatedAt); // ✅ Added createdAt
 | 
				
			||||||
 | 
					            printSection = itemView.findViewById(R.id.printSection);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										145
									
								
								app/src/main/java/com/example/bdkipoc/emv/EmvTTS.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,145 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.emv;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.speech.tts.TextToSpeech;
 | 
				
			||||||
 | 
					import android.speech.tts.UtteranceProgressListener;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.MyApplication;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.utils.LogUtil;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public final class EmvTTS extends UtteranceProgressListener {
 | 
				
			||||||
 | 
					    private static final String TAG = "EmvTTS";
 | 
				
			||||||
 | 
					    private TextToSpeech textToSpeech;
 | 
				
			||||||
 | 
					    private boolean supportTTS;
 | 
				
			||||||
 | 
					    private ITTSProgressListener listener;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private EmvTTS() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static EmvTTS getInstance() {
 | 
				
			||||||
 | 
					        return SingletonHolder.INSTANCE;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void setTTSListener(ITTSProgressListener l) {
 | 
				
			||||||
 | 
					        listener = l;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void removeTTSListener() {
 | 
				
			||||||
 | 
					        listener = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final class SingletonHolder {
 | 
				
			||||||
 | 
					        private static final EmvTTS INSTANCE = new EmvTTS();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void init() {
 | 
				
			||||||
 | 
					        //初始化TTS对象
 | 
				
			||||||
 | 
					        destroy();
 | 
				
			||||||
 | 
					        textToSpeech = new TextToSpeech(MyApplication.app, this::onTTSInit);
 | 
				
			||||||
 | 
					        textToSpeech.setOnUtteranceProgressListener(this);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void play(String text) {
 | 
				
			||||||
 | 
					        play(text, "0");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public void play(String text, String utteranceId) {
 | 
				
			||||||
 | 
					        if (!supportTTS) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "PinPadTTS: play TTS failed, TTS not support...");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (textToSpeech == null) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "PinPadTTS: play TTS slipped, textToSpeech not init..");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Log.e(TAG, "play() text: [" + text + "]");
 | 
				
			||||||
 | 
					        textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onStart(String utteranceId) {
 | 
				
			||||||
 | 
					        Log.e(TAG, "播放开始,utteranceId:" + utteranceId);
 | 
				
			||||||
 | 
					        if (listener != null) {
 | 
				
			||||||
 | 
					            listener.onStart(utteranceId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onDone(String utteranceId) {
 | 
				
			||||||
 | 
					        Log.e(TAG, "播放结束,utteranceId:" + utteranceId);
 | 
				
			||||||
 | 
					        if (listener != null) {
 | 
				
			||||||
 | 
					            listener.onDone(utteranceId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onError(String utteranceId) {
 | 
				
			||||||
 | 
					        Log.e(TAG, "播放出错,utteranceId:" + utteranceId);
 | 
				
			||||||
 | 
					        if (listener != null) {
 | 
				
			||||||
 | 
					            listener.onError(utteranceId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onStop(String utteranceId, boolean interrupted) {
 | 
				
			||||||
 | 
					        Log.e(TAG, "播放停止,utteranceId:" + utteranceId + ",interrupted:" + interrupted);
 | 
				
			||||||
 | 
					        if (listener != null) {
 | 
				
			||||||
 | 
					            listener.onStop(utteranceId, interrupted);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void stop() {
 | 
				
			||||||
 | 
					        if (textToSpeech != null) {
 | 
				
			||||||
 | 
					            int code = textToSpeech.stop();
 | 
				
			||||||
 | 
					            Log.e(TAG, "tts stop() code:" + code);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    boolean isSpeaking() {
 | 
				
			||||||
 | 
					        if (textToSpeech != null) {
 | 
				
			||||||
 | 
					            return textToSpeech.isSpeaking();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void destroy() {
 | 
				
			||||||
 | 
					        if (textToSpeech != null) {
 | 
				
			||||||
 | 
					            textToSpeech.stop();
 | 
				
			||||||
 | 
					            textToSpeech.shutdown();
 | 
				
			||||||
 | 
					            textToSpeech = null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** TTS初始化回调 */
 | 
				
			||||||
 | 
					    private void onTTSInit(int status) {
 | 
				
			||||||
 | 
					        if (status != TextToSpeech.SUCCESS) {
 | 
				
			||||||
 | 
					            LogUtil.e(TAG, "PinPadTTS: init TTS failed, status:" + status);
 | 
				
			||||||
 | 
					            supportTTS = false;
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        updateTtsLanguage();
 | 
				
			||||||
 | 
					        if (supportTTS) {
 | 
				
			||||||
 | 
					            textToSpeech.setPitch(1.0f);
 | 
				
			||||||
 | 
					            textToSpeech.setSpeechRate(1.0f);
 | 
				
			||||||
 | 
					            LogUtil.e(TAG, "onTTSInit() success,locale:" + textToSpeech.getVoice().getLocale());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** 更新TTS语言 */
 | 
				
			||||||
 | 
					    private void updateTtsLanguage() {
 | 
				
			||||||
 | 
					        Locale locale = Locale.ENGLISH;
 | 
				
			||||||
 | 
					        int result = textToSpeech.setLanguage(locale);
 | 
				
			||||||
 | 
					        if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
 | 
				
			||||||
 | 
					            supportTTS = false; //系统不支持当前Locale对应的语音播报
 | 
				
			||||||
 | 
					            LogUtil.e(TAG, "updateTtsLanguage() failed, TTS not support in locale:" + locale);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            supportTTS = true;
 | 
				
			||||||
 | 
					            LogUtil.e(TAG, "updateTtsLanguage() success, TTS locale:" + locale);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.emv;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.speech.tts.TextToSpeech;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public interface ITTSProgressListener {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Called when an utterance "starts" as perceived by the caller. This will
 | 
				
			||||||
 | 
					     * be soon before audio is played back in the case of a {@link TextToSpeech#speak}
 | 
				
			||||||
 | 
					     * or before the first bytes of a file are written to the file system in the case
 | 
				
			||||||
 | 
					     * of {@link TextToSpeech#synthesizeToFile}.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param utteranceId The utterance ID of the utterance.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    void onStart(String utteranceId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Called when an utterance has successfully completed processing.
 | 
				
			||||||
 | 
					     * All audio will have been played back by this point for audible output, and all
 | 
				
			||||||
 | 
					     * output will have been written to disk for file synthesis requests.
 | 
				
			||||||
 | 
					     * <p>
 | 
				
			||||||
 | 
					     * 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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,564 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.histori;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.BuildConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import android.os.AsyncTask;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.view.LayoutInflater;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.view.ViewGroup;
 | 
				
			||||||
 | 
					import android.widget.Button;
 | 
				
			||||||
 | 
					import android.widget.ImageView;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					import android.graphics.Color;
 | 
				
			||||||
 | 
					import androidx.annotation.NonNull;
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.LinearLayoutManager;
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||||
 | 
					import org.json.JSONArray;
 | 
				
			||||||
 | 
					import org.json.JSONException;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.text.NumberFormat;
 | 
				
			||||||
 | 
					import java.text.ParseException;
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Date;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					import java.util.TimeZone;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class HistoryActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private TextView tvTotalAmount;
 | 
				
			||||||
 | 
					    private TextView tvTotalTransactions;
 | 
				
			||||||
 | 
					    private TextView btnLihatDetailTop;
 | 
				
			||||||
 | 
					    private Button btnLihatDetailBottom;
 | 
				
			||||||
 | 
					    private RecyclerView recyclerView;
 | 
				
			||||||
 | 
					    private HistoryAdapter adapter;
 | 
				
			||||||
 | 
					    private List<HistoryItem> historyList;
 | 
				
			||||||
 | 
					    private LinearLayout backNavigation;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Store full data for detail view
 | 
				
			||||||
 | 
					    private static List<HistoryItem> fullHistoryData = new ArrayList<>();
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String API_URL;
 | 
				
			||||||
 | 
					    private String SUMMARY_API_URL;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_history);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        initViews();
 | 
				
			||||||
 | 
					        setupRecyclerView();
 | 
				
			||||||
 | 
					        buildApiUrl();
 | 
				
			||||||
 | 
					        fetchApiData();
 | 
				
			||||||
 | 
					        fetchSummaryData(); // Add this line to fetch summary data
 | 
				
			||||||
 | 
					        setupClickListeners();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void buildApiUrl() {
 | 
				
			||||||
 | 
					        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
 | 
				
			||||||
 | 
					        String todayDate = dateFormat.format(new Date());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Gunakan BuildConfig untuk base URL
 | 
				
			||||||
 | 
					        API_URL = BuildConfig.BACKEND_BASE_URL + "/transactions?page=0&limit=10&sortOrder=DESC&from_date=" 
 | 
				
			||||||
 | 
					                + todayDate + "&to_date=" + todayDate + "&location_id=0&merchant_id=0&tid=&mid=&sortColumn=id";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        SUMMARY_API_URL = BuildConfig.BACKEND_BASE_URL + "/transactions/list?from_date=" 
 | 
				
			||||||
 | 
					                + todayDate + "&to_date=" + todayDate + "&location_id=0&merchant_id=0";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void initViews() {
 | 
				
			||||||
 | 
					        tvTotalAmount = findViewById(R.id.tv_total_amount);
 | 
				
			||||||
 | 
					        tvTotalTransactions = findViewById(R.id.tv_total_transactions);
 | 
				
			||||||
 | 
					        btnLihatDetailTop = findViewById(R.id.btn_lihat_detail);
 | 
				
			||||||
 | 
					        btnLihatDetailBottom = findViewById(R.id.btn_lihat_detail_bottom);
 | 
				
			||||||
 | 
					        recyclerView = findViewById(R.id.recycler_view);
 | 
				
			||||||
 | 
					        backNavigation = findViewById(R.id.back_navigation);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        historyList = new ArrayList<>();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupRecyclerView() {
 | 
				
			||||||
 | 
					        adapter = new HistoryAdapter(historyList);
 | 
				
			||||||
 | 
					        recyclerView.setLayoutManager(new LinearLayoutManager(this));
 | 
				
			||||||
 | 
					        recyclerView.setAdapter(adapter);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupClickListeners() {
 | 
				
			||||||
 | 
					        backNavigation.setOnClickListener(new View.OnClickListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onClick(View v) {
 | 
				
			||||||
 | 
					                finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        View.OnClickListener detailClickListener = new View.OnClickListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onClick(View v) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    Intent intent = new Intent(HistoryActivity.this, HistoryListActivity.class);
 | 
				
			||||||
 | 
					                    startActivity(intent);
 | 
				
			||||||
 | 
					                } catch (Exception e) {
 | 
				
			||||||
 | 
					                    e.printStackTrace();
 | 
				
			||||||
 | 
					                    Toast.makeText(HistoryActivity.this, "Error opening detail", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        btnLihatDetailTop.setOnClickListener(detailClickListener);
 | 
				
			||||||
 | 
					        btnLihatDetailBottom.setOnClickListener(detailClickListener);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void fetchApiData() {
 | 
				
			||||||
 | 
					        new ApiTask().execute(API_URL);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void fetchSummaryData() {
 | 
				
			||||||
 | 
					        new SummaryApiTask().execute(SUMMARY_API_URL);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void processApiData(JSONArray dataArray) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            historyList.clear();
 | 
				
			||||||
 | 
					            fullHistoryData.clear(); // Clear static data
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for (int i = 0; i < dataArray.length(); i++) {
 | 
				
			||||||
 | 
					                JSONObject item = dataArray.getJSONObject(i);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String channelCode = item.getString("channel_code");
 | 
				
			||||||
 | 
					                String amount = item.getString("amount");
 | 
				
			||||||
 | 
					                String status = item.getString("status");
 | 
				
			||||||
 | 
					                String transactionDate = item.getString("transaction_date");
 | 
				
			||||||
 | 
					                String referenceId = item.getString("reference_id");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Parse amount safely
 | 
				
			||||||
 | 
					                double amountValue = 0;
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    amountValue = Double.parseDouble(amount);
 | 
				
			||||||
 | 
					                } catch (NumberFormatException e) {
 | 
				
			||||||
 | 
					                    amountValue = 0;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Create history item
 | 
				
			||||||
 | 
					                HistoryItem historyItem = new HistoryItem();
 | 
				
			||||||
 | 
					                historyItem.setTime(formatTime(transactionDate));
 | 
				
			||||||
 | 
					                historyItem.setDate(formatDate(transactionDate));
 | 
				
			||||||
 | 
					                historyItem.setAmount((long) amountValue);
 | 
				
			||||||
 | 
					                historyItem.setChannelName(formatChannelName(channelCode));
 | 
				
			||||||
 | 
					                historyItem.setStatus(status.toUpperCase()); // Ensure uppercase for consistency
 | 
				
			||||||
 | 
					                historyItem.setReferenceId(referenceId);
 | 
				
			||||||
 | 
					                historyItem.setFullDate(transactionDate);
 | 
				
			||||||
 | 
					                historyItem.setChannelCode(channelCode);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Add to both lists (since we're limiting to 10 in API call)
 | 
				
			||||||
 | 
					                historyList.add(historyItem);
 | 
				
			||||||
 | 
					                fullHistoryData.add(historyItem);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            runOnUiThread(new Runnable() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void run() {
 | 
				
			||||||
 | 
					                    adapter.notifyDataSetChanged();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (JSONException e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					            runOnUiThread(new Runnable() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void run() {
 | 
				
			||||||
 | 
					                    Toast.makeText(HistoryActivity.this, "Error parsing data", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    loadSampleData();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void processSummaryData(JSONArray dataArray) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            long totalAmount = 0;
 | 
				
			||||||
 | 
					            int totalTransactions = 0;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for (int i = 0; i < dataArray.length(); i++) {
 | 
				
			||||||
 | 
					                JSONObject item = dataArray.getJSONObject(i);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String amount = item.getString("amount");
 | 
				
			||||||
 | 
					                String status = item.getString("status");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Only count if status is settlement or success
 | 
				
			||||||
 | 
					                if ("SETTLEMENT".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status)) {
 | 
				
			||||||
 | 
					                    // Parse amount safely
 | 
				
			||||||
 | 
					                    double amountValue = 0;
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        amountValue = Double.parseDouble(amount);
 | 
				
			||||||
 | 
					                    } catch (NumberFormatException e) {
 | 
				
			||||||
 | 
					                        amountValue = 0;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    totalAmount += (long) amountValue;
 | 
				
			||||||
 | 
					                    totalTransactions++;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            final long finalTotalAmount = totalAmount;
 | 
				
			||||||
 | 
					            final int finalTotalTransactions = totalTransactions;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            runOnUiThread(new Runnable() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void run() {
 | 
				
			||||||
 | 
					                    updateSummary(finalTotalAmount, finalTotalTransactions);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (JSONException e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					            runOnUiThread(new Runnable() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void run() {
 | 
				
			||||||
 | 
					                    Toast.makeText(HistoryActivity.this, "Error parsing summary data", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    // Set default values if summary fails
 | 
				
			||||||
 | 
					                    updateSummary(0, 0);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void updateSummary(long totalAmount, int totalTransactions) {
 | 
				
			||||||
 | 
					        tvTotalAmount.setText("RP " + formatCurrency(totalAmount));
 | 
				
			||||||
 | 
					        tvTotalTransactions.setText(String.valueOf(totalTransactions));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void loadSampleData() {
 | 
				
			||||||
 | 
					        historyList.clear();
 | 
				
			||||||
 | 
					        fullHistoryData.clear();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Get today's date for sample data
 | 
				
			||||||
 | 
					        SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
 | 
				
			||||||
 | 
					        SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
 | 
				
			||||||
 | 
					        Date now = new Date();
 | 
				
			||||||
 | 
					        String todayDate = dateFormat.format(now);
 | 
				
			||||||
 | 
					        String currentTime = timeFormat.format(now);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Create sample data with today's date - limit to 10 items
 | 
				
			||||||
 | 
					        HistoryItem[] sampleData = {
 | 
				
			||||||
 | 
					            new HistoryItem("08:30", todayDate, 1500000, "QRIS", "SUCCESS", "TXN001"),
 | 
				
			||||||
 | 
					            new HistoryItem("09:15", todayDate, 750000, "Debit", "SUCCESS", "TXN002"),
 | 
				
			||||||
 | 
					            new HistoryItem("10:20", todayDate, 2250000, "QRIS", "FAILED", "TXN003"),
 | 
				
			||||||
 | 
					            new HistoryItem("11:45", todayDate, 980000, "Kredit", "SUCCESS", "TXN004"),
 | 
				
			||||||
 | 
					            new HistoryItem("12:30", todayDate, 1800000, "QRIS", "SUCCESS", "TXN005"),
 | 
				
			||||||
 | 
					            new HistoryItem("13:15", todayDate, 650000, "Debit", "FAILED", "TXN006"),
 | 
				
			||||||
 | 
					            new HistoryItem("14:00", todayDate, 3200000, "QRIS", "SUCCESS", "TXN007"),
 | 
				
			||||||
 | 
					            new HistoryItem("15:30", todayDate, 1100000, "Kredit", "SUCCESS", "TXN008"),
 | 
				
			||||||
 | 
					            new HistoryItem("16:45", todayDate, 890000, "Debit", "FAILED", "TXN009"),
 | 
				
			||||||
 | 
					            new HistoryItem("17:20", todayDate, 2100000, "QRIS", "SUCCESS", "TXN010")
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        long totalAmount = 0;
 | 
				
			||||||
 | 
					        for (HistoryItem item : sampleData) {
 | 
				
			||||||
 | 
					            historyList.add(item);
 | 
				
			||||||
 | 
					            fullHistoryData.add(item);
 | 
				
			||||||
 | 
					            totalAmount += item.getAmount();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        tvTotalAmount.setText("RP " + formatCurrency(totalAmount));
 | 
				
			||||||
 | 
					        tvTotalTransactions.setText("10");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        adapter.notifyDataSetChanged();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatChannelName(String channelCode) {
 | 
				
			||||||
 | 
					        switch (channelCode) {
 | 
				
			||||||
 | 
					            case "DEBIT_CARD":
 | 
				
			||||||
 | 
					                return "Debit";
 | 
				
			||||||
 | 
					            case "CREDIT_CARD":
 | 
				
			||||||
 | 
					                return "Kredit";
 | 
				
			||||||
 | 
					            case "QRIS":
 | 
				
			||||||
 | 
					                return "QRIS";
 | 
				
			||||||
 | 
					            case "E_MONEY":
 | 
				
			||||||
 | 
					                return "E-Money";
 | 
				
			||||||
 | 
					            case "OTHER":
 | 
				
			||||||
 | 
					                return "Lainnya";
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                return channelCode.substring(0, 1).toUpperCase() + 
 | 
				
			||||||
 | 
					                       channelCode.substring(1).toLowerCase();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatTime(String isoDate) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
 | 
				
			||||||
 | 
					            inputFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            SimpleDateFormat outputFormat = new SimpleDateFormat("HH.mm.ss", Locale.getDefault());
 | 
				
			||||||
 | 
					            outputFormat.setTimeZone(TimeZone.getDefault());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Date date = inputFormat.parse(isoDate);
 | 
				
			||||||
 | 
					            return outputFormat.format(date);
 | 
				
			||||||
 | 
					        } catch (ParseException e) {
 | 
				
			||||||
 | 
					            return "00.00.00";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String formatDate(String isoDate) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
 | 
				
			||||||
 | 
					            inputFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
 | 
				
			||||||
 | 
					            outputFormat.setTimeZone(TimeZone.getDefault());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Date date = inputFormat.parse(isoDate);
 | 
				
			||||||
 | 
					            return outputFormat.format(date);
 | 
				
			||||||
 | 
					        } catch (ParseException e) {
 | 
				
			||||||
 | 
					            SimpleDateFormat fallbackFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
 | 
				
			||||||
 | 
					            return fallbackFormat.format(new Date());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatCurrency(long amount) {
 | 
				
			||||||
 | 
					        NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
 | 
				
			||||||
 | 
					        return formatter.format(amount);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Public static method to get full data for detail activity
 | 
				
			||||||
 | 
					    public static List<HistoryItem> getFullHistoryData() {
 | 
				
			||||||
 | 
					        return new ArrayList<>(fullHistoryData);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // AsyncTask for main API call (transaction list with limit)
 | 
				
			||||||
 | 
					    private class ApiTask extends AsyncTask<String, Void, String> {
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected String doInBackground(String... urls) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                URL url = new URL(urls[0]);
 | 
				
			||||||
 | 
					                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                connection.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(10000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(10000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                if (responseCode == HttpURLConnection.HTTP_OK) {
 | 
				
			||||||
 | 
					                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(line);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    reader.close();
 | 
				
			||||||
 | 
					                    return response.toString();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (IOException e) {
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected void onPostExecute(String result) {
 | 
				
			||||||
 | 
					            if (result != null) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    JSONObject jsonResponse = new JSONObject(result);
 | 
				
			||||||
 | 
					                    if (jsonResponse.getInt("status") == 200) {
 | 
				
			||||||
 | 
					                        JSONObject results = jsonResponse.getJSONObject("results");
 | 
				
			||||||
 | 
					                        JSONArray dataArray = results.getJSONArray("data");
 | 
				
			||||||
 | 
					                        processApiData(dataArray);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        Toast.makeText(HistoryActivity.this, "API Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                        loadSampleData();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } catch (JSONException e) {
 | 
				
			||||||
 | 
					                    e.printStackTrace();
 | 
				
			||||||
 | 
					                    Toast.makeText(HistoryActivity.this, "JSON Parse Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    loadSampleData();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Toast.makeText(HistoryActivity.this, "Network Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                loadSampleData();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // AsyncTask for summary API call (all transactions for totals)
 | 
				
			||||||
 | 
					    private class SummaryApiTask extends AsyncTask<String, Void, String> {
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected String doInBackground(String... urls) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                URL url = new URL(urls[0]);
 | 
				
			||||||
 | 
					                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                connection.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(10000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(10000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                if (responseCode == HttpURLConnection.HTTP_OK) {
 | 
				
			||||||
 | 
					                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(line);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    reader.close();
 | 
				
			||||||
 | 
					                    return response.toString();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (IOException e) {
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected void onPostExecute(String result) {
 | 
				
			||||||
 | 
					            if (result != null) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    JSONObject jsonResponse = new JSONObject(result);
 | 
				
			||||||
 | 
					                    if (jsonResponse.getInt("status") == 200) {
 | 
				
			||||||
 | 
					                        JSONArray dataArray = jsonResponse.getJSONArray("data");
 | 
				
			||||||
 | 
					                        processSummaryData(dataArray);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        Toast.makeText(HistoryActivity.this, "Summary API Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                        updateSummary(0, 0);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } catch (JSONException e) {
 | 
				
			||||||
 | 
					                    e.printStackTrace();
 | 
				
			||||||
 | 
					                    Toast.makeText(HistoryActivity.this, "Summary JSON Parse Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    updateSummary(0, 0);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Toast.makeText(HistoryActivity.this, "Summary Network Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                updateSummary(0, 0);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HistoryItem class - enhanced with more fields
 | 
				
			||||||
 | 
					class HistoryItem {
 | 
				
			||||||
 | 
					    private String time;
 | 
				
			||||||
 | 
					    private String date;
 | 
				
			||||||
 | 
					    private long amount;
 | 
				
			||||||
 | 
					    private String channelName;
 | 
				
			||||||
 | 
					    private String status;
 | 
				
			||||||
 | 
					    private String referenceId;
 | 
				
			||||||
 | 
					    private String fullDate;
 | 
				
			||||||
 | 
					    private String channelCode;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public HistoryItem() {}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public HistoryItem(String time, String date, long amount, String channelName, String status, String referenceId) {
 | 
				
			||||||
 | 
					        this.time = time;
 | 
				
			||||||
 | 
					        this.date = date;
 | 
				
			||||||
 | 
					        this.amount = amount;
 | 
				
			||||||
 | 
					        this.channelName = channelName;
 | 
				
			||||||
 | 
					        this.status = status;
 | 
				
			||||||
 | 
					        this.referenceId = referenceId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Getters and Setters
 | 
				
			||||||
 | 
					    public String getTime() { return time; }
 | 
				
			||||||
 | 
					    public void setTime(String time) { this.time = time; }
 | 
				
			||||||
 | 
					    public String getDate() { return date; }
 | 
				
			||||||
 | 
					    public void setDate(String date) { this.date = date; }
 | 
				
			||||||
 | 
					    public long getAmount() { return amount; }
 | 
				
			||||||
 | 
					    public void setAmount(long amount) { this.amount = amount; }
 | 
				
			||||||
 | 
					    public String getChannelName() { return channelName; }
 | 
				
			||||||
 | 
					    public void setChannelName(String channelName) { this.channelName = channelName; }
 | 
				
			||||||
 | 
					    public String getStatus() { return status; }
 | 
				
			||||||
 | 
					    public void setStatus(String status) { this.status = status; }
 | 
				
			||||||
 | 
					    public String getReferenceId() { return referenceId; }
 | 
				
			||||||
 | 
					    public void setReferenceId(String referenceId) { this.referenceId = referenceId; }
 | 
				
			||||||
 | 
					    public String getFullDate() { return fullDate; }
 | 
				
			||||||
 | 
					    public void setFullDate(String fullDate) { this.fullDate = fullDate; }
 | 
				
			||||||
 | 
					    public String getChannelCode() { return channelCode; }
 | 
				
			||||||
 | 
					    public void setChannelCode(String channelCode) { this.channelCode = channelCode; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HistoryAdapter class - simplified and stable
 | 
				
			||||||
 | 
					class HistoryAdapter extends RecyclerView.Adapter<HistoryAdapter.HistoryViewHolder> {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private List<HistoryItem> historyList;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public HistoryAdapter(List<HistoryItem> historyList) {
 | 
				
			||||||
 | 
					        this.historyList = historyList;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @NonNull
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public HistoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 | 
				
			||||||
 | 
					        View view = LayoutInflater.from(parent.getContext())
 | 
				
			||||||
 | 
					                .inflate(R.layout.item_history, parent, false);
 | 
				
			||||||
 | 
					        return new HistoryViewHolder(view);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onBindViewHolder(@NonNull HistoryViewHolder holder, int position) {
 | 
				
			||||||
 | 
					        HistoryItem item = historyList.get(position);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Format time with dots instead of colons (09.00 instead of 09:00)
 | 
				
			||||||
 | 
					        String formattedTime = item.getTime().replace(":", ".") + ", " + item.getDate();
 | 
				
			||||||
 | 
					        holder.tvTime.setText(formattedTime);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        holder.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
 | 
				
			||||||
 | 
					        holder.tvChannel.setText(item.getChannelName());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set status color and text based on API response
 | 
				
			||||||
 | 
					        String status = item.getStatus();
 | 
				
			||||||
 | 
					        if ("SETTLEMENT".equalsIgnoreCase(status)) {
 | 
				
			||||||
 | 
					            holder.tvStatus.setText("Success");
 | 
				
			||||||
 | 
					            holder.tvStatus.setTextColor(Color.parseColor("#4CAF50")); // Green
 | 
				
			||||||
 | 
					        } else if ("SUCCESS".equalsIgnoreCase(status)) {
 | 
				
			||||||
 | 
					            holder.tvStatus.setText("Success");
 | 
				
			||||||
 | 
					            holder.tvStatus.setTextColor(Color.parseColor("#4CAF50")); // Green
 | 
				
			||||||
 | 
					        } else if ("EXPIRE".equalsIgnoreCase(status)) {
 | 
				
			||||||
 | 
					            holder.tvStatus.setText("Expired");
 | 
				
			||||||
 | 
					            holder.tvStatus.setTextColor(Color.parseColor("#F44336")); // Red
 | 
				
			||||||
 | 
					        } else if ("FAILED".equalsIgnoreCase(status)) {
 | 
				
			||||||
 | 
					            holder.tvStatus.setText("Failed");
 | 
				
			||||||
 | 
					            holder.tvStatus.setTextColor(Color.parseColor("#F44336")); // Red
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            holder.tvStatus.setText("Pending");
 | 
				
			||||||
 | 
					            holder.tvStatus.setTextColor(Color.parseColor("#3141FF")); // Blue
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public int getItemCount() {
 | 
				
			||||||
 | 
					        return historyList != null ? historyList.size() : 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatCurrency(long amount) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
 | 
				
			||||||
 | 
					            return formatter.format(amount);
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            return String.valueOf(amount);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    static class HistoryViewHolder extends RecyclerView.ViewHolder {
 | 
				
			||||||
 | 
					        TextView tvTime;
 | 
				
			||||||
 | 
					        TextView tvAmount;
 | 
				
			||||||
 | 
					        TextView tvChannel;
 | 
				
			||||||
 | 
					        TextView tvStatus;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public HistoryViewHolder(@NonNull View itemView) {
 | 
				
			||||||
 | 
					            super(itemView);
 | 
				
			||||||
 | 
					            tvTime = itemView.findViewById(R.id.tv_time);
 | 
				
			||||||
 | 
					            tvAmount = itemView.findViewById(R.id.tv_amount);
 | 
				
			||||||
 | 
					            tvChannel = itemView.findViewById(R.id.tv_channel);
 | 
				
			||||||
 | 
					            tvStatus = itemView.findViewById(R.id.tv_status);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,338 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.histori;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.BuildConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import android.graphics.Color;
 | 
				
			||||||
 | 
					import android.os.AsyncTask;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.view.LayoutInflater;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.view.ViewGroup;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.LinearLayoutManager;
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.json.JSONArray;
 | 
				
			||||||
 | 
					import org.json.JSONException;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.text.ParseException;
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Date;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					import java.util.TimeZone;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class HistoryListActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private RecyclerView rvHistory;
 | 
				
			||||||
 | 
					    private TextView tvEmpty;
 | 
				
			||||||
 | 
					    private HistoryListAdapter adapter;
 | 
				
			||||||
 | 
					    private List<Transaction> transactionList = new ArrayList<>();
 | 
				
			||||||
 | 
					    private String API_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_history_list);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Initialize views
 | 
				
			||||||
 | 
					        rvHistory = findViewById(R.id.rv_history);
 | 
				
			||||||
 | 
					        tvEmpty = findViewById(R.id.tv_empty);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set up RecyclerView
 | 
				
			||||||
 | 
					        adapter = new HistoryListAdapter(transactionList);
 | 
				
			||||||
 | 
					        rvHistory.setLayoutManager(new LinearLayoutManager(this));
 | 
				
			||||||
 | 
					        rvHistory.setAdapter(adapter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set up app bar
 | 
				
			||||||
 | 
					        setupAppBar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Build API URL and load data
 | 
				
			||||||
 | 
					        buildApiUrl();
 | 
				
			||||||
 | 
					        fetchTransactionData();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void buildApiUrl() {
 | 
				
			||||||
 | 
					        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
 | 
				
			||||||
 | 
					        String currentDate = dateFormat.format(new Date());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Gunakan BuildConfig untuk base URL
 | 
				
			||||||
 | 
					        API_URL = BuildConfig.BACKEND_BASE_URL + "/transactions/list?from_date=" + currentDate + 
 | 
				
			||||||
 | 
					                 "&to_date=" + currentDate + "&location_id=0&merchant_id=0";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void setupAppBar() {
 | 
				
			||||||
 | 
					        LinearLayout backNavigation = findViewById(R.id.back_navigation);
 | 
				
			||||||
 | 
					        TextView appbarTitle = findViewById(R.id.appbarTitle);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        appbarTitle.setText("Kembali");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        backNavigation.setOnClickListener(v -> onBackPressed());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void fetchTransactionData() {
 | 
				
			||||||
 | 
					        new FetchTransactionsTask().execute(API_URL);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void updateTransactionList(List<Transaction> transactions) {
 | 
				
			||||||
 | 
					        transactionList.clear();
 | 
				
			||||||
 | 
					        if (transactions != null && !transactions.isEmpty()) {
 | 
				
			||||||
 | 
					            transactionList.addAll(transactions);
 | 
				
			||||||
 | 
					            tvEmpty.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					            rvHistory.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            tvEmpty.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					            rvHistory.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        adapter.notifyDataSetChanged();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // AsyncTask to fetch transactions from API
 | 
				
			||||||
 | 
					    private class FetchTransactionsTask extends AsyncTask<String, Void, String> {
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected String doInBackground(String... urls) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                URL url = new URL(urls[0]);
 | 
				
			||||||
 | 
					                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                connection.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(10000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(10000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                if (responseCode == HttpURLConnection.HTTP_OK) {
 | 
				
			||||||
 | 
					                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(line);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    reader.close();
 | 
				
			||||||
 | 
					                    return response.toString();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (IOException e) {
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected void onPostExecute(String result) {
 | 
				
			||||||
 | 
					            if (result != null) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    JSONObject jsonResponse = new JSONObject(result);
 | 
				
			||||||
 | 
					                    if (jsonResponse.getInt("status") == 200) {
 | 
				
			||||||
 | 
					                        JSONArray dataArray = jsonResponse.getJSONArray("data");
 | 
				
			||||||
 | 
					                        List<Transaction> transactions = parseTransactions(dataArray);
 | 
				
			||||||
 | 
					                        updateTransactionList(transactions);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        showError("API Error: " + jsonResponse.getString("message"));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } catch (JSONException e) {
 | 
				
			||||||
 | 
					                    e.printStackTrace();
 | 
				
			||||||
 | 
					                    showError("Error parsing data");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                showError("Network error");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private List<Transaction> parseTransactions(JSONArray dataArray) throws JSONException {
 | 
				
			||||||
 | 
					        List<Transaction> transactions = new ArrayList<>();
 | 
				
			||||||
 | 
					        for (int i = 0; i < dataArray.length(); i++) {
 | 
				
			||||||
 | 
					            JSONObject item = dataArray.getJSONObject(i);
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                Transaction transaction = new Transaction(
 | 
				
			||||||
 | 
					                    item.getString("transaction_date"),
 | 
				
			||||||
 | 
					                    item.getString("amount"),
 | 
				
			||||||
 | 
					                    item.getString("channel_code"),
 | 
				
			||||||
 | 
					                    item.getString("status")
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                transactions.add(transaction);
 | 
				
			||||||
 | 
					            } catch (ParseException e) {
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Urutkan dari terbaru ke terlama
 | 
				
			||||||
 | 
					        transactions.sort((t1, t2) -> {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
 | 
				
			||||||
 | 
					                Date d1 = sdf.parse(t1.getDateTime());
 | 
				
			||||||
 | 
					                Date d2 = sdf.parse(t2.getDateTime());
 | 
				
			||||||
 | 
					                return d2.compareTo(d1); // terbaru di atas
 | 
				
			||||||
 | 
					            } catch (ParseException e) {
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					                return 0;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return transactions;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void showError(String message) {
 | 
				
			||||||
 | 
					        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					        tvEmpty.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        rvHistory.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Transaction model class
 | 
				
			||||||
 | 
					    public static class Transaction {
 | 
				
			||||||
 | 
					        private final String dateTime;
 | 
				
			||||||
 | 
					        private final String amount;
 | 
				
			||||||
 | 
					        private final String channel;
 | 
				
			||||||
 | 
					        private final String status;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public Transaction(String dateTime, String amount, String channel, String status) throws ParseException {
 | 
				
			||||||
 | 
					            this.dateTime = dateTime;
 | 
				
			||||||
 | 
					            this.amount = amount;
 | 
				
			||||||
 | 
					            this.channel = channel;
 | 
				
			||||||
 | 
					            this.status = status;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String getDateTime() {
 | 
				
			||||||
 | 
					            return dateTime;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String getAmount() {
 | 
				
			||||||
 | 
					            return amount;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String getChannel() {
 | 
				
			||||||
 | 
					            return channel;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String getStatus() {
 | 
				
			||||||
 | 
					            return status;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Adapter class
 | 
				
			||||||
 | 
					    public class HistoryListAdapter extends RecyclerView.Adapter<HistoryListAdapter.ViewHolder> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private final List<Transaction> transactions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public HistoryListAdapter(List<Transaction> transactions) {
 | 
				
			||||||
 | 
					            this.transactions = transactions;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
 | 
				
			||||||
 | 
					            View view = LayoutInflater.from(parent.getContext())
 | 
				
			||||||
 | 
					                    .inflate(R.layout.item_history_list, parent, false);
 | 
				
			||||||
 | 
					            return new ViewHolder(view);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public void onBindViewHolder(ViewHolder holder, int position) {
 | 
				
			||||||
 | 
					            Transaction transaction = transactions.get(position);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            // Format date with timezone conversion
 | 
				
			||||||
 | 
					            SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
 | 
				
			||||||
 | 
					            inputFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            SimpleDateFormat outputFormat = new SimpleDateFormat("HH:mm.ss, dd MMM yyyy", Locale.getDefault());
 | 
				
			||||||
 | 
					            outputFormat.setTimeZone(TimeZone.getDefault());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                Date date = inputFormat.parse(transaction.getDateTime());
 | 
				
			||||||
 | 
					                holder.tvTime.setText(outputFormat.format(date));
 | 
				
			||||||
 | 
					            } catch (ParseException e) {
 | 
				
			||||||
 | 
					                holder.tvTime.setText(transaction.getDateTime());
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Format amount
 | 
				
			||||||
 | 
					            holder.tvAmount.setText(String.format(Locale.getDefault(), "Rp. %s", transaction.getAmount()));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Set channel and status
 | 
				
			||||||
 | 
					            holder.tvChannel.setText(formatChannelName(transaction.getChannel()));
 | 
				
			||||||
 | 
					            holder.tvStatus.setText(formatStatusText(transaction.getStatus()));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Set status color
 | 
				
			||||||
 | 
					            int statusColor = Color.parseColor("#000000"); // default black
 | 
				
			||||||
 | 
					            if ("SUCCESS".equalsIgnoreCase(transaction.getStatus())) {
 | 
				
			||||||
 | 
					                statusColor = Color.parseColor("#4CAF50"); // green
 | 
				
			||||||
 | 
					            } else if ("FAILED".equalsIgnoreCase(transaction.getStatus())) {
 | 
				
			||||||
 | 
					                statusColor = Color.parseColor("#F44336"); // red
 | 
				
			||||||
 | 
					            } else if ("INIT".equalsIgnoreCase(transaction.getStatus())) {
 | 
				
			||||||
 | 
					                statusColor = Color.parseColor("#FF9800"); // orange
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            holder.tvStatus.setTextColor(statusColor);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public int getItemCount() {
 | 
				
			||||||
 | 
					            return transactions.size();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private String formatChannelName(String channelCode) {
 | 
				
			||||||
 | 
					            switch (channelCode) {
 | 
				
			||||||
 | 
					                case "E_MONEY":
 | 
				
			||||||
 | 
					                    return "E-Money";
 | 
				
			||||||
 | 
					                case "QRIS":
 | 
				
			||||||
 | 
					                    return "QRIS";
 | 
				
			||||||
 | 
					                case "CREDIT_CARD":
 | 
				
			||||||
 | 
					                    return "Kredit";
 | 
				
			||||||
 | 
					                case "DEBIT_CARD":
 | 
				
			||||||
 | 
					                    return "Debit";
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    return channelCode;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private String formatStatusText(String status) {
 | 
				
			||||||
 | 
					            switch (status) {
 | 
				
			||||||
 | 
					                case "SUCCESS":
 | 
				
			||||||
 | 
					                    return "Berhasil";
 | 
				
			||||||
 | 
					                case "FAILED":
 | 
				
			||||||
 | 
					                    return "Gagal";
 | 
				
			||||||
 | 
					                case "INIT":
 | 
				
			||||||
 | 
					                    return "Tertunda";
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    return status;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public class ViewHolder extends RecyclerView.ViewHolder {
 | 
				
			||||||
 | 
					            public final TextView tvTime;
 | 
				
			||||||
 | 
					            public final TextView tvAmount;
 | 
				
			||||||
 | 
					            public final TextView tvChannel;
 | 
				
			||||||
 | 
					            public final TextView tvStatus;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            public ViewHolder(View view) {
 | 
				
			||||||
 | 
					                super(view);
 | 
				
			||||||
 | 
					                tvTime = view.findViewById(R.id.tv_time);
 | 
				
			||||||
 | 
					                tvAmount = view.findViewById(R.id.tv_amount);
 | 
				
			||||||
 | 
					                tvChannel = view.findViewById(R.id.tv_channel);
 | 
				
			||||||
 | 
					                tvStatus = view.findViewById(R.id.tv_status);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Set click listener if needed
 | 
				
			||||||
 | 
					                view.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                    int position = getAdapterPosition();
 | 
				
			||||||
 | 
					                    if (position != RecyclerView.NO_POSITION) {
 | 
				
			||||||
 | 
					                        Transaction transaction = transactions.get(position);
 | 
				
			||||||
 | 
					                        // TODO: Handle item click, maybe open detail activity
 | 
				
			||||||
 | 
					                        // Intent intent = new Intent(HistoryListActivity.this, HistoryDetailActivity.class);
 | 
				
			||||||
 | 
					                        // Pass transaction data to detail activity
 | 
				
			||||||
 | 
					                        // startActivity(intent);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,424 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.infotoko;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.SharedPreferences;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.text.InputType;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.view.ViewParent;
 | 
				
			||||||
 | 
					import android.widget.EditText;
 | 
				
			||||||
 | 
					import android.widget.ImageView;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					import androidx.cardview.widget.CardView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.LoginActivity;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					import com.google.android.material.button.MaterialButton;
 | 
				
			||||||
 | 
					import com.google.android.material.textfield.TextInputEditText;
 | 
				
			||||||
 | 
					import com.google.android.material.textfield.TextInputLayout;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.text.method.PasswordTransformationMethod;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.json.JSONException;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class InfoTokoActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final String TAG = "InfoTokoActivity";
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Views
 | 
				
			||||||
 | 
					    private TextView tvStoreName;
 | 
				
			||||||
 | 
					    private TextView tvMerchantId;
 | 
				
			||||||
 | 
					    private TextView tvTerminalId;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private TextInputEditText etEmail;
 | 
				
			||||||
 | 
					    private TextInputEditText etPassword;
 | 
				
			||||||
 | 
					    private TextInputEditText etOwnerName;
 | 
				
			||||||
 | 
					    private TextInputEditText etNik;
 | 
				
			||||||
 | 
					    private TextInputEditText etPhone;
 | 
				
			||||||
 | 
					    private TextInputEditText etBusinessType;
 | 
				
			||||||
 | 
					    private TextInputEditText etBusinessName;
 | 
				
			||||||
 | 
					    private TextInputEditText etAddress;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private MaterialButton btnUpdate;
 | 
				
			||||||
 | 
					    private LinearLayout backNavigation; // Changed from ImageView to LinearLayout
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Data
 | 
				
			||||||
 | 
					    private String authToken;
 | 
				
			||||||
 | 
					    private JSONObject userData;
 | 
				
			||||||
 | 
					    private String userPassword; // Add password storage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_info_toko);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check if user is logged in
 | 
				
			||||||
 | 
					        if (!LoginActivity.isLoggedIn(this)) {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Initialize views
 | 
				
			||||||
 | 
					        initializeViews();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Load user data
 | 
				
			||||||
 | 
					        loadUserData();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Setup listeners
 | 
				
			||||||
 | 
					        setupListeners();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Display store information
 | 
				
			||||||
 | 
					        displayStoreInfo();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void initializeViews() {
 | 
				
			||||||
 | 
					        // Header
 | 
				
			||||||
 | 
					        tvStoreName = findViewById(R.id.tv_store_name);
 | 
				
			||||||
 | 
					        tvMerchantId = findViewById(R.id.tv_merchant_id);
 | 
				
			||||||
 | 
					        tvTerminalId = findViewById(R.id.tv_terminal_id);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Find the back navigation from the included layout
 | 
				
			||||||
 | 
					        backNavigation = findViewById(R.id.back_navigation);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Optionally, you can also update the title in the appbar
 | 
				
			||||||
 | 
					        TextView appbarTitle = findViewById(R.id.appbarTitle);
 | 
				
			||||||
 | 
					        if (appbarTitle != null) {
 | 
				
			||||||
 | 
					            appbarTitle.setText("Kembali");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Account Information
 | 
				
			||||||
 | 
					        etEmail = findViewById(R.id.et_email);
 | 
				
			||||||
 | 
					        etPassword = findViewById(R.id.et_password);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Store Information
 | 
				
			||||||
 | 
					        etOwnerName = findViewById(R.id.et_owner_name);
 | 
				
			||||||
 | 
					        etNik = findViewById(R.id.et_nik);
 | 
				
			||||||
 | 
					        etPhone = findViewById(R.id.et_phone);
 | 
				
			||||||
 | 
					        etBusinessType = findViewById(R.id.et_business_type);
 | 
				
			||||||
 | 
					        etBusinessName = findViewById(R.id.et_business_name);
 | 
				
			||||||
 | 
					        etAddress = findViewById(R.id.et_address);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Button
 | 
				
			||||||
 | 
					        btnUpdate = findViewById(R.id.btn_update);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void loadUserData() {
 | 
				
			||||||
 | 
					        // Get authentication token
 | 
				
			||||||
 | 
					        authToken = getIntent().getStringExtra("AUTH_TOKEN");
 | 
				
			||||||
 | 
					        if (authToken == null) {
 | 
				
			||||||
 | 
					            authToken = LoginActivity.getToken(this);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Get user data
 | 
				
			||||||
 | 
					        String userDataString = getIntent().getStringExtra("USER_DATA");
 | 
				
			||||||
 | 
					        if (userDataString != null) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                userData = new JSONObject(userDataString);
 | 
				
			||||||
 | 
					            } catch (JSONException e) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "Error parsing user data: " + e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (userData == null) {
 | 
				
			||||||
 | 
					            userData = LoginActivity.getUserDataAsJson(this);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Get saved password from SharedPreferences
 | 
				
			||||||
 | 
					        SharedPreferences prefs = getSharedPreferences("LoginPrefs", MODE_PRIVATE);
 | 
				
			||||||
 | 
					        userPassword = prefs.getString("current_password", ""); // Fix: use correct key
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "Loaded auth token: " + (authToken != null ? "✓" : "✗"));
 | 
				
			||||||
 | 
					        Log.d(TAG, "Loaded user data: " + (userData != null ? "✓" : "✗"));
 | 
				
			||||||
 | 
					        Log.d(TAG, "Loaded password: " + (!userPassword.isEmpty() ? "✓" : "✗"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupListeners() {
 | 
				
			||||||
 | 
					        // Back button - now using the LinearLayout
 | 
				
			||||||
 | 
					        if (backNavigation != null) {
 | 
				
			||||||
 | 
					            backNavigation.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                Log.d(TAG, "Back button clicked");
 | 
				
			||||||
 | 
					                finish();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Back navigation not found!");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Update button
 | 
				
			||||||
 | 
					        if (btnUpdate != null) {
 | 
				
			||||||
 | 
					            btnUpdate.setOnClickListener(v -> updateStoreInfo());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Password toggle listener
 | 
				
			||||||
 | 
					        setupPasswordToggle();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupPasswordToggle() {
 | 
				
			||||||
 | 
					        ViewParent passwordParentView = etPassword.getParent().getParent();
 | 
				
			||||||
 | 
					        if (passwordParentView instanceof TextInputLayout) {
 | 
				
			||||||
 | 
					            TextInputLayout passwordLayout = (TextInputLayout) passwordParentView;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Set initial state to visible
 | 
				
			||||||
 | 
					            etPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            passwordLayout.setEndIconOnClickListener(v -> {
 | 
				
			||||||
 | 
					                // Toggle password visibility
 | 
				
			||||||
 | 
					                if (etPassword.getTransformationMethod() == null) {
 | 
				
			||||||
 | 
					                    // Hide password
 | 
				
			||||||
 | 
					                    etPassword.setTransformationMethod(PasswordTransformationMethod.getInstance());
 | 
				
			||||||
 | 
					                    passwordLayout.setEndIconDrawable(R.drawable.ic_visibility_off); // Set your eye-off icon
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // Show password
 | 
				
			||||||
 | 
					                    etPassword.setTransformationMethod(null);
 | 
				
			||||||
 | 
					                    passwordLayout.setEndIconDrawable(R.drawable.ic_visibility); // Set your eye icon
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Move cursor to end
 | 
				
			||||||
 | 
					                etPassword.setSelection(etPassword.getText().length());
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void displayStoreInfo() {
 | 
				
			||||||
 | 
					        // Display store name and IDs (static for header)
 | 
				
			||||||
 | 
					        tvStoreName.setText("TOKO KLONTONG PAK EKO");
 | 
				
			||||||
 | 
					        tvMerchantId.setText("MID: 12345678901");
 | 
				
			||||||
 | 
					        tvTerminalId.setText("TID: 12345678901");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Hide fields that are not needed based on requirements
 | 
				
			||||||
 | 
					        hideUnnecessaryFields();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Display data from login response
 | 
				
			||||||
 | 
					        if (userData != null) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                // Email - from API response
 | 
				
			||||||
 | 
					                String email = userData.optString("email", "");
 | 
				
			||||||
 | 
					                if (!email.isEmpty()) {
 | 
				
			||||||
 | 
					                    etEmail.setText(email);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    etEmail.setText("Email tidak tersedia");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Password - show actual password from SharedPreferences (VISIBLE by default)
 | 
				
			||||||
 | 
					                if (!userPassword.isEmpty()) {
 | 
				
			||||||
 | 
					                    etPassword.setText(userPassword);
 | 
				
			||||||
 | 
					                    // Start with password visible
 | 
				
			||||||
 | 
					                    etPassword.setTransformationMethod(null);
 | 
				
			||||||
 | 
					                    // Refresh the eye icon state
 | 
				
			||||||
 | 
					                    ViewParent passwordParentView = etPassword.getParent().getParent();
 | 
				
			||||||
 | 
					                    if (passwordParentView instanceof TextInputLayout) {
 | 
				
			||||||
 | 
					                        ((TextInputLayout) passwordParentView).setEndIconDrawable(R.drawable.ic_visibility);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    etPassword.setText("");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                etPassword.setEnabled(true); // Enable for display with toggle
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Update the eye icon to show "hide" state initially
 | 
				
			||||||
 | 
					                ViewParent passwordParentView = etPassword.getParent().getParent();
 | 
				
			||||||
 | 
					                if (passwordParentView instanceof TextInputLayout) {
 | 
				
			||||||
 | 
					                    TextInputLayout passwordLayout = (TextInputLayout) passwordParentView;
 | 
				
			||||||
 | 
					                    passwordLayout.setPasswordVisibilityToggleEnabled(true);
 | 
				
			||||||
 | 
					                    // Force refresh the toggle icon
 | 
				
			||||||
 | 
					                    passwordLayout.refreshDrawableState();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Debug log
 | 
				
			||||||
 | 
					                Log.d(TAG, "Password field text: " + etPassword.getText().toString());
 | 
				
			||||||
 | 
					                Log.d(TAG, "Password field length: " + etPassword.getText().length());
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Nama Pemilik - from API response
 | 
				
			||||||
 | 
					                String ownerName = userData.optString("name", "");
 | 
				
			||||||
 | 
					                if (!ownerName.isEmpty()) {
 | 
				
			||||||
 | 
					                    etOwnerName.setText(ownerName);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    etOwnerName.setText("Nama tidak tersedia");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Nomor Telepon - from API response
 | 
				
			||||||
 | 
					                String phone = userData.optString("phone", "");
 | 
				
			||||||
 | 
					                if (!phone.isEmpty()) {
 | 
				
			||||||
 | 
					                    etPhone.setText(phone);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    etPhone.setText("Nomor telepon tidak tersedia");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Log the user data for debugging
 | 
				
			||||||
 | 
					                Log.d(TAG, "User Data: " + userData.toString());
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "Error displaying user info: " + e.getMessage());
 | 
				
			||||||
 | 
					                Toast.makeText(this, "Error loading user data", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Show default/empty values if no user data
 | 
				
			||||||
 | 
					            etEmail.setText("Email tidak tersedia");
 | 
				
			||||||
 | 
					            etPassword.setText("••••••••");
 | 
				
			||||||
 | 
					            etPassword.setEnabled(false);
 | 
				
			||||||
 | 
					            etOwnerName.setText("Nama tidak tersedia");
 | 
				
			||||||
 | 
					            etPhone.setText("Nomor telepon tidak tersedia");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void hideUnnecessaryFields() {
 | 
				
			||||||
 | 
					        // Hide NIK field and its container
 | 
				
			||||||
 | 
					        ViewParent nikContainer = etNik.getParent();
 | 
				
			||||||
 | 
					        if (nikContainer != null && nikContainer instanceof View) {
 | 
				
			||||||
 | 
					            ((View) nikContainer).setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Hide Business Type field and its container  
 | 
				
			||||||
 | 
					        ViewParent businessTypeContainer = etBusinessType.getParent();
 | 
				
			||||||
 | 
					        if (businessTypeContainer != null && businessTypeContainer instanceof View) {
 | 
				
			||||||
 | 
					            ((View) businessTypeContainer).setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Hide Business Name field and its container
 | 
				
			||||||
 | 
					        ViewParent businessNameContainer = etBusinessName.getParent();
 | 
				
			||||||
 | 
					        if (businessNameContainer != null && businessNameContainer instanceof View) {
 | 
				
			||||||
 | 
					            ((View) businessNameContainer).setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Hide Address field and its container
 | 
				
			||||||
 | 
					        ViewParent addressContainer = etAddress.getParent();
 | 
				
			||||||
 | 
					        if (addressContainer != null && addressContainer instanceof View) {
 | 
				
			||||||
 | 
					            ((View) addressContainer).setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Update the section title to be more accurate
 | 
				
			||||||
 | 
					        // Note: You'll need to add an ID to the section title TextView in the XML
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void updateStoreInfo() {
 | 
				
			||||||
 | 
					        // Validate inputs
 | 
				
			||||||
 | 
					        if (!validateInputs()) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Show loading
 | 
				
			||||||
 | 
					        btnUpdate.setEnabled(false);
 | 
				
			||||||
 | 
					        btnUpdate.setText("Memperbarui...");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Simulate update process (in real app, this would call API)
 | 
				
			||||||
 | 
					        btnUpdate.postDelayed(() -> {
 | 
				
			||||||
 | 
					            // In a real implementation, you would:
 | 
				
			||||||
 | 
					            // 1. Call API to update user info
 | 
				
			||||||
 | 
					            // 2. Update SharedPreferences with new data
 | 
				
			||||||
 | 
					            // 3. Show success/error message
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Show success message
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Informasi akun berhasil diperbarui", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // If password was changed, inform user
 | 
				
			||||||
 | 
					            String currentPasswordText = etPassword.getText().toString();
 | 
				
			||||||
 | 
					            if (!currentPasswordText.isEmpty() && !currentPasswordText.equals(userPassword)) {
 | 
				
			||||||
 | 
					                Toast.makeText(this, "Password berhasil diperbarui", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                userPassword = currentPasswordText; // Update local variable
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Reset button state
 | 
				
			||||||
 | 
					            btnUpdate.setEnabled(true);
 | 
				
			||||||
 | 
					            btnUpdate.setText("Perbarui Informasi Toko");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Optional: Save updated data locally
 | 
				
			||||||
 | 
					            saveUpdatedData();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        }, 2000); // Simulate 2 second delay
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private boolean validateInputs() {
 | 
				
			||||||
 | 
					        // Check email
 | 
				
			||||||
 | 
					        String email = etEmail.getText().toString().trim();
 | 
				
			||||||
 | 
					        if (email.isEmpty()) {
 | 
				
			||||||
 | 
					            etEmail.setError("Email tidak boleh kosong");
 | 
				
			||||||
 | 
					            etEmail.requestFocus();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
 | 
				
			||||||
 | 
					            etEmail.setError("Format email tidak valid");
 | 
				
			||||||
 | 
					            etEmail.requestFocus();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check owner name
 | 
				
			||||||
 | 
					        String ownerName = etOwnerName.getText().toString().trim();
 | 
				
			||||||
 | 
					        if (ownerName.isEmpty()) {
 | 
				
			||||||
 | 
					            etOwnerName.setError("Nama pemilik tidak boleh kosong");
 | 
				
			||||||
 | 
					            etOwnerName.requestFocus();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check phone
 | 
				
			||||||
 | 
					        String phone = etPhone.getText().toString().trim();
 | 
				
			||||||
 | 
					        if (phone.isEmpty()) {
 | 
				
			||||||
 | 
					            etPhone.setError("Nomor telepon tidak boleh kosong");
 | 
				
			||||||
 | 
					            etPhone.requestFocus();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (phone.length() < 10) {
 | 
				
			||||||
 | 
					            etPhone.setError("Nomor telepon tidak valid");
 | 
				
			||||||
 | 
					            etPhone.requestFocus();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check password if changed
 | 
				
			||||||
 | 
					        String password = etPassword.getText().toString();
 | 
				
			||||||
 | 
					        if (!password.isEmpty() && password.length() < 6) {
 | 
				
			||||||
 | 
					            etPassword.setError("Password minimal 6 karakter");
 | 
				
			||||||
 | 
					            etPassword.requestFocus();
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void saveUpdatedData() {
 | 
				
			||||||
 | 
					        // In a real app, this would update the user data in SharedPreferences
 | 
				
			||||||
 | 
					        // and call an API to update the server
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            JSONObject updatedData = new JSONObject();
 | 
				
			||||||
 | 
					            updatedData.put("email", etEmail.getText().toString().trim());
 | 
				
			||||||
 | 
					            updatedData.put("name", etOwnerName.getText().toString().trim());
 | 
				
			||||||
 | 
					            updatedData.put("phone", etPhone.getText().toString().trim());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Merge with existing userData
 | 
				
			||||||
 | 
					            if (userData != null) {
 | 
				
			||||||
 | 
					                // Keep other fields from original userData
 | 
				
			||||||
 | 
					                updatedData.put("id", userData.optString("id", ""));
 | 
				
			||||||
 | 
					                updatedData.put("role", userData.optString("role", ""));
 | 
				
			||||||
 | 
					                // Add other fields as needed
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Save updated password to SharedPreferences
 | 
				
			||||||
 | 
					            String newPassword = etPassword.getText().toString();
 | 
				
			||||||
 | 
					            if (!newPassword.isEmpty() && !newPassword.equals(userPassword)) {
 | 
				
			||||||
 | 
					                SharedPreferences prefs = getSharedPreferences("LoginPrefs", MODE_PRIVATE);
 | 
				
			||||||
 | 
					                prefs.edit().putString("current_password", newPassword).apply(); // Fix: use correct key
 | 
				
			||||||
 | 
					                Log.d(TAG, "Password updated in SharedPreferences");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Log.d(TAG, "Updated data: " + updatedData.toString());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // In real app, you would:
 | 
				
			||||||
 | 
					            // 1. Call API to update user data
 | 
				
			||||||
 | 
					            // 2. On success, update SharedPreferences:
 | 
				
			||||||
 | 
					            // SharedPreferences prefs = getSharedPreferences("LoginPrefs", MODE_PRIVATE);
 | 
				
			||||||
 | 
					            // prefs.edit().putString("user_data", updatedData.toString()).apply();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (JSONException e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Error creating updated data: " + e.getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onBackPressed() {
 | 
				
			||||||
 | 
					        super.onBackPressed();
 | 
				
			||||||
 | 
					        finish();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										901
									
								
								app/src/main/java/com/example/bdkipoc/qris/QrisActivity.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,901 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.view.QrisResultActivity;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.BuildConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.Context;
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import android.content.SharedPreferences;
 | 
				
			||||||
 | 
					import android.graphics.Bitmap;
 | 
				
			||||||
 | 
					import android.os.AsyncTask;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import android.view.MenuItem;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.view.inputmethod.InputMethodManager;
 | 
				
			||||||
 | 
					import android.widget.Button;
 | 
				
			||||||
 | 
					import android.widget.EditText;
 | 
				
			||||||
 | 
					import android.widget.ImageView;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					import android.widget.ProgressBar;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.annotation.Nullable;
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					import androidx.appcompat.widget.Toolbar;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.json.JSONArray;
 | 
				
			||||||
 | 
					import org.json.JSONException;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.InputStream;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.io.OutputStream;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URI;
 | 
				
			||||||
 | 
					import java.net.URISyntaxException;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.util.Random;
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class QrisActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private ProgressBar progressBar;
 | 
				
			||||||
 | 
					    private Button initiatePaymentButton;
 | 
				
			||||||
 | 
					    private TextView statusTextView;
 | 
				
			||||||
 | 
					    private EditText editTextAmount;
 | 
				
			||||||
 | 
					    private TextView referenceIdTextView;
 | 
				
			||||||
 | 
					    private LinearLayout backNavigation;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Numpad buttons
 | 
				
			||||||
 | 
					    private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000, btnDelete;
 | 
				
			||||||
 | 
					    private TextView descriptionText;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String transactionId;
 | 
				
			||||||
 | 
					    private String transactionUuid;
 | 
				
			||||||
 | 
					    private String transactionDate;
 | 
				
			||||||
 | 
					    private String referenceId;
 | 
				
			||||||
 | 
					    private int amount;
 | 
				
			||||||
 | 
					    private JSONObject midtransResponse;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private StringBuilder currentAmount = new StringBuilder();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ✅ FRONTEND DEDUPLICATION: Add SharedPreferences for tracking
 | 
				
			||||||
 | 
					    private SharedPreferences transactionPrefs;
 | 
				
			||||||
 | 
					    private static final String PREF_RECENT_REFERENCES = "recent_references";
 | 
				
			||||||
 | 
					    private static final String PREF_LAST_TRANSACTION_TIME = "last_transaction_time";
 | 
				
			||||||
 | 
					    private static final String PREF_CURRENT_REFERENCE = "current_reference";
 | 
				
			||||||
 | 
					    private static final String PREF_LAST_SUCCESSFUL_TX = "last_successful_tx";
 | 
				
			||||||
 | 
					    private static final long REFERENCE_COOLDOWN_MS = 60000; // 1 minute cooldown
 | 
				
			||||||
 | 
					    private static final long TRANSACTION_COOLDOWN_MS = 5000; // 5 second cooldown
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final String MIDTRANS_AUTH = BuildConfig.MIDTRANS_SANDBOX_AUTH;    
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(@Nullable Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_qris);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ✅ Initialize SharedPreferences for duplicate prevention
 | 
				
			||||||
 | 
					        transactionPrefs = getSharedPreferences("qris_transactions", MODE_PRIVATE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Initialize views
 | 
				
			||||||
 | 
					        progressBar = findViewById(R.id.progressBar);
 | 
				
			||||||
 | 
					        initiatePaymentButton = findViewById(R.id.initiatePaymentButton);
 | 
				
			||||||
 | 
					        statusTextView = findViewById(R.id.statusTextView);
 | 
				
			||||||
 | 
					        editTextAmount = findViewById(R.id.editTextAmount);
 | 
				
			||||||
 | 
					        referenceIdTextView = findViewById(R.id.referenceIdTextView);
 | 
				
			||||||
 | 
					        backNavigation = findViewById(R.id.back_navigation);
 | 
				
			||||||
 | 
					        descriptionText = findViewById(R.id.descriptionText);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Initialize 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);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // ✅ Generate unique reference ID with duplicate prevention
 | 
				
			||||||
 | 
					        referenceId = generateUniqueReferenceId();
 | 
				
			||||||
 | 
					        referenceIdTextView.setText(referenceId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set up click listeners
 | 
				
			||||||
 | 
					        initiatePaymentButton.setOnClickListener(v -> createTransaction());
 | 
				
			||||||
 | 
					        backNavigation.setOnClickListener(v -> finish());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set up numpad listeners
 | 
				
			||||||
 | 
					        setupNumpadListeners();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Initially disable the button
 | 
				
			||||||
 | 
					        initiatePaymentButton.setEnabled(false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String getCurrentDateTime() {
 | 
				
			||||||
 | 
					        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", new java.util.Locale("id", "ID"));
 | 
				
			||||||
 | 
					        return sdf.format(new java.util.Date());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * ✅ FRONTEND DEDUPLICATION: Generate unique reference ID with local tracking
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private String generateUniqueReferenceId() {
 | 
				
			||||||
 | 
					        Log.d("QrisActivity", "🔄 Generating unique reference ID...");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String baseRef = "ref-" + generateRandomString(8);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check if this reference was recently created
 | 
				
			||||||
 | 
					        String recentRefs = transactionPrefs.getString(PREF_RECENT_REFERENCES, "");
 | 
				
			||||||
 | 
					        long currentTime = System.currentTimeMillis();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Clean up old references (older than cooldown period)
 | 
				
			||||||
 | 
					        StringBuilder validRefs = new StringBuilder();
 | 
				
			||||||
 | 
					        if (!recentRefs.isEmpty()) {
 | 
				
			||||||
 | 
					            String[] refs = recentRefs.split(",");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for (String refEntry : refs) {
 | 
				
			||||||
 | 
					                if (refEntry.contains(":")) {
 | 
				
			||||||
 | 
					                    String[] parts = refEntry.split(":");
 | 
				
			||||||
 | 
					                    if (parts.length == 2) {
 | 
				
			||||||
 | 
					                        try {
 | 
				
			||||||
 | 
					                            long timestamp = Long.parseLong(parts[1]);
 | 
				
			||||||
 | 
					                            if (currentTime - timestamp < REFERENCE_COOLDOWN_MS) {
 | 
				
			||||||
 | 
					                                // Reference is still in cooldown period
 | 
				
			||||||
 | 
					                                if (validRefs.length() > 0) validRefs.append(",");
 | 
				
			||||||
 | 
					                                validRefs.append(refEntry);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        } catch (NumberFormatException e) {
 | 
				
			||||||
 | 
					                            // Skip invalid entries
 | 
				
			||||||
 | 
					                            Log.w("QrisActivity", "Invalid reference entry: " + refEntry);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check if baseRef already exists in recent references
 | 
				
			||||||
 | 
					        if (validRefs.length() > 0) {
 | 
				
			||||||
 | 
					            String[] validRefArray = validRefs.toString().split(",");
 | 
				
			||||||
 | 
					            for (String refEntry : validRefArray) {
 | 
				
			||||||
 | 
					                if (refEntry.startsWith(baseRef + ":")) {
 | 
				
			||||||
 | 
					                    // Reference already exists, generate a new one
 | 
				
			||||||
 | 
					                    Log.w("QrisActivity", "⚠️ Reference " + baseRef + " recently used, generating new one");
 | 
				
			||||||
 | 
					                    return generateUniqueReferenceId(); // Recursive call with new random string
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Add this reference to recent references
 | 
				
			||||||
 | 
					        if (validRefs.length() > 0) validRefs.append(",");
 | 
				
			||||||
 | 
					        validRefs.append(baseRef).append(":").append(currentTime);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Save updated references
 | 
				
			||||||
 | 
					        transactionPrefs.edit()
 | 
				
			||||||
 | 
					            .putString(PREF_RECENT_REFERENCES, validRefs.toString())
 | 
				
			||||||
 | 
					            .apply();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d("QrisActivity", "✅ Generated unique reference: " + baseRef);
 | 
				
			||||||
 | 
					        return baseRef;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * ✅ FRONTEND DEDUPLICATION: Check if transaction is currently being processed
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private boolean isTransactionInProgress() {
 | 
				
			||||||
 | 
					        long lastTransactionTime = transactionPrefs.getLong(PREF_LAST_TRANSACTION_TIME, 0);
 | 
				
			||||||
 | 
					        long currentTime = System.currentTimeMillis();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // If last transaction was less than cooldown period, consider it in progress
 | 
				
			||||||
 | 
					        boolean inProgress = (currentTime - lastTransactionTime) < TRANSACTION_COOLDOWN_MS;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (inProgress) {
 | 
				
			||||||
 | 
					            Log.w("QrisActivity", "⏸️ Transaction in progress, cooldown active");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return inProgress;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * ✅ FRONTEND DEDUPLICATION: Mark transaction processing status
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void markTransactionInProgress(boolean inProgress) {
 | 
				
			||||||
 | 
					        SharedPreferences.Editor editor = transactionPrefs.edit();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (inProgress) {
 | 
				
			||||||
 | 
					            editor.putLong(PREF_LAST_TRANSACTION_TIME, System.currentTimeMillis())
 | 
				
			||||||
 | 
					                  .putString(PREF_CURRENT_REFERENCE, referenceId);
 | 
				
			||||||
 | 
					            Log.d("QrisActivity", "🔒 Marked transaction in progress: " + referenceId);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            editor.remove(PREF_CURRENT_REFERENCE);
 | 
				
			||||||
 | 
					            Log.d("QrisActivity", "🔓 Cleared transaction progress status");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        editor.apply();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * ✅ FRONTEND DEDUPLICATION: Save successful transaction for future reference
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void saveSuccessfulTransaction() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            JSONObject txData = new JSONObject();
 | 
				
			||||||
 | 
					            txData.put("reference_id", referenceId);
 | 
				
			||||||
 | 
					            txData.put("transaction_uuid", transactionUuid);
 | 
				
			||||||
 | 
					            txData.put("amount", amount);
 | 
				
			||||||
 | 
					            txData.put("created_at", System.currentTimeMillis());
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Save to SharedPreferences
 | 
				
			||||||
 | 
					            transactionPrefs.edit()
 | 
				
			||||||
 | 
					                .putString(PREF_LAST_SUCCESSFUL_TX, txData.toString())
 | 
				
			||||||
 | 
					                .putLong("last_success_time", System.currentTimeMillis())
 | 
				
			||||||
 | 
					                .apply();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            Log.d("QrisActivity", "💾 Saved successful transaction: " + referenceId);
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.w("QrisActivity", "Failed to save transaction data: " + e.getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * ✅ FRONTEND DEDUPLICATION: Create client info for better backend tracking
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private JSONObject createClientInfo() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            JSONObject clientInfo = new JSONObject();
 | 
				
			||||||
 | 
					            clientInfo.put("app_version", "1.0.0");
 | 
				
			||||||
 | 
					            clientInfo.put("platform", "android");
 | 
				
			||||||
 | 
					            clientInfo.put("timestamp", System.currentTimeMillis());
 | 
				
			||||||
 | 
					            clientInfo.put("session_id", generateRandomString(16));
 | 
				
			||||||
 | 
					            clientInfo.put("reference_generation_time", System.currentTimeMillis());
 | 
				
			||||||
 | 
					            return clientInfo;
 | 
				
			||||||
 | 
					        } catch (JSONException e) {
 | 
				
			||||||
 | 
					            Log.w("QrisActivity", "Failed to create client info: " + e.getMessage());
 | 
				
			||||||
 | 
					            return new JSONObject();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupNumpadListeners() {
 | 
				
			||||||
 | 
					        View.OnClickListener numberClickListener = v -> {
 | 
				
			||||||
 | 
					            TextView button = (TextView) v;
 | 
				
			||||||
 | 
					            String number = button.getText().toString();
 | 
				
			||||||
 | 
					            appendNumber(number);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        btn1.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn2.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn3.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn4.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn5.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn6.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn7.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn8.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn9.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn0.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        btn000.setOnClickListener(numberClickListener);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        btnDelete.setOnClickListener(v -> deleteLastDigit());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void appendNumber(String number) {
 | 
				
			||||||
 | 
					        currentAmount.append(number);
 | 
				
			||||||
 | 
					        updateAmountDisplay();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void deleteLastDigit() {
 | 
				
			||||||
 | 
					        if (currentAmount.length() > 0) {
 | 
				
			||||||
 | 
					            currentAmount.deleteCharAt(currentAmount.length() - 1);
 | 
				
			||||||
 | 
					            updateAmountDisplay();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void updateAmountDisplay() {
 | 
				
			||||||
 | 
					        String amountStr = currentAmount.toString();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (amountStr.isEmpty()) {
 | 
				
			||||||
 | 
					            editTextAmount.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					            descriptionText.setText("Pastikan kembali nominal pembayaran pelanggan Anda");
 | 
				
			||||||
 | 
					            initiatePaymentButton.setEnabled(false);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            editTextAmount.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					            editTextAmount.setText(formatAmount(amountStr));
 | 
				
			||||||
 | 
					            descriptionText.setText("Tekan Konfirmasi untuk melanjutkan");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Enable button if amount is valid (any positive amount)
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                int amt = Integer.parseInt(amountStr);
 | 
				
			||||||
 | 
					                initiatePaymentButton.setEnabled(amt > 0); // Changed from >= 1000 to > 0
 | 
				
			||||||
 | 
					            } catch (NumberFormatException e) {
 | 
				
			||||||
 | 
					                initiatePaymentButton.setEnabled(false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatAmount(String amount) {
 | 
				
			||||||
 | 
					        if (amount.isEmpty()) return "";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            long num = Long.parseLong(amount);
 | 
				
			||||||
 | 
					            return String.format("%,d", num);
 | 
				
			||||||
 | 
					        } catch (NumberFormatException e) {
 | 
				
			||||||
 | 
					            return amount;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * ✅ ENHANCED: Modified createTransaction with comprehensive duplicate prevention
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void createTransaction() {
 | 
				
			||||||
 | 
					        if (currentAmount.length() == 0) {
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Masukkan jumlah pembayaran", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // ✅ FRONTEND CHECK: Prevent rapid duplicate submissions
 | 
				
			||||||
 | 
					        if (isTransactionInProgress()) {
 | 
				
			||||||
 | 
					            Toast.makeText(this, "Transaksi sedang diproses, harap tunggu...", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d("QrisActivity", "🚀 Starting transaction creation process");
 | 
				
			||||||
 | 
					        Log.d("QrisActivity", "   Reference ID: " + referenceId);
 | 
				
			||||||
 | 
					        Log.d("QrisActivity", "   Amount: " + currentAmount.toString());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        progressBar.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        initiatePaymentButton.setEnabled(false);
 | 
				
			||||||
 | 
					        statusTextView.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        statusTextView.setText("Creating transaction...");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Mark transaction as in progress
 | 
				
			||||||
 | 
					        markTransactionInProgress(true);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        new CreateTransactionTask().execute();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String generateRandomString(int length) {
 | 
				
			||||||
 | 
					        String chars = "abcdefghijklmnopqrstuvwxyz0123456789";
 | 
				
			||||||
 | 
					        StringBuilder sb = new StringBuilder();
 | 
				
			||||||
 | 
					        Random random = new Random();
 | 
				
			||||||
 | 
					        for (int i = 0; i < length; i++) {
 | 
				
			||||||
 | 
					            int index = random.nextInt(chars.length());
 | 
				
			||||||
 | 
					            sb.append(chars.charAt(index));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return sb.toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String getServerKey() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // MIDTRANS_AUTH = 'Basic base64string'
 | 
				
			||||||
 | 
					            String base64 = MIDTRANS_AUTH.replace("Basic ", "");
 | 
				
			||||||
 | 
					            byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT);
 | 
				
			||||||
 | 
					            String decodedString = new String(decoded);
 | 
				
			||||||
 | 
					            // Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present.
 | 
				
			||||||
 | 
					            return decodedString.replace(":", "");
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e("MidtransCharge", "Error decoding server key: " + e.getMessage());
 | 
				
			||||||
 | 
					            return "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private boolean isValidServerKey(String serverKey) {
 | 
				
			||||||
 | 
					        return serverKey != null && 
 | 
				
			||||||
 | 
					            (serverKey.startsWith("SB-Mid-server-") ||  // Sandbox format
 | 
				
			||||||
 | 
					                serverKey.startsWith("Mid-server-")) &&    // Production format
 | 
				
			||||||
 | 
					            serverKey.length() > 20;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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("MidtransCharge", "Error generating signature: " + e.getMessage());
 | 
				
			||||||
 | 
					            return "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public boolean onOptionsItemSelected(MenuItem item) {
 | 
				
			||||||
 | 
					        if (item.getItemId() == android.R.id.home) {
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return super.onOptionsItemSelected(item);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private class CreateTransactionTask extends AsyncTask<Void, Void, Boolean> {
 | 
				
			||||||
 | 
					        private String errorMessage;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected Boolean doInBackground(Void... voids) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                // Generate a UUID for the transaction
 | 
				
			||||||
 | 
					                transactionUuid = UUID.randomUUID().toString();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // ✅ ENHANCED LOGGING: Better tracking for debugging
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "=== TRANSACTION CREATION START ===");
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Reference ID: " + referenceId);
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Transaction UUID: " + transactionUuid);
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Timestamp: " + System.currentTimeMillis());
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Create transaction JSON payload
 | 
				
			||||||
 | 
					                JSONObject payload = new JSONObject();
 | 
				
			||||||
 | 
					                payload.put("type", "PAYMENT");
 | 
				
			||||||
 | 
					                payload.put("channel_category", "RETAIL_OUTLET");
 | 
				
			||||||
 | 
					                payload.put("channel_code", "QRIS");
 | 
				
			||||||
 | 
					                payload.put("reference_id", referenceId);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // ✅ FRONTEND ENHANCEMENT: Add client-side metadata for better tracking
 | 
				
			||||||
 | 
					                payload.put("client_info", createClientInfo());
 | 
				
			||||||
 | 
					                payload.put("is_initial_creation", true); // Mark as initial creation
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Get amount from current input
 | 
				
			||||||
 | 
					                String amountText = currentAmount.toString();
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Raw amount text: " + amountText);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    // Parse amount - expecting integer in lowest denomination (Indonesian Rupiah)
 | 
				
			||||||
 | 
					                    amount = Integer.parseInt(amountText);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // // Validate minimum amount
 | 
				
			||||||
 | 
					                    // if (amount < 1000) {
 | 
				
			||||||
 | 
					                    //     errorMessage = "Minimum amount is IDR 1,000";
 | 
				
			||||||
 | 
					                    //     return false;
 | 
				
			||||||
 | 
					                    // }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // // Validate maximum amount for testing
 | 
				
			||||||
 | 
					                    // if (amount > 10000000) {
 | 
				
			||||||
 | 
					                    //     errorMessage = "Maximum amount is IDR 10,000,000";
 | 
				
			||||||
 | 
					                    //     return false;
 | 
				
			||||||
 | 
					                    // }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "Parsed amount: " + amount);
 | 
				
			||||||
 | 
					                } catch (NumberFormatException e) {
 | 
				
			||||||
 | 
					                    Log.e("MidtransCharge", "Amount parsing error: " + e.getMessage());
 | 
				
			||||||
 | 
					                    errorMessage = "Invalid amount format. Please enter numbers only.";
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                payload.put("amount", amount);
 | 
				
			||||||
 | 
					                payload.put("cashflow", "MONEY_IN");
 | 
				
			||||||
 | 
					                payload.put("status", "INIT");
 | 
				
			||||||
 | 
					                payload.put("device_id", 1);
 | 
				
			||||||
 | 
					                payload.put("transaction_uuid", transactionUuid);
 | 
				
			||||||
 | 
					                payload.put("transaction_date", getCurrentDateTime());
 | 
				
			||||||
 | 
					                payload.put("transaction_time_seconds", 0.0);
 | 
				
			||||||
 | 
					                payload.put("device_code", "PB4K252T00021");
 | 
				
			||||||
 | 
					                payload.put("merchant_name", "Marcel Panjaitan");
 | 
				
			||||||
 | 
					                payload.put("mid", "71000026521");
 | 
				
			||||||
 | 
					                payload.put("tid", "73001500");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Backend transaction payload: " + payload.toString());
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Make the API call
 | 
				
			||||||
 | 
					                URL url = new URI(BuildConfig.BACKEND_BASE_URL + "/transactions").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");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // ✅ FRONTEND ENHANCEMENT: Add client headers for better backend tracking
 | 
				
			||||||
 | 
					                conn.setRequestProperty("X-Client-Reference", referenceId);
 | 
				
			||||||
 | 
					                conn.setRequestProperty("X-Client-Timestamp", String.valueOf(System.currentTimeMillis()));
 | 
				
			||||||
 | 
					                conn.setRequestProperty("X-Client-Version", "1.0.0");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                conn.setDoOutput(true);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                try (OutputStream os = conn.getOutputStream()) {
 | 
				
			||||||
 | 
					                    byte[] input = payload.toString().getBytes("utf-8");
 | 
				
			||||||
 | 
					                    os.write(input, 0, input.length);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = conn.getResponseCode();
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Backend response code: " + responseCode);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (responseCode == 200 || responseCode == 201) {
 | 
				
			||||||
 | 
					                    // Success - process response
 | 
				
			||||||
 | 
					                    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("MidtransCharge", "Backend success response: " + response.toString());
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Parse the response to get transaction ID
 | 
				
			||||||
 | 
					                    JSONObject jsonResponse = new JSONObject(response.toString());
 | 
				
			||||||
 | 
					                    JSONObject data = jsonResponse.getJSONObject("data");
 | 
				
			||||||
 | 
					                    transactionId = String.valueOf(data.getInt("id"));
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "✅ Created transaction ID: " + transactionId);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // ✅ FRONTEND SUCCESS: Save successful transaction info
 | 
				
			||||||
 | 
					                    saveSuccessfulTransaction();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Now generate QRIS via Midtrans
 | 
				
			||||||
 | 
					                    return generateQris(amount);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } else if (responseCode == 409 || responseCode == 400) {
 | 
				
			||||||
 | 
					                    // ✅ ENHANCED DUPLICATE HANDLING: Handle gracefully
 | 
				
			||||||
 | 
					                    Log.w("MidtransCharge", "⚠️ Potential duplicate detected (HTTP " + responseCode + ")");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Try to read and parse error response
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        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());
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        String errorResponseStr = errorResponse.toString();
 | 
				
			||||||
 | 
					                        Log.d("MidtransCharge", "Error response: " + errorResponseStr);
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Check if it's actually a duplicate reference error
 | 
				
			||||||
 | 
					                        if (errorResponseStr.toLowerCase().contains("duplicate") || 
 | 
				
			||||||
 | 
					                            errorResponseStr.toLowerCase().contains("already exists") ||
 | 
				
			||||||
 | 
					                            errorResponseStr.toLowerCase().contains("reference") ||
 | 
				
			||||||
 | 
					                            responseCode == 409) {
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            Log.i("MidtransCharge", "✅ Confirmed duplicate reference - proceeding with QRIS generation");
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // For duplicates, we can still generate QRIS with existing reference
 | 
				
			||||||
 | 
					                            return generateQris(amount);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } catch (Exception e) {
 | 
				
			||||||
 | 
					                        Log.w("MidtransCharge", "Could not parse error response: " + e.getMessage());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // If we can't determine the exact error, try QRIS generation anyway
 | 
				
			||||||
 | 
					                    Log.i("MidtransCharge", "🔄 Proceeding with QRIS generation despite backend error");
 | 
				
			||||||
 | 
					                    return generateQris(amount);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // Other HTTP errors
 | 
				
			||||||
 | 
					                    BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String responseLine;
 | 
				
			||||||
 | 
					                    while ((responseLine = br.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(responseLine.trim());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    String errorResponse = response.toString();
 | 
				
			||||||
 | 
					                    Log.e("MidtransCharge", "❌ Backend error (HTTP " + responseCode + "): " + errorResponse);
 | 
				
			||||||
 | 
					                    errorMessage = "Backend error (" + responseCode + "): " + errorResponse;
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e("MidtransCharge", "❌ Backend transaction exception: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                errorMessage = "Network error: " + e.getMessage();
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        private boolean generateQris(int amount) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                // Validate server key first
 | 
				
			||||||
 | 
					                String serverKey = getServerKey();
 | 
				
			||||||
 | 
					                if (!isValidServerKey(serverKey)) {
 | 
				
			||||||
 | 
					                    Log.e("MidtransCharge", "Invalid server key format");
 | 
				
			||||||
 | 
					                    errorMessage = "Invalid server key configuration";
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Using server key: " + serverKey.substring(0, Math.min(20, serverKey.length())) + "...");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Create QRIS charge JSON payload
 | 
				
			||||||
 | 
					                JSONObject payload = new JSONObject();
 | 
				
			||||||
 | 
					                payload.put("payment_type", "qris");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                JSONObject transactionDetails = new JSONObject();
 | 
				
			||||||
 | 
					                transactionDetails.put("order_id", transactionUuid);
 | 
				
			||||||
 | 
					                transactionDetails.put("gross_amount", amount);
 | 
				
			||||||
 | 
					                payload.put("transaction_details", transactionDetails);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Add customer details (recommended for better success rate)
 | 
				
			||||||
 | 
					                JSONObject customerDetails = new JSONObject();
 | 
				
			||||||
 | 
					                customerDetails.put("first_name", "Test");
 | 
				
			||||||
 | 
					                customerDetails.put("last_name", "Customer");
 | 
				
			||||||
 | 
					                customerDetails.put("email", "test@example.com");
 | 
				
			||||||
 | 
					                customerDetails.put("phone", "081234567890");
 | 
				
			||||||
 | 
					                payload.put("customer_details", customerDetails);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Add item details (optional but recommended)
 | 
				
			||||||
 | 
					                JSONArray itemDetails = new JSONArray();
 | 
				
			||||||
 | 
					                JSONObject item = new JSONObject();
 | 
				
			||||||
 | 
					                item.put("id", "item1");
 | 
				
			||||||
 | 
					                item.put("price", amount);
 | 
				
			||||||
 | 
					                item.put("quantity", 1);
 | 
				
			||||||
 | 
					                item.put("name", "QRIS Payment - " + referenceId);
 | 
				
			||||||
 | 
					                itemDetails.put(item);
 | 
				
			||||||
 | 
					                payload.put("item_details", itemDetails);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // ✅ FRONTEND ENHANCEMENT: Add tracking info for reference linkage
 | 
				
			||||||
 | 
					                JSONObject customField1 = new JSONObject();
 | 
				
			||||||
 | 
					                customField1.put("app_reference_id", referenceId);
 | 
				
			||||||
 | 
					                customField1.put("creation_time", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date()));
 | 
				
			||||||
 | 
					                customField1.put("client_version", "1.0.0");
 | 
				
			||||||
 | 
					                payload.put("custom_field1", customField1.toString());
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Log the request details
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "=== MIDTRANS QRIS REQUEST ===");
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "URL: " + BuildConfig.MIDTRANS_CHARGE_URL);
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "X-Override-Notification: " + BuildConfig.WEBHOOK_URL);
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Reference ID: " + referenceId);
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Order ID: " + transactionUuid);
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Amount: " + amount);
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "================================");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Make the API call to Midtrans
 | 
				
			||||||
 | 
					                URL url = new URI(BuildConfig.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", BuildConfig.WEBHOOK_URL);
 | 
				
			||||||
 | 
					                conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
 | 
				
			||||||
 | 
					                conn.setDoOutput(true);
 | 
				
			||||||
 | 
					                conn.setConnectTimeout(30000); // 30 seconds
 | 
				
			||||||
 | 
					                conn.setReadTimeout(30000); // 30 seconds
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                try (OutputStream os = conn.getOutputStream()) {
 | 
				
			||||||
 | 
					                    byte[] input = payload.toString().getBytes("utf-8");
 | 
				
			||||||
 | 
					                    os.write(input, 0, input.length);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = conn.getResponseCode();
 | 
				
			||||||
 | 
					                Log.d("MidtransCharge", "Midtrans HTTP Response Code: " + responseCode);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (responseCode == 200 || responseCode == 201) {
 | 
				
			||||||
 | 
					                    InputStream inputStream = conn.getInputStream();
 | 
				
			||||||
 | 
					                    if (inputStream != null) {
 | 
				
			||||||
 | 
					                        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
 | 
				
			||||||
 | 
					                        StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                        String responseLine;
 | 
				
			||||||
 | 
					                        while ((responseLine = br.readLine()) != null) {
 | 
				
			||||||
 | 
					                            response.append(responseLine.trim());
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        Log.d("MidtransCharge", "Midtrans Success Response: " + response.toString());
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Parse the response
 | 
				
			||||||
 | 
					                        midtransResponse = new JSONObject(response.toString());
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Check if response contains error within success response
 | 
				
			||||||
 | 
					                        if (midtransResponse.has("status_code")) {
 | 
				
			||||||
 | 
					                            String statusCode = midtransResponse.getString("status_code");
 | 
				
			||||||
 | 
					                            if (!statusCode.equals("201")) {
 | 
				
			||||||
 | 
					                                String statusMessage = midtransResponse.optString("status_message", "Unknown error");
 | 
				
			||||||
 | 
					                                Log.e("MidtransCharge", "Midtrans Error in response: " + statusCode + " - " + statusMessage);
 | 
				
			||||||
 | 
					                                errorMessage = "Midtrans Error: " + statusMessage + " (Code: " + statusCode + ")";
 | 
				
			||||||
 | 
					                                return false;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Validate response has required fields
 | 
				
			||||||
 | 
					                        if (!midtransResponse.has("actions") || 
 | 
				
			||||||
 | 
					                            !midtransResponse.has("transaction_id") ||
 | 
				
			||||||
 | 
					                            !midtransResponse.has("gross_amount")) {
 | 
				
			||||||
 | 
					                            Log.e("MidtransCharge", "Missing required fields in Midtrans response");
 | 
				
			||||||
 | 
					                            errorMessage = "Invalid response from Midtrans - missing required fields";
 | 
				
			||||||
 | 
					                            return false;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        Log.d("MidtransCharge", "✅ QRIS generation successful!");
 | 
				
			||||||
 | 
					                        return true;
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        Log.e("MidtransCharge", "HTTP " + responseCode + ": No input stream available");
 | 
				
			||||||
 | 
					                        errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No input stream available";
 | 
				
			||||||
 | 
					                        return false;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    InputStream errorStream = conn.getErrorStream();
 | 
				
			||||||
 | 
					                    if (errorStream != null) {
 | 
				
			||||||
 | 
					                        BufferedReader br = new BufferedReader(new InputStreamReader(errorStream, "utf-8"));
 | 
				
			||||||
 | 
					                        StringBuilder errorResponse = new StringBuilder();
 | 
				
			||||||
 | 
					                        String responseLine;
 | 
				
			||||||
 | 
					                        while ((responseLine = br.readLine()) != null) {
 | 
				
			||||||
 | 
					                            errorResponse.append(responseLine.trim());
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        Log.e("MidtransCharge", "Midtrans HTTP " + responseCode + ": " + errorResponse.toString());
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Try to parse error JSON for better error message
 | 
				
			||||||
 | 
					                        try {
 | 
				
			||||||
 | 
					                            JSONObject errorJson = new JSONObject(errorResponse.toString());
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            // Handle different error response formats
 | 
				
			||||||
 | 
					                            String errorMessage = "";
 | 
				
			||||||
 | 
					                            if (errorJson.has("error_messages")) {
 | 
				
			||||||
 | 
					                                errorMessage = errorJson.optString("error_messages", "Unknown error");
 | 
				
			||||||
 | 
					                            } else if (errorJson.has("status_message")) {
 | 
				
			||||||
 | 
					                                errorMessage = errorJson.optString("status_message", "Unknown error");
 | 
				
			||||||
 | 
					                            } else if (errorJson.has("message")) {
 | 
				
			||||||
 | 
					                                errorMessage = errorJson.optString("message", "Unknown error");
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                errorMessage = errorResponse.toString();
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            this.errorMessage = "Midtrans Error: " + errorMessage;
 | 
				
			||||||
 | 
					                        } catch (JSONException e) {
 | 
				
			||||||
 | 
					                            this.errorMessage = "HTTP " + responseCode + ": " + errorResponse.toString();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        Log.e("MidtransCharge", "HTTP " + responseCode + ": No error stream available");
 | 
				
			||||||
 | 
					                        this.errorMessage = "Error generating QRIS: HTTP " + responseCode + ": No error stream available";
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e("MidtransCharge", "Midtrans QRIS generation exception: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                errorMessage = "Network error: " + e.getMessage();
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected void onPostExecute(Boolean success) {
 | 
				
			||||||
 | 
					            // ✅ FRONTEND CLEANUP: Always clear in-progress status
 | 
				
			||||||
 | 
					            markTransactionInProgress(false);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (success && midtransResponse != null) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    // Extract needed values from midtransResponse
 | 
				
			||||||
 | 
					                    JSONArray actionsArray = midtransResponse.getJSONArray("actions");
 | 
				
			||||||
 | 
					                    if (actionsArray.length() == 0) {
 | 
				
			||||||
 | 
					                        Log.e("MidtransCharge", "No actions found in Midtrans response");
 | 
				
			||||||
 | 
					                        Toast.makeText(QrisActivity.this, "Error: No QR code URL found in response", Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        initiatePaymentButton.setEnabled(true);
 | 
				
			||||||
 | 
					                        progressBar.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					                        statusTextView.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    JSONObject actions = actionsArray.getJSONObject(0);
 | 
				
			||||||
 | 
					                    String qrImageUrl = actions.getString("url");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Extract transaction_id
 | 
				
			||||||
 | 
					                    String transactionId = midtransResponse.getString("transaction_id");
 | 
				
			||||||
 | 
					                    String transactionTime = midtransResponse.getString("transaction_time");
 | 
				
			||||||
 | 
					                    String acquirer = midtransResponse.getString("acquirer");
 | 
				
			||||||
 | 
					                    String merchantId = midtransResponse.getString("merchant_id");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Send raw amount as string without decimal conversion
 | 
				
			||||||
 | 
					                    String rawAmountString = String.valueOf(amount); // Keep original integer amount
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Log everything before launching activity
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "=== LAUNCHING QRIS RESULT ACTIVITY ===");
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "✅ Transaction created successfully!");
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "qrImageUrl: " + qrImageUrl);
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "amount (raw): " + amount);
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "rawAmountString: " + rawAmountString);
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "referenceId: " + referenceId);
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "transactionUuid (orderId): " + transactionUuid);
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "transaction_id: " + transactionId);
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "transactionTime: " + transactionTime);
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "acquirer: " + acquirer);
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "merchantId: " + merchantId);
 | 
				
			||||||
 | 
					                    Log.d("MidtransCharge", "========================================");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // ✅ FINAL SUCCESS: Update transaction status in preferences
 | 
				
			||||||
 | 
					                    transactionPrefs.edit()
 | 
				
			||||||
 | 
					                        .putString("last_qris_url", qrImageUrl)
 | 
				
			||||||
 | 
					                        .putString("last_qris_reference", referenceId)
 | 
				
			||||||
 | 
					                        .putLong("last_qris_time", System.currentTimeMillis())
 | 
				
			||||||
 | 
					                        .apply();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Launch QrisResultActivity
 | 
				
			||||||
 | 
					                    Intent intent = new Intent(QrisActivity.this, QrisResultActivity.class);
 | 
				
			||||||
 | 
					                    intent.putExtra("qrImageUrl", qrImageUrl);
 | 
				
			||||||
 | 
					                    intent.putExtra("amount", amount); // Keep as int
 | 
				
			||||||
 | 
					                    intent.putExtra("referenceId", referenceId);
 | 
				
			||||||
 | 
					                    intent.putExtra("orderId", transactionUuid); // Order ID
 | 
				
			||||||
 | 
					                    intent.putExtra("transactionId", transactionId); // Actual Midtrans transaction_id
 | 
				
			||||||
 | 
					                    intent.putExtra("grossAmount", rawAmountString); // Raw amount as string (no decimals)
 | 
				
			||||||
 | 
					                    intent.putExtra("transactionTime", transactionTime); // For timestamp
 | 
				
			||||||
 | 
					                    intent.putExtra("acquirer", acquirer);
 | 
				
			||||||
 | 
					                    intent.putExtra("merchantId", merchantId);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        startActivity(intent);
 | 
				
			||||||
 | 
					                        finish(); // Close QrisActivity
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        Log.d("MidtransCharge", "🎉 Successfully launched QrisResultActivity");
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                    } catch (Exception e) {
 | 
				
			||||||
 | 
					                        Log.e("MidtransCharge", "Failed to start QrisResultActivity: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                        Toast.makeText(QrisActivity.this, "Error launching QR display: " + e.getMessage(), Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        // Re-enable button on error
 | 
				
			||||||
 | 
					                        initiatePaymentButton.setEnabled(true);
 | 
				
			||||||
 | 
					                        progressBar.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					                        statusTextView.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } catch (JSONException e) {
 | 
				
			||||||
 | 
					                    Log.e("MidtransCharge", "QRIS response JSON error: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                    Toast.makeText(QrisActivity.this, "Error processing QRIS response: " + e.getMessage(), Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Handle error case
 | 
				
			||||||
 | 
					                String message = (errorMessage != null && !errorMessage.isEmpty()) ? 
 | 
				
			||||||
 | 
					                    errorMessage : "Unknown error occurred. Please check your connection and try again.";
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                Log.e("MidtransCharge", "❌ Transaction failed: " + message);
 | 
				
			||||||
 | 
					                Toast.makeText(QrisActivity.this, message, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Re-enable button for retry
 | 
				
			||||||
 | 
					                initiatePaymentButton.setEnabled(true);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Always hide progress indicators
 | 
				
			||||||
 | 
					            progressBar.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					            statusTextView.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onDestroy() {
 | 
				
			||||||
 | 
					        super.onDestroy();
 | 
				
			||||||
 | 
					        // ✅ CLEANUP: Clear any in-progress status when activity is destroyed
 | 
				
			||||||
 | 
					        markTransactionInProgress(false);
 | 
				
			||||||
 | 
					        Log.d("QrisActivity", "🧹 QrisActivity destroyed, cleared progress status");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onPause() {
 | 
				
			||||||
 | 
					        super.onPause();
 | 
				
			||||||
 | 
					        // Keep progress status when paused (user might come back)
 | 
				
			||||||
 | 
					        Log.d("QrisActivity", "⏸️ QrisActivity paused");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onResume() {
 | 
				
			||||||
 | 
					        super.onResume();
 | 
				
			||||||
 | 
					        Log.d("QrisActivity", "▶️ QrisActivity resumed");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Check if there's a recent successful transaction
 | 
				
			||||||
 | 
					        String lastSuccessfulTx = transactionPrefs.getString(PREF_LAST_SUCCESSFUL_TX, "");
 | 
				
			||||||
 | 
					        if (!lastSuccessfulTx.isEmpty()) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                JSONObject txData = new JSONObject(lastSuccessfulTx);
 | 
				
			||||||
 | 
					                String lastRef = txData.getString("reference_id");
 | 
				
			||||||
 | 
					                long lastTime = txData.getLong("created_at");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // If last successful transaction was recent (within 5 minutes) and same reference
 | 
				
			||||||
 | 
					                if (System.currentTimeMillis() - lastTime < 300000 && lastRef.equals(referenceId)) {
 | 
				
			||||||
 | 
					                    Log.d("QrisActivity", "🔄 Recent successful transaction detected for same reference");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.w("QrisActivity", "Could not parse last successful transaction: " + e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onBackPressed() {
 | 
				
			||||||
 | 
					        // ✅ CLEANUP: Clear progress status when user goes back
 | 
				
			||||||
 | 
					        markTransactionInProgress(false);
 | 
				
			||||||
 | 
					        super.onBackPressed();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,187 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.qris.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.network.QrisApiService;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Repository class untuk menghandle semua data access terkait QRIS
 | 
				
			||||||
 | 
					 * Mengabstraksi sumber data (API, local storage, etc.)
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class QrisRepository {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private static final String TAG = "QrisRepository";
 | 
				
			||||||
 | 
					    private QrisApiService apiService;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Singleton pattern
 | 
				
			||||||
 | 
					    private static QrisRepository instance;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private QrisRepository() {
 | 
				
			||||||
 | 
					        this.apiService = new QrisApiService();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public static QrisRepository getInstance() {
 | 
				
			||||||
 | 
					        if (instance == null) {
 | 
				
			||||||
 | 
					            instance = new QrisRepository();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return instance;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Interface untuk callback hasil operasi
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public interface RepositoryCallback<T> {
 | 
				
			||||||
 | 
					        void onSuccess(T result);
 | 
				
			||||||
 | 
					        void onError(String errorMessage);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Refresh QR Code
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void refreshQrCode(QrisTransaction transaction, RepositoryCallback<QrRefreshResult> callback) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔄 Refreshing QR code for transaction: " + transaction.getOrderId());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        new Thread(() -> {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                QrRefreshResult result = apiService.generateNewQrCode(transaction);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (result != null && result.qrUrl != null && !result.qrUrl.isEmpty()) {
 | 
				
			||||||
 | 
					                    Log.d(TAG, "✅ QR refresh successful");
 | 
				
			||||||
 | 
					                    callback.onSuccess(result);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Log.e(TAG, "❌ QR refresh failed - empty result");
 | 
				
			||||||
 | 
					                    callback.onError("Failed to generate new QR code");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "❌ QR refresh exception: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                callback.onError("QR refresh error: " + e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).start();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check payment status
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void checkPaymentStatus(QrisTransaction transaction, RepositoryCallback<PaymentStatusResult> callback) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔍 Checking payment status for: " + transaction.getCurrentQrTransactionId());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        new Thread(() -> {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                // Gunakan current transaction ID, bukan original
 | 
				
			||||||
 | 
					                PaymentStatusResult result = apiService.checkTransactionStatus(transaction);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (result != null) {
 | 
				
			||||||
 | 
					                    Log.d(TAG, "✅ Payment status check successful: " + result.status);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Update transaction ID jika berbeda
 | 
				
			||||||
 | 
					                    if (result.transactionId != null && 
 | 
				
			||||||
 | 
					                        !result.transactionId.equals(transaction.getCurrentQrTransactionId())) {
 | 
				
			||||||
 | 
					                        transaction.setCurrentQrTransactionId(result.transactionId);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    callback.onSuccess(result);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Log.w(TAG, "⚠️ Payment status check returned null");
 | 
				
			||||||
 | 
					                    callback.onError("Failed to check payment status");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "❌ Payment status check exception: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                callback.onError("Payment status error: " + e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).start();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Send webhook simulation
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void simulatePayment(QrisTransaction transaction, RepositoryCallback<Boolean> callback) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🚀 Simulating payment for: " + transaction.getOrderId());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        new Thread(() -> {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                boolean success = apiService.simulateWebhook(transaction);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (success) {
 | 
				
			||||||
 | 
					                    Log.d(TAG, "✅ Payment simulation successful");
 | 
				
			||||||
 | 
					                    callback.onSuccess(true);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Log.e(TAG, "❌ Payment simulation failed");
 | 
				
			||||||
 | 
					                    callback.onError("Payment simulation failed");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "❌ Payment simulation exception: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                callback.onError("Payment simulation error: " + e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).start();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Poll for payment logs
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void pollPaymentLogs(String orderId, RepositoryCallback<PaymentLogResult> callback) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "📊 Polling payment logs for: " + orderId);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        new Thread(() -> {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                PaymentLogResult result = apiService.pollPendingPaymentLog(orderId);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (result != null) {
 | 
				
			||||||
 | 
					                    Log.d(TAG, "✅ Payment log polling successful");
 | 
				
			||||||
 | 
					                    callback.onSuccess(result);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Log.w(TAG, "⚠️ No payment logs found");
 | 
				
			||||||
 | 
					                    callback.onError("No payment logs found");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "❌ Payment log polling exception: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                callback.onError("Payment log polling error: " + e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }).start();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Result classes
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static class QrRefreshResult {
 | 
				
			||||||
 | 
					        public String qrUrl;
 | 
				
			||||||
 | 
					        public String qrString;
 | 
				
			||||||
 | 
					        public String transactionId;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public QrRefreshResult(String qrUrl, String qrString, String transactionId) {
 | 
				
			||||||
 | 
					            this.qrUrl = qrUrl;
 | 
				
			||||||
 | 
					            this.qrString = qrString;
 | 
				
			||||||
 | 
					            this.transactionId = transactionId;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public static class PaymentStatusResult {
 | 
				
			||||||
 | 
					        public String status;
 | 
				
			||||||
 | 
					        public String paymentType;
 | 
				
			||||||
 | 
					        public String issuer;
 | 
				
			||||||
 | 
					        public String acquirer;
 | 
				
			||||||
 | 
					        public String qrString;
 | 
				
			||||||
 | 
					        public boolean statusChanged;
 | 
				
			||||||
 | 
					        public String transactionId;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public PaymentStatusResult(String status) {
 | 
				
			||||||
 | 
					            this.status = status;
 | 
				
			||||||
 | 
					            this.statusChanged = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public static class PaymentLogResult {
 | 
				
			||||||
 | 
					        public boolean found;
 | 
				
			||||||
 | 
					        public String status;
 | 
				
			||||||
 | 
					        public String orderId;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public PaymentLogResult(boolean found, String status, String orderId) {
 | 
				
			||||||
 | 
					            this.found = found;
 | 
				
			||||||
 | 
					            this.status = status;
 | 
				
			||||||
 | 
					            this.orderId = orderId;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,289 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.qris.model;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.BuildConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Model class untuk data transaksi QRIS
 | 
				
			||||||
 | 
					 * Menampung semua data yang dibutuhkan untuk transaksi
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class QrisTransaction {
 | 
				
			||||||
 | 
					    private static final String TAG = "QrisTransaction";
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Transaction identifiers
 | 
				
			||||||
 | 
					    private String orderId;
 | 
				
			||||||
 | 
					    private String transactionId;
 | 
				
			||||||
 | 
					    private String referenceId;
 | 
				
			||||||
 | 
					    private String merchantId;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Amount information
 | 
				
			||||||
 | 
					    private int originalAmount;
 | 
				
			||||||
 | 
					    private String grossAmount;
 | 
				
			||||||
 | 
					    private String formattedAmount;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // QR Code information
 | 
				
			||||||
 | 
					    private String qrImageUrl;
 | 
				
			||||||
 | 
					    private String qrString;
 | 
				
			||||||
 | 
					    private long qrCreationTime;
 | 
				
			||||||
 | 
					    private int qrExpirationMinutes;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Provider information
 | 
				
			||||||
 | 
					    private String acquirer;
 | 
				
			||||||
 | 
					    private String detectedProvider;
 | 
				
			||||||
 | 
					    private String actualIssuer;
 | 
				
			||||||
 | 
					    private String actualAcquirer;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Transaction timing
 | 
				
			||||||
 | 
					    private String transactionTime;
 | 
				
			||||||
 | 
					    private long creationTimestamp;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Status tracking
 | 
				
			||||||
 | 
					    private String currentStatus;
 | 
				
			||||||
 | 
					    private boolean paymentProcessed;
 | 
				
			||||||
 | 
					    private boolean isQrRefreshTransaction;
 | 
				
			||||||
 | 
					    private String currentQrTransactionId;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Provider expiration mapping
 | 
				
			||||||
 | 
					    private static final Map<String, Integer> PROVIDER_EXPIRATION_MAP = new HashMap<String, Integer>() {{
 | 
				
			||||||
 | 
					        put("shopeepay", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("shopee", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("airpay shopee", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("gopay", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("dana", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("ovo", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("linkaja", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("link aja", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("jenius", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("qris", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					        put("others", BuildConfig.DEFAULT_QR_EXPIRATION_MINUTES);
 | 
				
			||||||
 | 
					    }};
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Provider display name mapping
 | 
				
			||||||
 | 
					    private static final Map<String, String> ISSUER_DISPLAY_MAP = new HashMap<String, String>() {{
 | 
				
			||||||
 | 
					        put("airpay shopee", "ShopeePay");
 | 
				
			||||||
 | 
					        put("shopeepay", "ShopeePay");
 | 
				
			||||||
 | 
					        put("shopee", "ShopeePay");
 | 
				
			||||||
 | 
					        put("linkaja", "LinkAja");
 | 
				
			||||||
 | 
					        put("link aja", "LinkAja");
 | 
				
			||||||
 | 
					        put("dana", "DANA");
 | 
				
			||||||
 | 
					        put("ovo", "OVO");
 | 
				
			||||||
 | 
					        put("gopay", "GoPay");
 | 
				
			||||||
 | 
					        put("jenius", "Jenius");
 | 
				
			||||||
 | 
					        put("sakuku", "Sakuku");
 | 
				
			||||||
 | 
					        put("bni", "BNI");
 | 
				
			||||||
 | 
					        put("bca", "BCA");
 | 
				
			||||||
 | 
					        put("mandiri", "Mandiri");
 | 
				
			||||||
 | 
					        put("bri", "BRI");
 | 
				
			||||||
 | 
					        put("cimb", "CIMB Niaga");
 | 
				
			||||||
 | 
					        put("permata", "Permata");
 | 
				
			||||||
 | 
					        put("maybank", "Maybank");
 | 
				
			||||||
 | 
					        put("qris", "QRIS");
 | 
				
			||||||
 | 
					    }};
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Constructor
 | 
				
			||||||
 | 
					    public QrisTransaction() {
 | 
				
			||||||
 | 
					        this.creationTimestamp = System.currentTimeMillis();
 | 
				
			||||||
 | 
					        this.currentStatus = "pending";
 | 
				
			||||||
 | 
					        this.paymentProcessed = false;
 | 
				
			||||||
 | 
					        this.isQrRefreshTransaction = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Initialization method
 | 
				
			||||||
 | 
					    public void initialize(String orderId, String transactionId, int amount, 
 | 
				
			||||||
 | 
					                          String qrImageUrl, String qrString, String acquirer) {
 | 
				
			||||||
 | 
					        this.orderId = orderId;
 | 
				
			||||||
 | 
					        this.transactionId = transactionId;
 | 
				
			||||||
 | 
					        this.currentQrTransactionId = transactionId;
 | 
				
			||||||
 | 
					        this.originalAmount = amount;
 | 
				
			||||||
 | 
					        this.qrImageUrl = qrImageUrl;
 | 
				
			||||||
 | 
					        this.qrString = qrString;
 | 
				
			||||||
 | 
					        this.acquirer = acquirer;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Detect provider and set expiration
 | 
				
			||||||
 | 
					        this.detectedProvider = detectProviderFromData();
 | 
				
			||||||
 | 
					        this.qrExpirationMinutes = PROVIDER_EXPIRATION_MAP.get(detectedProvider.toLowerCase());
 | 
				
			||||||
 | 
					        this.qrCreationTime = System.currentTimeMillis();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Format amount
 | 
				
			||||||
 | 
					        this.formattedAmount = formatRupiahAmount(String.valueOf(amount));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Detect provider dari acquirer atau QR string
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private String detectProviderFromData() {
 | 
				
			||||||
 | 
					        // Try to detect from acquirer first
 | 
				
			||||||
 | 
					        if (acquirer != null && !acquirer.isEmpty()) {
 | 
				
			||||||
 | 
					            String lowerAcquirer = acquirer.toLowerCase().trim();
 | 
				
			||||||
 | 
					            if (PROVIDER_EXPIRATION_MAP.containsKey(lowerAcquirer)) {
 | 
				
			||||||
 | 
					                return lowerAcquirer;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Try to detect from QR string content
 | 
				
			||||||
 | 
					        if (qrString != null && !qrString.isEmpty()) {
 | 
				
			||||||
 | 
					            String lowerQrString = qrString.toLowerCase();
 | 
				
			||||||
 | 
					            for (String provider : PROVIDER_EXPIRATION_MAP.keySet()) {
 | 
				
			||||||
 | 
					                if (lowerQrString.contains(provider.toLowerCase())) {
 | 
				
			||||||
 | 
					                    return provider;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return "others";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Format amount ke format Rupiah
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private String formatRupiahAmount(String amount) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            String cleanAmount = amount.replaceAll("[^0-9]", "");
 | 
				
			||||||
 | 
					            long amountLong = Long.parseLong(cleanAmount);
 | 
				
			||||||
 | 
					            return "RP." + String.format("%,d", amountLong).replace(',', '.');
 | 
				
			||||||
 | 
					        } catch (NumberFormatException e) {
 | 
				
			||||||
 | 
					            return "RP." + amount;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check apakah QR sudah expired
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean isQrExpired() {
 | 
				
			||||||
 | 
					        long currentTime = System.currentTimeMillis();
 | 
				
			||||||
 | 
					        long elapsedMinutes = (currentTime - qrCreationTime) / (1000 * 60);
 | 
				
			||||||
 | 
					        boolean expired = elapsedMinutes >= qrExpirationMinutes;
 | 
				
			||||||
 | 
					        Log.d(TAG, "QR expired check: " + elapsedMinutes + "/" + qrExpirationMinutes + " = " + expired);
 | 
				
			||||||
 | 
					        return expired;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get remaining time dalam detik
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public int getRemainingTimeInSeconds() {
 | 
				
			||||||
 | 
					        long currentTime = System.currentTimeMillis();
 | 
				
			||||||
 | 
					        long elapsedMs = currentTime - qrCreationTime;
 | 
				
			||||||
 | 
					        long totalExpirationMs = qrExpirationMinutes * 60 * 1000;
 | 
				
			||||||
 | 
					        long remainingMs = totalExpirationMs - elapsedMs;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return (int) Math.max(0, remainingMs / 1000);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update QR code dengan data baru
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void updateQrCode(String newQrUrl, String newQrString, String newTransactionId) {
 | 
				
			||||||
 | 
					        this.qrImageUrl = newQrUrl;
 | 
				
			||||||
 | 
					        this.qrString = newQrString;
 | 
				
			||||||
 | 
					        this.qrCreationTime = System.currentTimeMillis();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (newTransactionId != null && !newTransactionId.isEmpty()) {
 | 
				
			||||||
 | 
					            this.currentQrTransactionId = newTransactionId;
 | 
				
			||||||
 | 
					            this.isQrRefreshTransaction = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get display name untuk provider
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public String getDisplayProviderName() {
 | 
				
			||||||
 | 
					        String issuerToCheck = actualIssuer != null && !actualIssuer.isEmpty() 
 | 
				
			||||||
 | 
					            ? actualIssuer : acquirer;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        if (issuerToCheck == null || issuerToCheck.isEmpty()) {
 | 
				
			||||||
 | 
					            return "QRIS";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String lowerName = issuerToCheck.toLowerCase().trim();
 | 
				
			||||||
 | 
					        String displayName = ISSUER_DISPLAY_MAP.get(lowerName);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (displayName != null) {
 | 
				
			||||||
 | 
					            return displayName;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Fallback: capitalize first letter
 | 
				
			||||||
 | 
					        String[] words = issuerToCheck.split("\\s+");
 | 
				
			||||||
 | 
					        StringBuilder result = new StringBuilder();
 | 
				
			||||||
 | 
					        for (String word : words) {
 | 
				
			||||||
 | 
					            if (word.length() > 0) {
 | 
				
			||||||
 | 
					                result.append(Character.toUpperCase(word.charAt(0)))
 | 
				
			||||||
 | 
					                      .append(word.substring(1).toLowerCase())
 | 
				
			||||||
 | 
					                      .append(" ");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return result.toString().trim();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Getters and Setters
 | 
				
			||||||
 | 
					    public String getOrderId() { return orderId; }
 | 
				
			||||||
 | 
					    public void setOrderId(String orderId) { this.orderId = orderId; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getTransactionId() { return transactionId; }
 | 
				
			||||||
 | 
					    public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getCurrentQrTransactionId() { return currentQrTransactionId; }
 | 
				
			||||||
 | 
					    public void setCurrentQrTransactionId(String currentQrTransactionId) { 
 | 
				
			||||||
 | 
					        this.currentQrTransactionId = currentQrTransactionId; 
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getReferenceId() { return referenceId; }
 | 
				
			||||||
 | 
					    public void setReferenceId(String referenceId) { this.referenceId = referenceId; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getMerchantId() { return merchantId; }
 | 
				
			||||||
 | 
					    public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public int getOriginalAmount() { return originalAmount; }
 | 
				
			||||||
 | 
					    public void setOriginalAmount(int originalAmount) { this.originalAmount = originalAmount; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getGrossAmount() { return grossAmount; }
 | 
				
			||||||
 | 
					    public void setGrossAmount(String grossAmount) { this.grossAmount = grossAmount; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getFormattedAmount() { return formattedAmount; }
 | 
				
			||||||
 | 
					    public void setFormattedAmount(String formattedAmount) { this.formattedAmount = formattedAmount; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getQrImageUrl() { return qrImageUrl; }
 | 
				
			||||||
 | 
					    public void setQrImageUrl(String qrImageUrl) { this.qrImageUrl = qrImageUrl; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getQrString() { return qrString; }
 | 
				
			||||||
 | 
					    public void setQrString(String qrString) { this.qrString = qrString; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public long getQrCreationTime() { return qrCreationTime; }
 | 
				
			||||||
 | 
					    public void setQrCreationTime(long qrCreationTime) { this.qrCreationTime = qrCreationTime; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public int getQrExpirationMinutes() { return qrExpirationMinutes; }
 | 
				
			||||||
 | 
					    public void setQrExpirationMinutes(int qrExpirationMinutes) { 
 | 
				
			||||||
 | 
					        this.qrExpirationMinutes = qrExpirationMinutes; 
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getAcquirer() { return acquirer; }
 | 
				
			||||||
 | 
					    public void setAcquirer(String acquirer) { this.acquirer = acquirer; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getDetectedProvider() { return detectedProvider; }
 | 
				
			||||||
 | 
					    public void setDetectedProvider(String detectedProvider) { this.detectedProvider = detectedProvider; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getActualIssuer() { return actualIssuer; }
 | 
				
			||||||
 | 
					    public void setActualIssuer(String actualIssuer) { this.actualIssuer = actualIssuer; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getActualAcquirer() { return actualAcquirer; }
 | 
				
			||||||
 | 
					    public void setActualAcquirer(String actualAcquirer) { this.actualAcquirer = actualAcquirer; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getTransactionTime() { return transactionTime; }
 | 
				
			||||||
 | 
					    public void setTransactionTime(String transactionTime) { this.transactionTime = transactionTime; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public long getCreationTimestamp() { return creationTimestamp; }
 | 
				
			||||||
 | 
					    public void setCreationTimestamp(long creationTimestamp) { this.creationTimestamp = creationTimestamp; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getCurrentStatus() { return currentStatus; }
 | 
				
			||||||
 | 
					    public void setCurrentStatus(String currentStatus) { this.currentStatus = currentStatus; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public boolean isPaymentProcessed() { return paymentProcessed; }
 | 
				
			||||||
 | 
					    public void setPaymentProcessed(boolean paymentProcessed) { this.paymentProcessed = paymentProcessed; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public boolean isQrRefreshTransaction() { return isQrRefreshTransaction; }
 | 
				
			||||||
 | 
					    public void setQrRefreshTransaction(boolean qrRefreshTransaction) { 
 | 
				
			||||||
 | 
					        this.isQrRefreshTransaction = qrRefreshTransaction; 
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,411 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.qris.network;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.BuildConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.model.QrisRepository;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.model.QrisTransaction;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.model.QrisRepository.PaymentStatusResult;
 | 
				
			||||||
 | 
					import org.json.JSONArray;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.InputStream;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.io.OutputStream;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URI;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.security.MessageDigest;
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat;
 | 
				
			||||||
 | 
					import java.util.Date;
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * API Service untuk handling semua network calls terkait QRIS
 | 
				
			||||||
 | 
					 * Mengabstraksi implementasi detail dari repository
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class QrisApiService {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private static final String TAG = "QrisApiService";
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // API Endpoints
 | 
				
			||||||
 | 
					    private static final String MIDTRANS_SANDBOX_AUTH = BuildConfig.MIDTRANS_SANDBOX_AUTH;
 | 
				
			||||||
 | 
					    private static final String MIDTRANS_PRODUCTION_AUTH = BuildConfig.MIDTRANS_PRODUCTION_AUTH;
 | 
				
			||||||
 | 
					    private static final String MIDTRANS_AUTH = BuildConfig.MIDTRANS_SANDBOX_AUTH;
 | 
				
			||||||
 | 
					    private static final String MIDTRANS_CHARGE_URL = BuildConfig.MIDTRANS_CHARGE_URL;
 | 
				
			||||||
 | 
					    private static final String MIDTRANS_STATUS_BASE_URL = BuildConfig.MIDTRANS_STATUS_BASE_URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String backendBase = BuildConfig.BACKEND_BASE_URL;
 | 
				
			||||||
 | 
					    private String webhookUrl = BuildConfig.WEBHOOK_URL;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Generate new QR code via Midtrans API
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public QrisRepository.QrRefreshResult generateNewQrCode(QrisTransaction transaction) throws Exception {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔧 Generating new QR code for: " + transaction.getOrderId());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Generate unique order ID untuk QR refresh
 | 
				
			||||||
 | 
					        String shortTimestamp = String.valueOf(System.currentTimeMillis()).substring(7);
 | 
				
			||||||
 | 
					        String newOrderId = transaction.getOrderId().substring(0, Math.min(transaction.getOrderId().length(), 43)) + "-q" + shortTimestamp;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Validate order ID length
 | 
				
			||||||
 | 
					        if (newOrderId.length() > 50) {
 | 
				
			||||||
 | 
					            newOrderId = transaction.getOrderId().substring(0, 36) + "-q" + shortTimestamp.substring(0, Math.min(shortTimestamp.length(), 7));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "🆕 New QR Order ID: " + newOrderId);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Create enhanced payload
 | 
				
			||||||
 | 
					        JSONObject payload = createQrRefreshPayload(transaction, newOrderId);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Make API call
 | 
				
			||||||
 | 
					        URL url = new URI(MIDTRANS_CHARGE_URL).toURL();
 | 
				
			||||||
 | 
					        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					        conn.setRequestMethod("POST");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("Accept", "application/json");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("Content-Type", "application/json");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
 | 
				
			||||||
 | 
					        conn.setRequestProperty("X-Override-Notification", webhookUrl);
 | 
				
			||||||
 | 
					        conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0 QR-Refresh-Enhanced");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("X-QR-Refresh", "true");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("X-Parent-Transaction", transaction.getTransactionId());
 | 
				
			||||||
 | 
					        conn.setRequestProperty("X-Provider", transaction.getDetectedProvider());
 | 
				
			||||||
 | 
					        conn.setRequestProperty("X-Expiration-Minutes", String.valueOf(transaction.getQrExpirationMinutes()));
 | 
				
			||||||
 | 
					        conn.setDoOutput(true);
 | 
				
			||||||
 | 
					        conn.setConnectTimeout(30000);
 | 
				
			||||||
 | 
					        conn.setReadTimeout(30000);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try (OutputStream os = conn.getOutputStream()) {
 | 
				
			||||||
 | 
					            byte[] input = payload.toString().getBytes("utf-8");
 | 
				
			||||||
 | 
					            os.write(input, 0, input.length);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        int responseCode = conn.getResponseCode();
 | 
				
			||||||
 | 
					        Log.d(TAG, "📥 QR refresh response code: " + responseCode);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (responseCode == 200 || responseCode == 201) {
 | 
				
			||||||
 | 
					            String response = readResponse(conn.getInputStream());
 | 
				
			||||||
 | 
					            return parseQrRefreshResponse(response);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            String errorResponse = readResponse(conn.getErrorStream());
 | 
				
			||||||
 | 
					            throw new Exception("QR refresh failed: HTTP " + responseCode + " - " + errorResponse);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check transaction status via Midtrans API
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public PaymentStatusResult checkTransactionStatus(QrisTransaction transaction) throws Exception {
 | 
				
			||||||
 | 
					        String monitoringTransactionId = transaction.getCurrentQrTransactionId();
 | 
				
			||||||
 | 
					        String statusUrl = MIDTRANS_STATUS_BASE_URL + monitoringTransactionId + "/status";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔍 Checking status for: " + monitoringTransactionId);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        URL url = new URI(statusUrl).toURL();
 | 
				
			||||||
 | 
					        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					        conn.setRequestMethod("GET");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("Accept", "application/json");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
 | 
				
			||||||
 | 
					        conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
 | 
				
			||||||
 | 
					        conn.setConnectTimeout(8000);
 | 
				
			||||||
 | 
					        conn.setReadTimeout(8000);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        int responseCode = conn.getResponseCode();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (responseCode == 200) {
 | 
				
			||||||
 | 
					            String response = readResponse(conn.getInputStream());
 | 
				
			||||||
 | 
					            PaymentStatusResult result = parseStatusResponse(response, transaction);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            JSONObject statusResponse = new JSONObject(response);
 | 
				
			||||||
 | 
					            result.transactionId = statusResponse.optString("transaction_id", monitoringTransactionId);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return result;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            throw new Exception("Status check failed: HTTP " + responseCode);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Simulate webhook payment
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public boolean simulateWebhook(QrisTransaction transaction) throws Exception {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🚀 Simulating webhook for: " + transaction.getOrderId());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String serverKey = getServerKey();
 | 
				
			||||||
 | 
					        String signatureKey = generateSignature(
 | 
				
			||||||
 | 
					            transaction.getOrderId(), 
 | 
				
			||||||
 | 
					            "200", 
 | 
				
			||||||
 | 
					            transaction.getGrossAmount() != null ? transaction.getGrossAmount() : String.valueOf(transaction.getOriginalAmount()), 
 | 
				
			||||||
 | 
					            serverKey
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        JSONObject payload = createWebhookPayload(transaction, signatureKey);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        URL url = new URL(webhookUrl);
 | 
				
			||||||
 | 
					        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					        conn.setRequestMethod("POST");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("Content-Type", "application/json");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0-Enhanced-Simulation");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("X-Simulation", "true");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("X-Provider", transaction.getDetectedProvider());
 | 
				
			||||||
 | 
					        conn.setDoOutput(true);
 | 
				
			||||||
 | 
					        conn.setConnectTimeout(1000);
 | 
				
			||||||
 | 
					        conn.setReadTimeout(1000);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try (OutputStream os = conn.getOutputStream()) {
 | 
				
			||||||
 | 
					            os.write(payload.toString().getBytes("utf-8"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        int responseCode = conn.getResponseCode();
 | 
				
			||||||
 | 
					        Log.d(TAG, "📥 Webhook simulation response: " + responseCode);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (responseCode == 200 || responseCode == 201) {
 | 
				
			||||||
 | 
					            String response = readResponse(conn.getInputStream());
 | 
				
			||||||
 | 
					            Log.d(TAG, "✅ Webhook simulation successful");
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            String errorResponse = readResponse(conn.getErrorStream());
 | 
				
			||||||
 | 
					            throw new Exception("Webhook simulation failed: HTTP " + responseCode + " - " + errorResponse);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Poll for pending payment logs
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public QrisRepository.PaymentLogResult pollPendingPaymentLog(String orderId) throws Exception {
 | 
				
			||||||
 | 
					        Log.d(TAG, "📊 Polling payment logs for: " + orderId);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String urlStr = backendBase + "/api-logs?request_body_search_strict={\"order_id\":\"" + orderId + "\"}";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        URL url = new URL(urlStr);
 | 
				
			||||||
 | 
					        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					        conn.setRequestMethod("GET");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("Accept", "application/json");
 | 
				
			||||||
 | 
					        conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0-Enhanced");
 | 
				
			||||||
 | 
					        conn.setConnectTimeout(5000);
 | 
				
			||||||
 | 
					        conn.setReadTimeout(5000);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        int responseCode = conn.getResponseCode();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (responseCode == 200) {
 | 
				
			||||||
 | 
					            String response = readResponse(conn.getInputStream());
 | 
				
			||||||
 | 
					            return parsePaymentLogResponse(response, orderId);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            throw new Exception("Payment log polling failed: HTTP " + responseCode);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Helper methods
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private JSONObject createQrRefreshPayload(QrisTransaction transaction, String newOrderId) throws Exception {
 | 
				
			||||||
 | 
					        JSONObject customField1 = new JSONObject();
 | 
				
			||||||
 | 
					        customField1.put("parent_transaction_id", transaction.getTransactionId());
 | 
				
			||||||
 | 
					        customField1.put("parent_order_id", transaction.getOrderId());
 | 
				
			||||||
 | 
					        customField1.put("parent_reference_id", transaction.getReferenceId());
 | 
				
			||||||
 | 
					        customField1.put("qr_refresh_time", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()).format(new Date()));
 | 
				
			||||||
 | 
					        customField1.put("qr_refresh_count", System.currentTimeMillis());
 | 
				
			||||||
 | 
					        customField1.put("is_qr_refresh", true);
 | 
				
			||||||
 | 
					        customField1.put("detected_provider", transaction.getDetectedProvider());
 | 
				
			||||||
 | 
					        customField1.put("expiration_minutes", transaction.getQrExpirationMinutes());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        JSONObject payload = new JSONObject();
 | 
				
			||||||
 | 
					        payload.put("payment_type", "qris");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        JSONObject transactionDetails = new JSONObject();
 | 
				
			||||||
 | 
					        transactionDetails.put("order_id", newOrderId);
 | 
				
			||||||
 | 
					        transactionDetails.put("gross_amount", transaction.getOriginalAmount());
 | 
				
			||||||
 | 
					        payload.put("transaction_details", transactionDetails);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        JSONObject customerDetails = new JSONObject();
 | 
				
			||||||
 | 
					        customerDetails.put("first_name", "Test");
 | 
				
			||||||
 | 
					        customerDetails.put("last_name", "Customer");
 | 
				
			||||||
 | 
					        customerDetails.put("email", "test@example.com");
 | 
				
			||||||
 | 
					        customerDetails.put("phone", "081234567890");
 | 
				
			||||||
 | 
					        payload.put("customer_details", customerDetails);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        JSONArray itemDetails = new JSONArray();
 | 
				
			||||||
 | 
					        JSONObject item = new JSONObject();
 | 
				
			||||||
 | 
					        item.put("id", "item1_qr_refresh_" + System.currentTimeMillis());
 | 
				
			||||||
 | 
					        item.put("price", transaction.getOriginalAmount());
 | 
				
			||||||
 | 
					        item.put("quantity", 1);
 | 
				
			||||||
 | 
					        item.put("name", "QRIS Payment QR Refresh - " + new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(new Date()) + 
 | 
				
			||||||
 | 
					                " (" + transaction.getDetectedProvider().toUpperCase() + " - " + transaction.getQrExpirationMinutes() + "min)");
 | 
				
			||||||
 | 
					        itemDetails.put(item);
 | 
				
			||||||
 | 
					        payload.put("item_details", itemDetails);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        payload.put("custom_field1", customField1.toString());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        JSONObject qrisDetails = new JSONObject();
 | 
				
			||||||
 | 
					        qrisDetails.put("acquirer", "gopay");
 | 
				
			||||||
 | 
					        qrisDetails.put("qr_refresh", true);
 | 
				
			||||||
 | 
					        qrisDetails.put("parent_transaction_id", transaction.getTransactionId());
 | 
				
			||||||
 | 
					        qrisDetails.put("refresh_timestamp", System.currentTimeMillis());
 | 
				
			||||||
 | 
					        qrisDetails.put("provider", transaction.getDetectedProvider());
 | 
				
			||||||
 | 
					        qrisDetails.put("expiration_minutes", transaction.getQrExpirationMinutes());
 | 
				
			||||||
 | 
					        payload.put("qris", qrisDetails);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return payload;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private JSONObject createWebhookPayload(QrisTransaction transaction, String signatureKey) throws Exception {
 | 
				
			||||||
 | 
					        JSONObject payload = new JSONObject();
 | 
				
			||||||
 | 
					        payload.put("transaction_type", "on-us");
 | 
				
			||||||
 | 
					        payload.put("transaction_time", transaction.getTransactionTime() != null ? transaction.getTransactionTime() : getCurrentISOTime());
 | 
				
			||||||
 | 
					        payload.put("transaction_status", "settlement");
 | 
				
			||||||
 | 
					        payload.put("transaction_id", transaction.getCurrentQrTransactionId());
 | 
				
			||||||
 | 
					        payload.put("status_message", "midtrans payment notification");
 | 
				
			||||||
 | 
					        payload.put("status_code", "200");
 | 
				
			||||||
 | 
					        payload.put("signature_key", signatureKey);
 | 
				
			||||||
 | 
					        payload.put("settlement_time", getCurrentISOTime());
 | 
				
			||||||
 | 
					        payload.put("payment_type", "qris");
 | 
				
			||||||
 | 
					        payload.put("order_id", transaction.getOrderId());
 | 
				
			||||||
 | 
					        payload.put("merchant_id", transaction.getMerchantId() != null ? transaction.getMerchantId() : "G616299250");
 | 
				
			||||||
 | 
					        payload.put("issuer", transaction.getActualIssuer() != null ? transaction.getActualIssuer() : transaction.getAcquirer());
 | 
				
			||||||
 | 
					        payload.put("gross_amount", transaction.getGrossAmount() != null ? transaction.getGrossAmount() : String.valueOf(transaction.getOriginalAmount()));
 | 
				
			||||||
 | 
					        payload.put("fraud_status", "accept");
 | 
				
			||||||
 | 
					        payload.put("currency", "IDR");
 | 
				
			||||||
 | 
					        payload.put("acquirer", transaction.getActualAcquirer() != null ? transaction.getActualAcquirer() : transaction.getAcquirer());
 | 
				
			||||||
 | 
					        payload.put("shopeepay_reference_number", "");
 | 
				
			||||||
 | 
					        payload.put("reference_id", transaction.getReferenceId() != null ? transaction.getReferenceId() : "DUMMY_REFERENCE_ID");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Enhanced fields
 | 
				
			||||||
 | 
					        payload.put("detected_provider", transaction.getDetectedProvider());
 | 
				
			||||||
 | 
					        payload.put("qr_expiration_minutes", transaction.getQrExpirationMinutes());
 | 
				
			||||||
 | 
					        payload.put("is_simulation", true);
 | 
				
			||||||
 | 
					        payload.put("simulation_type", "enhanced_manual");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (transaction.getQrString() != null && !transaction.getQrString().isEmpty()) {
 | 
				
			||||||
 | 
					            payload.put("qr_string", transaction.getQrString());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return payload;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private QrisRepository.QrRefreshResult parseQrRefreshResponse(String response) throws Exception {
 | 
				
			||||||
 | 
					        JSONObject jsonResponse = new JSONObject(response);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (jsonResponse.has("status_code")) {
 | 
				
			||||||
 | 
					            String statusCode = jsonResponse.getString("status_code");
 | 
				
			||||||
 | 
					            if (!statusCode.equals("201")) {
 | 
				
			||||||
 | 
					                String statusMessage = jsonResponse.optString("status_message", "Unknown error");
 | 
				
			||||||
 | 
					                throw new Exception("QR refresh failed: " + statusCode + " - " + statusMessage);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String newQrUrl = null;
 | 
				
			||||||
 | 
					        String newQrString = null;
 | 
				
			||||||
 | 
					        String newTransactionId = jsonResponse.optString("transaction_id", "");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Get QR URL from actions
 | 
				
			||||||
 | 
					        if (jsonResponse.has("actions")) {
 | 
				
			||||||
 | 
					            JSONArray actionsArray = jsonResponse.getJSONArray("actions");
 | 
				
			||||||
 | 
					            if (actionsArray.length() > 0) {
 | 
				
			||||||
 | 
					                JSONObject actions = actionsArray.getJSONObject(0);
 | 
				
			||||||
 | 
					                newQrUrl = actions.getString("url");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Get QR String
 | 
				
			||||||
 | 
					        if (jsonResponse.has("qr_string")) {
 | 
				
			||||||
 | 
					            newQrString = jsonResponse.getString("qr_string");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return new QrisRepository.QrRefreshResult(newQrUrl, newQrString, newTransactionId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private QrisRepository.PaymentStatusResult parseStatusResponse(String response, QrisTransaction transaction) throws Exception {
 | 
				
			||||||
 | 
					        JSONObject statusResponse = new JSONObject(response);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String transactionStatus = statusResponse.optString("transaction_status", "");
 | 
				
			||||||
 | 
					        String paymentType = statusResponse.optString("payment_type", "");
 | 
				
			||||||
 | 
					        String actualIssuer = statusResponse.optString("issuer", "");
 | 
				
			||||||
 | 
					        String actualAcquirer = statusResponse.optString("acquirer", "");
 | 
				
			||||||
 | 
					        String qrStringFromStatus = statusResponse.optString("qr_string", "");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        QrisRepository.PaymentStatusResult result = new QrisRepository.PaymentStatusResult(transactionStatus);
 | 
				
			||||||
 | 
					        result.paymentType = paymentType;
 | 
				
			||||||
 | 
					        result.issuer = actualIssuer;
 | 
				
			||||||
 | 
					        result.acquirer = actualAcquirer;
 | 
				
			||||||
 | 
					        result.qrString = qrStringFromStatus;
 | 
				
			||||||
 | 
					        result.statusChanged = !transactionStatus.equals(transaction.getCurrentStatus());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return result;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private QrisRepository.PaymentLogResult parsePaymentLogResponse(String response, String orderId) throws Exception {
 | 
				
			||||||
 | 
					        JSONObject json = new JSONObject(response);
 | 
				
			||||||
 | 
					        JSONArray results = json.optJSONArray("results");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (results != null && results.length() > 0) {
 | 
				
			||||||
 | 
					            for (int i = 0; i < results.length(); i++) {
 | 
				
			||||||
 | 
					                JSONObject log = results.getJSONObject(i);
 | 
				
			||||||
 | 
					                JSONObject reqBody = log.optJSONObject("request_body");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (reqBody != null) {
 | 
				
			||||||
 | 
					                    String transactionStatus = reqBody.optString("transaction_status");
 | 
				
			||||||
 | 
					                    String logOrderId = reqBody.optString("order_id");
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if (orderId.equals(logOrderId) && 
 | 
				
			||||||
 | 
					                        (transactionStatus.equals("pending") || 
 | 
				
			||||||
 | 
					                         transactionStatus.equals("settlement") || 
 | 
				
			||||||
 | 
					                         transactionStatus.equals("capture") ||
 | 
				
			||||||
 | 
					                         transactionStatus.equals("success"))) {
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        return new QrisRepository.PaymentLogResult(true, transactionStatus, logOrderId);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return new QrisRepository.PaymentLogResult(false, "", orderId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String readResponse(InputStream inputStream) throws Exception {
 | 
				
			||||||
 | 
					        if (inputStream == null) return "";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
 | 
				
			||||||
 | 
					        StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					        String line;
 | 
				
			||||||
 | 
					        while ((line = br.readLine()) != null) {
 | 
				
			||||||
 | 
					            response.append(line);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return response.toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String getServerKey() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            String base64 = MIDTRANS_AUTH.replace("Basic ", "");
 | 
				
			||||||
 | 
					            byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT);
 | 
				
			||||||
 | 
					            String decodedString = new String(decoded);
 | 
				
			||||||
 | 
					            return decodedString.replace(":", "");
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Error decoding server key: " + e.getMessage());
 | 
				
			||||||
 | 
					            return "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
 | 
				
			||||||
 | 
					        String input = orderId + statusCode + grossAmount + serverKey;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            MessageDigest md = MessageDigest.getInstance("SHA-512");
 | 
				
			||||||
 | 
					            byte[] messageDigest = md.digest(input.getBytes());
 | 
				
			||||||
 | 
					            StringBuilder hexString = new StringBuilder();
 | 
				
			||||||
 | 
					            for (byte b : messageDigest) {
 | 
				
			||||||
 | 
					                String hex = Integer.toHexString(0xff & b);
 | 
				
			||||||
 | 
					                if (hex.length() == 1) hexString.append('0');
 | 
				
			||||||
 | 
					                hexString.append(hex);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return hexString.toString();
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Error generating signature: " + e.getMessage());
 | 
				
			||||||
 | 
					            return "dummy_signature";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String getCurrentISOTime() {
 | 
				
			||||||
 | 
					        return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
 | 
				
			||||||
 | 
					            .format(new Date());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,544 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.qris.presenter;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.BuildConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.os.Handler;
 | 
				
			||||||
 | 
					import android.os.Looper;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.model.QrisRepository;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.model.QrisTransaction;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.view.QrisResultContract;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Presenter untuk QrisResult module
 | 
				
			||||||
 | 
					 * Menghandle semua business logic dan koordinasi antara Model dan View
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class QrisResultPresenter implements QrisResultContract.Presenter {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private static final String TAG = "QrisResultPresenter";
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private QrisResultContract.View view;
 | 
				
			||||||
 | 
					    private QrisRepository repository;
 | 
				
			||||||
 | 
					    private QrisTransaction transaction;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Handlers untuk background tasks
 | 
				
			||||||
 | 
					    private Handler timerHandler;
 | 
				
			||||||
 | 
					    private Handler qrRefreshHandler;
 | 
				
			||||||
 | 
					    private Handler paymentMonitorHandler;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Runnables untuk periodic tasks
 | 
				
			||||||
 | 
					    private Runnable timerRunnable;
 | 
				
			||||||
 | 
					    private Runnable qrRefreshRunnable;
 | 
				
			||||||
 | 
					    private Runnable paymentMonitorRunnable;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // State management
 | 
				
			||||||
 | 
					    private boolean isTimerActive = false;
 | 
				
			||||||
 | 
					    private boolean isQrRefreshActive = false;
 | 
				
			||||||
 | 
					    private boolean isPaymentMonitorActive = false;
 | 
				
			||||||
 | 
					    private String lastKnownStatus = "pending";
 | 
				
			||||||
 | 
					    private int refreshCounter = 0;
 | 
				
			||||||
 | 
					    private static final int MAX_REFRESH_ATTEMPTS = BuildConfig.MAX_REFRESH_ATTEMPTS;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public QrisResultPresenter() {
 | 
				
			||||||
 | 
					        this.repository = QrisRepository.getInstance();
 | 
				
			||||||
 | 
					        this.transaction = new QrisTransaction();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Initialize handlers
 | 
				
			||||||
 | 
					        this.timerHandler = new Handler(Looper.getMainLooper());
 | 
				
			||||||
 | 
					        this.qrRefreshHandler = new Handler(Looper.getMainLooper());
 | 
				
			||||||
 | 
					        this.paymentMonitorHandler = new Handler(Looper.getMainLooper());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void attachView(QrisResultContract.View view) {
 | 
				
			||||||
 | 
					        this.view = view;
 | 
				
			||||||
 | 
					        Log.d(TAG, "📎 View attached to presenter");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void detachView() {
 | 
				
			||||||
 | 
					        this.view = null;
 | 
				
			||||||
 | 
					        Log.d(TAG, "📎 View detached from presenter");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onDestroy() {
 | 
				
			||||||
 | 
					        stopAllTimers();
 | 
				
			||||||
 | 
					        detachView();
 | 
				
			||||||
 | 
					        Log.d(TAG, "💀 Presenter destroyed");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void initializeTransaction(String orderId, String transactionId, String amount, 
 | 
				
			||||||
 | 
					                                    String qrImageUrl, String qrString, String acquirer) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🚀 Initializing transaction");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            int amountInt = Integer.parseInt(amount);
 | 
				
			||||||
 | 
					            transaction.initialize(orderId, transactionId, amountInt, qrImageUrl, qrString, acquirer);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (view != null) {
 | 
				
			||||||
 | 
					                view.showAmount(transaction.getFormattedAmount());
 | 
				
			||||||
 | 
					                view.showQrImage(transaction.getQrImageUrl());
 | 
				
			||||||
 | 
					                view.showProviderName(transaction.getDisplayProviderName());
 | 
				
			||||||
 | 
					                view.showStatus("Waiting for payment...");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Log.d(TAG, "✅ Transaction initialized successfully");
 | 
				
			||||||
 | 
					            Log.d(TAG, "   Provider: " + transaction.getDetectedProvider());
 | 
				
			||||||
 | 
					            Log.d(TAG, "   Expiration: " + transaction.getQrExpirationMinutes() + " minutes");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "❌ Failed to initialize transaction: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					            if (view != null) {
 | 
				
			||||||
 | 
					                view.showError("Failed to initialize transaction: " + e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void startQrManagement() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔄 Starting QR management");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        isQrRefreshActive = true;
 | 
				
			||||||
 | 
					        startTimer();
 | 
				
			||||||
 | 
					        startQrRefreshMonitoring();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void stopQrManagement() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🛑 Stopping QR management");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        isQrRefreshActive = false;
 | 
				
			||||||
 | 
					        stopTimer();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (qrRefreshHandler != null && qrRefreshRunnable != null) {
 | 
				
			||||||
 | 
					            qrRefreshHandler.removeCallbacks(qrRefreshRunnable);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void startTimer() {
 | 
				
			||||||
 | 
					        if (isTimerActive) {
 | 
				
			||||||
 | 
					            stopTimer();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Log.d(TAG, "⏰ Starting timer");
 | 
				
			||||||
 | 
					        isTimerActive = true;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Reset creation time
 | 
				
			||||||
 | 
					        transaction.setQrCreationTime(System.currentTimeMillis());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        timerRunnable = new Runnable() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void run() {
 | 
				
			||||||
 | 
					                if (!isTimerActive || transaction.isPaymentProcessed()) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int remainingSeconds = transaction.getRemainingTimeInSeconds();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (remainingSeconds > 0) {
 | 
				
			||||||
 | 
					                    // Update UI di main thread
 | 
				
			||||||
 | 
					                    new Handler(Looper.getMainLooper()).post(() -> {
 | 
				
			||||||
 | 
					                        if (view != null) {
 | 
				
			||||||
 | 
					                            int displayMinutes = remainingSeconds / 60;
 | 
				
			||||||
 | 
					                            int displaySeconds = remainingSeconds % 60;
 | 
				
			||||||
 | 
					                            String timeDisplay = String.format("%d:%02d", displayMinutes, displaySeconds);
 | 
				
			||||||
 | 
					                            view.showTimer(timeDisplay);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Schedule next update
 | 
				
			||||||
 | 
					                    timerHandler.postDelayed(this, 1000);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // Timer expired
 | 
				
			||||||
 | 
					                    Log.w(TAG, "⏰ Timer expired");
 | 
				
			||||||
 | 
					                    isTimerActive = false;
 | 
				
			||||||
 | 
					                    onQrExpired();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        timerHandler.post(timerRunnable);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void stopTimer() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "⏰ Stopping timer");
 | 
				
			||||||
 | 
					        isTimerActive = false;
 | 
				
			||||||
 | 
					        if (timerHandler != null && timerRunnable != null) {
 | 
				
			||||||
 | 
					            timerHandler.removeCallbacks(timerRunnable);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void refreshQrCode() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔄 Refreshing QR code - Attempt " + refreshCounter);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Pastikan di Main Thread untuk UI updates
 | 
				
			||||||
 | 
					        new Handler(Looper.getMainLooper()).post(() -> {
 | 
				
			||||||
 | 
					            if (view != null) {
 | 
				
			||||||
 | 
					                view.showQrRefreshing();
 | 
				
			||||||
 | 
					                view.showLoading();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        repository.refreshQrCode(transaction, new QrisRepository.RepositoryCallback<QrisRepository.QrRefreshResult>() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onSuccess(QrisRepository.QrRefreshResult result) {
 | 
				
			||||||
 | 
					                Log.d(TAG, "✅ QR refresh successful");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Update transaction data
 | 
				
			||||||
 | 
					                transaction.updateQrCode(result.qrUrl, result.qrString, result.transactionId);
 | 
				
			||||||
 | 
					                transaction.setQrCreationTime(System.currentTimeMillis()); // Reset creation time
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Pastikan di Main Thread untuk UI updates
 | 
				
			||||||
 | 
					                new Handler(Looper.getMainLooper()).post(() -> {
 | 
				
			||||||
 | 
					                    if (view != null) {
 | 
				
			||||||
 | 
					                        view.hideLoading();
 | 
				
			||||||
 | 
					                        view.updateQrImage(result.qrUrl);
 | 
				
			||||||
 | 
					                        view.updateQrUrl(result.qrUrl);
 | 
				
			||||||
 | 
					                        view.showQrRefreshSuccess();
 | 
				
			||||||
 | 
					                        view.showToast("QR Code berhasil diperbarui!");
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Stop dan restart timer dengan benar
 | 
				
			||||||
 | 
					                    stopTimer();
 | 
				
			||||||
 | 
					                    startTimer();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    // Restart monitoring
 | 
				
			||||||
 | 
					                    isQrRefreshActive = true;
 | 
				
			||||||
 | 
					                    startQrRefreshMonitoring();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onError(String errorMessage) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "❌ QR refresh failed: " + errorMessage);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                new Handler(Looper.getMainLooper()).post(() -> {
 | 
				
			||||||
 | 
					                    if (view != null) {
 | 
				
			||||||
 | 
					                        view.hideLoading();
 | 
				
			||||||
 | 
					                        view.showQrRefreshFailed(errorMessage);
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        if (refreshCounter >= MAX_REFRESH_ATTEMPTS) {
 | 
				
			||||||
 | 
					                            view.navigateToMain();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onQrExpired() {
 | 
				
			||||||
 | 
					        Log.w(TAG, "⏰ Handling QR expiration");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Stop current timers to prevent race conditions
 | 
				
			||||||
 | 
					        stopTimer();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (view != null) {
 | 
				
			||||||
 | 
					            view.showQrExpired();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Cek apakah sudah mencapai limit refresh
 | 
				
			||||||
 | 
					        if (refreshCounter >= MAX_REFRESH_ATTEMPTS) {
 | 
				
			||||||
 | 
					            Log.w(TAG, "🛑 Maximum refresh attempts reached");
 | 
				
			||||||
 | 
					            if (view != null) {
 | 
				
			||||||
 | 
					                view.showToast("Maksimum percobaan refresh QR tercapai");
 | 
				
			||||||
 | 
					                view.navigateToMain();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Increment counter
 | 
				
			||||||
 | 
					        refreshCounter++;
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔄 Refresh attempt #" + refreshCounter);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Auto-refresh tanpa delay
 | 
				
			||||||
 | 
					        refreshQrCode();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void startQrRefreshMonitoring() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔄 Starting QR refresh monitoring");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        qrRefreshRunnable = new Runnable() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void run() {
 | 
				
			||||||
 | 
					                if (!isQrRefreshActive || transaction.isPaymentProcessed()) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Check if QR expired
 | 
				
			||||||
 | 
					                if (transaction.isQrExpired()) {
 | 
				
			||||||
 | 
					                    Log.w(TAG, "⏰ QR Code expired during monitoring");
 | 
				
			||||||
 | 
					                    onQrExpired();
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Schedule next check in 30 seconds
 | 
				
			||||||
 | 
					                if (isQrRefreshActive) {
 | 
				
			||||||
 | 
					                    qrRefreshHandler.postDelayed(this, 30000);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        qrRefreshHandler.post(qrRefreshRunnable);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void startPaymentMonitoring() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔍 Starting payment monitoring");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        isPaymentMonitorActive = true;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        paymentMonitorRunnable = new Runnable() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void run() {
 | 
				
			||||||
 | 
					                if (!isPaymentMonitorActive || transaction.isPaymentProcessed()) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                checkPaymentStatus();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Schedule next check in 3 seconds
 | 
				
			||||||
 | 
					                if (isPaymentMonitorActive && !transaction.isPaymentProcessed()) {
 | 
				
			||||||
 | 
					                    paymentMonitorHandler.postDelayed(this, 3000);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        paymentMonitorHandler.post(paymentMonitorRunnable);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void stopPaymentMonitoring() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔍 Stopping payment monitoring");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        isPaymentMonitorActive = false;
 | 
				
			||||||
 | 
					        if (paymentMonitorHandler != null && paymentMonitorRunnable != null) {
 | 
				
			||||||
 | 
					            paymentMonitorHandler.removeCallbacks(paymentMonitorRunnable);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void checkPaymentStatus() {
 | 
				
			||||||
 | 
					        repository.checkPaymentStatus(transaction, new QrisRepository.RepositoryCallback<QrisRepository.PaymentStatusResult>() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onSuccess(QrisRepository.PaymentStatusResult result) {
 | 
				
			||||||
 | 
					                handlePaymentStatusResult(result);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onError(String errorMessage) {
 | 
				
			||||||
 | 
					                Log.w(TAG, "⚠️ Payment status check failed: " + errorMessage);
 | 
				
			||||||
 | 
					                // Don't show error to user untuk status check failures
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void handlePaymentStatusResult(QrisRepository.PaymentStatusResult result) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "💳 Payment status result: " + result.status);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Update transaction dengan actual issuer/acquirer
 | 
				
			||||||
 | 
					        if (result.issuer != null && !result.issuer.isEmpty()) {
 | 
				
			||||||
 | 
					            transaction.setActualIssuer(result.issuer);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (result.acquirer != null && !result.acquirer.isEmpty()) {
 | 
				
			||||||
 | 
					            transaction.setActualAcquirer(result.acquirer);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Update QR string jika ada
 | 
				
			||||||
 | 
					        if (result.qrString != null && !result.qrString.isEmpty()) {
 | 
				
			||||||
 | 
					            transaction.setQrString(result.qrString);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Handle status changes
 | 
				
			||||||
 | 
					        if (!result.status.equals(lastKnownStatus)) {
 | 
				
			||||||
 | 
					            Log.d(TAG, "📊 Status changed: " + lastKnownStatus + " -> " + result.status);
 | 
				
			||||||
 | 
					            lastKnownStatus = result.status;
 | 
				
			||||||
 | 
					            transaction.setCurrentStatus(result.status);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (view != null) {
 | 
				
			||||||
 | 
					                switch (result.status) {
 | 
				
			||||||
 | 
					                    case "settlement":
 | 
				
			||||||
 | 
					                    case "capture":
 | 
				
			||||||
 | 
					                    case "success":
 | 
				
			||||||
 | 
					                        if (!transaction.isPaymentProcessed()) {
 | 
				
			||||||
 | 
					                            handlePaymentSuccess();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                    case "expire":
 | 
				
			||||||
 | 
					                    case "cancel":
 | 
				
			||||||
 | 
					                        view.showPaymentFailed("Payment " + result.status);
 | 
				
			||||||
 | 
					                        stopAllTimers();
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                    case "pending":
 | 
				
			||||||
 | 
					                        view.showPaymentPending();
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                    default:
 | 
				
			||||||
 | 
					                        view.showStatus("Status: " + result.status);
 | 
				
			||||||
 | 
					                        break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void handlePaymentSuccess() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🎉 Payment successful!");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        transaction.setPaymentProcessed(true);
 | 
				
			||||||
 | 
					        stopAllTimers();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (view != null) {
 | 
				
			||||||
 | 
					            String providerName = transaction.getDisplayProviderName();
 | 
				
			||||||
 | 
					            view.showPaymentSuccess(providerName);
 | 
				
			||||||
 | 
					            view.startSuccessAnimation();
 | 
				
			||||||
 | 
					            view.showToast("Pembayaran " + providerName + " berhasil! 🎉");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Don't auto-navigate here - let the view handle the navigation timing
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onCancelClicked() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "❌ Cancel clicked");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        stopAllTimers();
 | 
				
			||||||
 | 
					        if (view != null) {
 | 
				
			||||||
 | 
					            view.finishActivity();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onBackPressed() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "⬅️ Back pressed");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        stopAllTimers();
 | 
				
			||||||
 | 
					        if (view != null) {
 | 
				
			||||||
 | 
					            view.finishActivity();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onSimulatePayment() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🚀 Simulating payment");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (transaction.isPaymentProcessed()) {
 | 
				
			||||||
 | 
					            Log.w(TAG, "⚠️ Payment already processed");
 | 
				
			||||||
 | 
					            if (view != null) {
 | 
				
			||||||
 | 
					                view.showToast("Pembayaran sudah diproses");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        stopAllTimers();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (view != null) {
 | 
				
			||||||
 | 
					            view.showToast("Mensimulasikan pembayaran...");
 | 
				
			||||||
 | 
					            view.showLoading();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        repository.simulatePayment(transaction, new QrisRepository.RepositoryCallback<Boolean>() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onSuccess(Boolean result) {
 | 
				
			||||||
 | 
					                Log.d(TAG, "✅ Payment simulation successful");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (view != null) {
 | 
				
			||||||
 | 
					                    view.hideLoading();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Wait a bit then trigger success
 | 
				
			||||||
 | 
					                new Handler(Looper.getMainLooper()).postDelayed(() -> {
 | 
				
			||||||
 | 
					                    if (!transaction.isPaymentProcessed()) {
 | 
				
			||||||
 | 
					                        handlePaymentSuccess();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }, 2000);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onError(String errorMessage) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "❌ Payment simulation failed: " + errorMessage);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (view != null) {
 | 
				
			||||||
 | 
					                    view.hideLoading();
 | 
				
			||||||
 | 
					                    view.showError("Simulasi gagal: " + errorMessage);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Restart monitoring after simulation failure
 | 
				
			||||||
 | 
					                startPaymentMonitoring();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Stop all timers dan background tasks
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void stopAllTimers() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🛑 Stopping all timers");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        stopTimer();
 | 
				
			||||||
 | 
					        stopQrManagement();
 | 
				
			||||||
 | 
					        stopPaymentMonitoring();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Clear all pending callbacks
 | 
				
			||||||
 | 
					        if (timerHandler != null) {
 | 
				
			||||||
 | 
					            timerHandler.removeCallbacksAndMessages(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (qrRefreshHandler != null) {
 | 
				
			||||||
 | 
					            qrRefreshHandler.removeCallbacksAndMessages(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (paymentMonitorHandler != null) {
 | 
				
			||||||
 | 
					            paymentMonitorHandler.removeCallbacksAndMessages(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Public method untuk start semua monitoring
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public void startAllMonitoring() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🚀 Starting all monitoring");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        startQrManagement();
 | 
				
			||||||
 | 
					        startPaymentMonitoring();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Start polling untuk payment logs
 | 
				
			||||||
 | 
					        repository.pollPaymentLogs(transaction.getOrderId(), new QrisRepository.RepositoryCallback<QrisRepository.PaymentLogResult>() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onSuccess(QrisRepository.PaymentLogResult result) {
 | 
				
			||||||
 | 
					                if (result.found) {
 | 
				
			||||||
 | 
					                    Log.d(TAG, "📊 Payment log found with status: " + result.status);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if ("settlement".equals(result.status) || 
 | 
				
			||||||
 | 
					                        "capture".equals(result.status) || 
 | 
				
			||||||
 | 
					                        "success".equals(result.status)) {
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        if (!transaction.isPaymentProcessed()) {
 | 
				
			||||||
 | 
					                            handlePaymentSuccess();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if (view != null) {
 | 
				
			||||||
 | 
					                        view.showToast("Payment log found!");
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onError(String errorMessage) {
 | 
				
			||||||
 | 
					                Log.w(TAG, "⚠️ Payment log polling failed: " + errorMessage);
 | 
				
			||||||
 | 
					                // Don't show error to user
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Getter untuk transaction (untuk testing atau debugging)
 | 
				
			||||||
 | 
					    public QrisTransaction getTransaction() {
 | 
				
			||||||
 | 
					        return transaction;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,162 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.qris.utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.graphics.Bitmap;
 | 
				
			||||||
 | 
					import android.graphics.BitmapFactory;
 | 
				
			||||||
 | 
					import android.os.AsyncTask;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import android.widget.ImageView;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.InputStream;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URI;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Utility class untuk loading QR images secara asynchronous
 | 
				
			||||||
 | 
					 * Dengan error handling dan validation yang proper
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class QrImageLoader {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private static final String TAG = "QrImageLoader";
 | 
				
			||||||
 | 
					    private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1PM2t1bXkwVDl4M1VvYnVvVTc3NW5QbXc=";
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Interface untuk callback hasil loading image
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public interface ImageLoadCallback {
 | 
				
			||||||
 | 
					        void onImageLoaded(Bitmap bitmap);
 | 
				
			||||||
 | 
					        void onImageLoadFailed(String errorMessage);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Load QR image dari URL dengan callback
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void loadQrImage(String qrImageUrl, ImageLoadCallback callback) {
 | 
				
			||||||
 | 
					        if (qrImageUrl == null || qrImageUrl.isEmpty()) {
 | 
				
			||||||
 | 
					            Log.w(TAG, "⚠️ QR image URL is empty");
 | 
				
			||||||
 | 
					            callback.onImageLoadFailed("QR image URL is empty");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (!qrImageUrl.startsWith("http")) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "❌ Invalid QR URL format: " + qrImageUrl);
 | 
				
			||||||
 | 
					            callback.onImageLoadFailed("Invalid QR code URL format");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "🖼️ Loading QR image from: " + qrImageUrl);
 | 
				
			||||||
 | 
					        new EnhancedDownloadImageTask(callback).execute(qrImageUrl);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Load QR image langsung ke ImageView (legacy support)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static void loadQrImageToView(String qrImageUrl, ImageView imageView) {
 | 
				
			||||||
 | 
					        loadQrImage(qrImageUrl, new ImageLoadCallback() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onImageLoaded(Bitmap bitmap) {
 | 
				
			||||||
 | 
					                if (imageView != null) {
 | 
				
			||||||
 | 
					                    imageView.setImageBitmap(bitmap);
 | 
				
			||||||
 | 
					                    Log.d(TAG, "✅ QR code image displayed successfully");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onImageLoadFailed(String errorMessage) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "❌ Failed to display QR code image: " + errorMessage);
 | 
				
			||||||
 | 
					                if (imageView != null) {
 | 
				
			||||||
 | 
					                    imageView.setImageResource(android.R.drawable.ic_menu_report_image);
 | 
				
			||||||
 | 
					                    if (imageView.getContext() != null) {
 | 
				
			||||||
 | 
					                        Toast.makeText(imageView.getContext(), "QR Error: " + errorMessage, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Enhanced AsyncTask untuk download image dengan proper error handling
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static class EnhancedDownloadImageTask extends AsyncTask<String, Void, Bitmap> {
 | 
				
			||||||
 | 
					        private ImageLoadCallback callback;
 | 
				
			||||||
 | 
					        private String errorMessage;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        EnhancedDownloadImageTask(ImageLoadCallback callback) {
 | 
				
			||||||
 | 
					            this.callback = callback;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected Bitmap doInBackground(String... urls) {
 | 
				
			||||||
 | 
					            String urlDisplay = urls[0];
 | 
				
			||||||
 | 
					            Bitmap bitmap = null;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                if (urlDisplay == null || urlDisplay.isEmpty()) {
 | 
				
			||||||
 | 
					                    Log.e(TAG, "❌ Empty QR URL provided");
 | 
				
			||||||
 | 
					                    errorMessage = "QR URL is empty";
 | 
				
			||||||
 | 
					                    return null;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (!urlDisplay.startsWith("http")) {
 | 
				
			||||||
 | 
					                    Log.e(TAG, "❌ Invalid QR URL format: " + urlDisplay);
 | 
				
			||||||
 | 
					                    errorMessage = "Invalid QR URL format";
 | 
				
			||||||
 | 
					                    return null;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                Log.d(TAG, "📥 Downloading image from: " + urlDisplay);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                URL url = new URI(urlDisplay).toURL();
 | 
				
			||||||
 | 
					                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                connection.setDoInput(true);
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(1000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(1000);
 | 
				
			||||||
 | 
					                connection.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
 | 
				
			||||||
 | 
					                connection.setRequestProperty("Accept", "image/*");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Add auth header untuk Midtrans URLs
 | 
				
			||||||
 | 
					                if (urlDisplay.contains("midtrans.com")) {
 | 
				
			||||||
 | 
					                    connection.setRequestProperty("Authorization", MIDTRANS_AUTH);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                connection.connect();
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                Log.d(TAG, "📥 Image download response code: " + responseCode);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (responseCode == 200) {
 | 
				
			||||||
 | 
					                    InputStream input = connection.getInputStream();
 | 
				
			||||||
 | 
					                    bitmap = BitmapFactory.decodeStream(input);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if (bitmap != null) {
 | 
				
			||||||
 | 
					                        Log.d(TAG, "✅ Image downloaded successfully. Size: " + 
 | 
				
			||||||
 | 
					                              bitmap.getWidth() + "x" + bitmap.getHeight());
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        Log.e(TAG, "❌ Failed to decode bitmap from stream");
 | 
				
			||||||
 | 
					                        errorMessage = "Failed to decode QR code image";
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Log.e(TAG, "❌ Failed to download image. HTTP code: " + responseCode);
 | 
				
			||||||
 | 
					                    errorMessage = "Failed to download QR code (HTTP " + responseCode + ")";
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (Exception e) {
 | 
				
			||||||
 | 
					                Log.e(TAG, "❌ Exception downloading image: " + e.getMessage(), e);
 | 
				
			||||||
 | 
					                errorMessage = "Error downloading QR code: " + e.getMessage();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return bitmap;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected void onPostExecute(Bitmap result) {
 | 
				
			||||||
 | 
					            if (callback != null) {
 | 
				
			||||||
 | 
					                if (result != null) {
 | 
				
			||||||
 | 
					                    callback.onImageLoaded(result);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    callback.onImageLoadFailed(errorMessage != null ? errorMessage : "Unknown error");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,757 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.qris.view;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.animation.AnimatorSet;
 | 
				
			||||||
 | 
					import android.animation.ObjectAnimator;
 | 
				
			||||||
 | 
					import android.content.ClipData;
 | 
				
			||||||
 | 
					import android.content.ClipboardManager;
 | 
				
			||||||
 | 
					import android.content.Context;
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import android.graphics.Bitmap;
 | 
				
			||||||
 | 
					import android.net.Uri;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.os.Handler;
 | 
				
			||||||
 | 
					import android.os.Looper;
 | 
				
			||||||
 | 
					import android.util.Log;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.widget.Button;
 | 
				
			||||||
 | 
					import android.widget.ImageView;
 | 
				
			||||||
 | 
					import android.widget.ProgressBar;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import androidx.annotation.Nullable;
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					import androidx.cardview.widget.CardView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.ReceiptActivity;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.model.QrisTransaction;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.presenter.QrisResultPresenter;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.utils.QrImageLoader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * QrisResultActivity - refactored menggunakan MVP pattern
 | 
				
			||||||
 | 
					 * Hanya menghandle UI logic, business logic ada di Presenter
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class QrisResultActivity extends AppCompatActivity implements QrisResultContract.View {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private static final String TAG = "QrisResultActivity";
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Presenter
 | 
				
			||||||
 | 
					    private QrisResultPresenter presenter;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Main UI Components
 | 
				
			||||||
 | 
					    private ImageView qrImageView;
 | 
				
			||||||
 | 
					    private TextView amountTextView;
 | 
				
			||||||
 | 
					    private TextView timerTextView;
 | 
				
			||||||
 | 
					    private Button cancelButton;
 | 
				
			||||||
 | 
					    private TextView qrisLogo;
 | 
				
			||||||
 | 
					    private CardView mainCard;
 | 
				
			||||||
 | 
					    private View headerBackground;
 | 
				
			||||||
 | 
					    private View backNavigation;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Hidden components for functionality
 | 
				
			||||||
 | 
					    private TextView referenceTextView;
 | 
				
			||||||
 | 
					    private TextView statusTextView;
 | 
				
			||||||
 | 
					    private TextView qrStatusTextView;
 | 
				
			||||||
 | 
					    private ProgressBar progressBar;
 | 
				
			||||||
 | 
					    private Button downloadQrisButton;
 | 
				
			||||||
 | 
					    private Button checkStatusButton;
 | 
				
			||||||
 | 
					    private Button returnMainButton;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Success screen views
 | 
				
			||||||
 | 
					    private View successScreen;
 | 
				
			||||||
 | 
					    private ImageView successIcon;
 | 
				
			||||||
 | 
					    private TextView successMessage;
 | 
				
			||||||
 | 
					    private TextView qrUrlTextView;
 | 
				
			||||||
 | 
					    private Button simulatorButton;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Animation handler
 | 
				
			||||||
 | 
					    private Handler animationHandler = new Handler(Looper.getMainLooper());
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(@Nullable Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_qris_result);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "=== QRIS RESULT ACTIVITY STARTED (MVP) ===");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Initialize presenter
 | 
				
			||||||
 | 
					        presenter = new QrisResultPresenter();
 | 
				
			||||||
 | 
					        presenter.attachView(this);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Initialize views
 | 
				
			||||||
 | 
					        initializeViews();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Setup UI components
 | 
				
			||||||
 | 
					        setupUI();
 | 
				
			||||||
 | 
					        setupClickListeners();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Get intent data dan initialize transaction
 | 
				
			||||||
 | 
					        initializeFromIntent();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Start monitoring
 | 
				
			||||||
 | 
					        presenter.startAllMonitoring();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Initialize all view components
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void initializeViews() {
 | 
				
			||||||
 | 
					        // Main visible components
 | 
				
			||||||
 | 
					        qrImageView = findViewById(R.id.qrImageView);
 | 
				
			||||||
 | 
					        amountTextView = findViewById(R.id.amountTextView);
 | 
				
			||||||
 | 
					        timerTextView = findViewById(R.id.timerTextView);
 | 
				
			||||||
 | 
					        cancelButton = findViewById(R.id.cancel_button);
 | 
				
			||||||
 | 
					        qrisLogo = findViewById(R.id.qris_logo);
 | 
				
			||||||
 | 
					        mainCard = findViewById(R.id.main_card);
 | 
				
			||||||
 | 
					        headerBackground = findViewById(R.id.header_background);
 | 
				
			||||||
 | 
					        backNavigation = findViewById(R.id.back_navigation);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Hidden components for functionality
 | 
				
			||||||
 | 
					        referenceTextView = findViewById(R.id.referenceTextView);
 | 
				
			||||||
 | 
					        statusTextView = findViewById(R.id.statusTextView);
 | 
				
			||||||
 | 
					        qrStatusTextView = findViewById(R.id.qrStatusTextView);
 | 
				
			||||||
 | 
					        progressBar = findViewById(R.id.progressBar);
 | 
				
			||||||
 | 
					        downloadQrisButton = findViewById(R.id.downloadQrisButton);
 | 
				
			||||||
 | 
					        checkStatusButton = findViewById(R.id.checkStatusButton);
 | 
				
			||||||
 | 
					        returnMainButton = findViewById(R.id.returnMainButton);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Success screen views
 | 
				
			||||||
 | 
					        successScreen = findViewById(R.id.success_screen);
 | 
				
			||||||
 | 
					        successIcon = findViewById(R.id.success_icon);
 | 
				
			||||||
 | 
					        successMessage = findViewById(R.id.success_message);
 | 
				
			||||||
 | 
					        qrUrlTextView = findViewById(R.id.qrUrlTextView);
 | 
				
			||||||
 | 
					        simulatorButton = findViewById(R.id.simulatorButton);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Setup basic UI components
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void setupUI() {
 | 
				
			||||||
 | 
					        // Hide success screen initially
 | 
				
			||||||
 | 
					        if (successScreen != null) {
 | 
				
			||||||
 | 
					            successScreen.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Disable check status button initially
 | 
				
			||||||
 | 
					        if (checkStatusButton != null) {
 | 
				
			||||||
 | 
					            checkStatusButton.setEnabled(false);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Setup URL copy functionality
 | 
				
			||||||
 | 
					        setupUrlCopyFunctionality();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Setup simulator button
 | 
				
			||||||
 | 
					        setupSimulatorButton();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get data dari intent dan initialize transaction
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void initializeFromIntent() {
 | 
				
			||||||
 | 
					        Intent intent = getIntent();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        String orderId = intent.getStringExtra("orderId");
 | 
				
			||||||
 | 
					        String transactionId = intent.getStringExtra("transactionId");
 | 
				
			||||||
 | 
					        String amount = String.valueOf(intent.getIntExtra("amount", 0));
 | 
				
			||||||
 | 
					        String qrImageUrl = intent.getStringExtra("qrImageUrl");
 | 
				
			||||||
 | 
					        String qrString = intent.getStringExtra("qrString");
 | 
				
			||||||
 | 
					        String acquirer = intent.getStringExtra("acquirer");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "Initializing transaction with data:");
 | 
				
			||||||
 | 
					        Log.d(TAG, "  Order ID: " + orderId);
 | 
				
			||||||
 | 
					        Log.d(TAG, "  Transaction ID: " + transactionId);
 | 
				
			||||||
 | 
					        Log.d(TAG, "  Amount: " + amount);
 | 
				
			||||||
 | 
					        Log.d(TAG, "  QR URL: " + qrImageUrl);
 | 
				
			||||||
 | 
					        Log.d(TAG, "  Acquirer: " + acquirer);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Validate required data
 | 
				
			||||||
 | 
					        if (orderId == null || transactionId == null) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "❌ Critical error: orderId or transactionId is null!");
 | 
				
			||||||
 | 
					            showError("Missing transaction details! Cannot proceed.");
 | 
				
			||||||
 | 
					            finish();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Initialize via presenter
 | 
				
			||||||
 | 
					        presenter.initializeTransaction(orderId, transactionId, amount, qrImageUrl, qrString, acquirer);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set additional data
 | 
				
			||||||
 | 
					        if (referenceTextView != null) {
 | 
				
			||||||
 | 
					            String referenceId = intent.getStringExtra("referenceId");
 | 
				
			||||||
 | 
					            referenceTextView.setText("Reference ID: " + referenceId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Setup click listeners untuk semua buttons dan views
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void setupClickListeners() {
 | 
				
			||||||
 | 
					        // Cancel button
 | 
				
			||||||
 | 
					        if (cancelButton != null) {
 | 
				
			||||||
 | 
					            cancelButton.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                addClickAnimation(v);
 | 
				
			||||||
 | 
					                presenter.onCancelClicked();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Back navigation
 | 
				
			||||||
 | 
					        if (backNavigation != null) {
 | 
				
			||||||
 | 
					            backNavigation.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                addClickAnimation(v);
 | 
				
			||||||
 | 
					                presenter.onBackPressed();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Hidden check status button untuk testing
 | 
				
			||||||
 | 
					        if (checkStatusButton != null) {
 | 
				
			||||||
 | 
					            checkStatusButton.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                Log.d(TAG, "Manual payment simulation triggered");
 | 
				
			||||||
 | 
					                presenter.onSimulatePayment();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Hidden return main button
 | 
				
			||||||
 | 
					        if (returnMainButton != null) {
 | 
				
			||||||
 | 
					            returnMainButton.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                navigateToMain();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Double tap pada QR logo untuk testing
 | 
				
			||||||
 | 
					        if (qrisLogo != null) {
 | 
				
			||||||
 | 
					            qrisLogo.setOnClickListener(new View.OnClickListener() {
 | 
				
			||||||
 | 
					                private int clickCount = 0;
 | 
				
			||||||
 | 
					                private Handler handler = new Handler();
 | 
				
			||||||
 | 
					                private final int DOUBLE_TAP_TIMEOUT = 300;
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void onClick(View v) {
 | 
				
			||||||
 | 
					                    clickCount++;
 | 
				
			||||||
 | 
					                    if (clickCount == 1) {
 | 
				
			||||||
 | 
					                        handler.postDelayed(() -> clickCount = 0, DOUBLE_TAP_TIMEOUT);
 | 
				
			||||||
 | 
					                    } else if (clickCount == 2) {
 | 
				
			||||||
 | 
					                        // Double tap detected - simulate payment
 | 
				
			||||||
 | 
					                        clickCount = 0;
 | 
				
			||||||
 | 
					                        Log.d(TAG, "Double tap detected - simulating payment");
 | 
				
			||||||
 | 
					                        presenter.onSimulatePayment();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupUrlCopyFunctionality() {
 | 
				
			||||||
 | 
					        if (qrUrlTextView != null) {
 | 
				
			||||||
 | 
					            qrUrlTextView.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                if (presenter.getTransaction() != null) {
 | 
				
			||||||
 | 
					                    String qrUrl = presenter.getTransaction().getQrImageUrl();
 | 
				
			||||||
 | 
					                    if (qrUrl != null) {
 | 
				
			||||||
 | 
					                        ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
 | 
				
			||||||
 | 
					                        ClipData clip = ClipData.newPlainText("QR URL", qrUrl);
 | 
				
			||||||
 | 
					                        clipboard.setPrimaryClip(clip);
 | 
				
			||||||
 | 
					                        showToast("URL copied to clipboard");
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupSimulatorButton() {
 | 
				
			||||||
 | 
					        if (simulatorButton != null) {
 | 
				
			||||||
 | 
					            simulatorButton.setOnClickListener(v -> {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    String simulatorUrl = "https://simulator.sandbox.midtrans.com/v2/qris/index";
 | 
				
			||||||
 | 
					                    Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(simulatorUrl));
 | 
				
			||||||
 | 
					                    startActivity(browserIntent);
 | 
				
			||||||
 | 
					                } catch (Exception e) {
 | 
				
			||||||
 | 
					                    showToast("Could not open browser");
 | 
				
			||||||
 | 
					                    Log.e(TAG, "Error opening simulator URL", e);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // ========================================================================================
 | 
				
			||||||
 | 
					    // MVP CONTRACT VIEW IMPLEMENTATIONS
 | 
				
			||||||
 | 
					    // ========================================================================================
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showQrImage(String qrImageUrl) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🖼️ Showing QR image: " + qrImageUrl);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (qrImageUrl != null && !qrImageUrl.isEmpty()) {
 | 
				
			||||||
 | 
					            QrImageLoader.loadQrImage(qrImageUrl, new QrImageLoader.ImageLoadCallback() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void onImageLoaded(Bitmap bitmap) {
 | 
				
			||||||
 | 
					                    if (qrImageView != null) {
 | 
				
			||||||
 | 
					                        qrImageView.setImageBitmap(bitmap);
 | 
				
			||||||
 | 
					                        qrImageView.setAlpha(1.0f); // Ensure fully visible
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void onImageLoadFailed(String errorMessage) {
 | 
				
			||||||
 | 
					                    Log.e(TAG, "❌ Failed to load QR image: " + errorMessage);
 | 
				
			||||||
 | 
					                    if (qrImageView != null) {
 | 
				
			||||||
 | 
					                        qrImageView.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    showError("Failed to load QR code: " + errorMessage);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Update URL display
 | 
				
			||||||
 | 
					            if (qrUrlTextView != null) {
 | 
				
			||||||
 | 
					                qrUrlTextView.setText(qrImageUrl);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Log.w(TAG, "⚠️ QR image URL is not available");
 | 
				
			||||||
 | 
					            if (qrImageView != null) {
 | 
				
			||||||
 | 
					                qrImageView.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            showToast("QR code URL not available");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showAmount(String formattedAmount) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "💰 Showing amount: " + formattedAmount);
 | 
				
			||||||
 | 
					        if (amountTextView != null) {
 | 
				
			||||||
 | 
					            amountTextView.setText(formattedAmount);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showTimer(String timeDisplay) {
 | 
				
			||||||
 | 
					        if (timerTextView != null) {
 | 
				
			||||||
 | 
					            timerTextView.setText(timeDisplay);
 | 
				
			||||||
 | 
					            timerTextView.setTextColor(getResources().getColor(android.R.color.black));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showStatus(String status) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "📊 Showing status: " + status);
 | 
				
			||||||
 | 
					        if (statusTextView != null) {
 | 
				
			||||||
 | 
					            statusTextView.setText(status);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showProviderName(String providerName) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🏷️ Showing provider: " + providerName);
 | 
				
			||||||
 | 
					        // Provider name bisa ditampilkan di UI jika ada komponen khusus
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void updateQrImage(String newQrImageUrl) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔄 Updating QR image: " + newQrImageUrl);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        runOnUiThread(() -> {
 | 
				
			||||||
 | 
					            // Reset QR image appearance first
 | 
				
			||||||
 | 
					            if (qrImageView != null) {
 | 
				
			||||||
 | 
					                qrImageView.setAlpha(1.0f);
 | 
				
			||||||
 | 
					                qrImageView.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Load new QR image
 | 
				
			||||||
 | 
					            showQrImage(newQrImageUrl);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Update timer display
 | 
				
			||||||
 | 
					            if (timerTextView != null) {
 | 
				
			||||||
 | 
					                timerTextView.setTextColor(getResources().getColor(android.R.color.black));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void updateQrUrl(String newQrUrl) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔄 Updating QR URL: " + newQrUrl);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        runOnUiThread(() -> {
 | 
				
			||||||
 | 
					            if (qrUrlTextView != null) {
 | 
				
			||||||
 | 
					                qrUrlTextView.setText(newQrUrl);
 | 
				
			||||||
 | 
					                qrUrlTextView.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showQrExpired() {
 | 
				
			||||||
 | 
					        Log.w(TAG, "⏰ Showing QR expired");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        runOnUiThread(() -> {
 | 
				
			||||||
 | 
					            // Make QR semi-transparent
 | 
				
			||||||
 | 
					            if (qrImageView != null) {
 | 
				
			||||||
 | 
					                qrImageView.setAlpha(0.5f);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (timerTextView != null) {
 | 
				
			||||||
 | 
					                timerTextView.setText("EXPIRED");
 | 
				
			||||||
 | 
					                timerTextView.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showQrRefreshing() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🔄 Showing QR refreshing");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        runOnUiThread(() -> {
 | 
				
			||||||
 | 
					            if (timerTextView != null) {
 | 
				
			||||||
 | 
					                timerTextView.setText("Refreshing...");
 | 
				
			||||||
 | 
					                timerTextView.setTextColor(getResources().getColor(android.R.color.holo_orange_dark));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showQrRefreshFailed(String errorMessage) {
 | 
				
			||||||
 | 
					        Log.e(TAG, "❌ QR refresh failed: " + errorMessage);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        runOnUiThread(() -> {
 | 
				
			||||||
 | 
					            if (timerTextView != null) {
 | 
				
			||||||
 | 
					                timerTextView.setText("Refresh Gagal");
 | 
				
			||||||
 | 
					                timerTextView.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Tidak langsung navigate, biarkan presenter handle
 | 
				
			||||||
 | 
					            showToast("Gagal refresh QR: " + errorMessage);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showQrRefreshSuccess() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "✅ QR refresh successful");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        runOnUiThread(() -> {
 | 
				
			||||||
 | 
					            // Reset QR image appearance
 | 
				
			||||||
 | 
					            if (qrImageView != null) {
 | 
				
			||||||
 | 
					                qrImageView.setAlpha(1.0f);
 | 
				
			||||||
 | 
					                qrImageView.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Reset timer color and show success message
 | 
				
			||||||
 | 
					            if (timerTextView != null) {
 | 
				
			||||||
 | 
					                timerTextView.setTextColor(getResources().getColor(android.R.color.black));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Show success toast
 | 
				
			||||||
 | 
					            showToast("QR Code berhasil diperbarui!");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showPaymentSuccess(String providerName) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🎉 Showing payment success for: " + providerName);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        runOnUiThread(() -> {
 | 
				
			||||||
 | 
					            showFullScreenSuccess(providerName);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Navigate to receipt after 3 seconds, then to main activity
 | 
				
			||||||
 | 
					            new Handler(Looper.getMainLooper()).postDelayed(() -> {
 | 
				
			||||||
 | 
					                // Fixed: Remove the undefined 'view' variable and just check if activity is still valid
 | 
				
			||||||
 | 
					                if (!isFinishing() && !isDestroyed()) {
 | 
				
			||||||
 | 
					                    navigateToReceipt(presenter.getTransaction());
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }, 3000);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showPaymentFailed(String reason) {
 | 
				
			||||||
 | 
					        Log.w(TAG, "❌ Payment failed: " + reason);
 | 
				
			||||||
 | 
					        showToast("Payment failed: " + reason);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showPaymentPending() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "⏳ Payment pending");
 | 
				
			||||||
 | 
					        showStatus("Payment pending...");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showLoading() {
 | 
				
			||||||
 | 
					        if (progressBar != null) {
 | 
				
			||||||
 | 
					            progressBar.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void hideLoading() {
 | 
				
			||||||
 | 
					        if (progressBar != null) {
 | 
				
			||||||
 | 
					            progressBar.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 | 
				
			||||||
 | 
					        super.onActivityResult(requestCode, resultCode, data);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (requestCode == 1001) { // Receipt activity result
 | 
				
			||||||
 | 
					            Log.d(TAG, "📄 Receipt activity finished, navigating to main");
 | 
				
			||||||
 | 
					            navigateToMainWithTransactionComplete();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private void navigateToMainWithTransactionComplete() {
 | 
				
			||||||
 | 
					        Intent intent = new Intent(this, com.example.bdkipoc.MainActivity.class);
 | 
				
			||||||
 | 
					        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Add transaction completion data
 | 
				
			||||||
 | 
					        intent.putExtra("transaction_completed", true);
 | 
				
			||||||
 | 
					        if (presenter != null && presenter.getTransaction() != null) {
 | 
				
			||||||
 | 
					            intent.putExtra("transaction_amount", String.valueOf(presenter.getTransaction().getOriginalAmount()));
 | 
				
			||||||
 | 
					            intent.putExtra("payment_provider", presenter.getTransaction().getDisplayProviderName());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        startActivity(intent);
 | 
				
			||||||
 | 
					        finishAffinity(); // Clear all activities in the task
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void navigateToReceipt(QrisTransaction transaction) {
 | 
				
			||||||
 | 
					        Log.d(TAG, "📄 Navigating to receipt");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Intent intent = new Intent(this, ReceiptActivity.class);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Put transaction data
 | 
				
			||||||
 | 
					        intent.putExtra("calling_activity", "QrisResultActivity");
 | 
				
			||||||
 | 
					        intent.putExtra("transaction_id", transaction.getTransactionId());
 | 
				
			||||||
 | 
					        intent.putExtra("reference_id", transaction.getReferenceId());
 | 
				
			||||||
 | 
					        intent.putExtra("order_id", transaction.getOrderId());
 | 
				
			||||||
 | 
					        intent.putExtra("transaction_amount", String.valueOf(transaction.getOriginalAmount()));
 | 
				
			||||||
 | 
					        intent.putExtra("gross_amount", transaction.getGrossAmount() != null ? transaction.getGrossAmount() : String.valueOf(transaction.getOriginalAmount()));
 | 
				
			||||||
 | 
					        intent.putExtra("created_at", getCurrentDateTime());
 | 
				
			||||||
 | 
					        intent.putExtra("transaction_date", getCurrentDateTime());
 | 
				
			||||||
 | 
					        intent.putExtra("payment_method", "QRIS");
 | 
				
			||||||
 | 
					        intent.putExtra("channel_code", "QRIS");
 | 
				
			||||||
 | 
					        intent.putExtra("channel_category", "RETAIL_OUTLET");
 | 
				
			||||||
 | 
					        intent.putExtra("card_type", transaction.getDisplayProviderName());
 | 
				
			||||||
 | 
					        intent.putExtra("merchant_name", "Marcel Panjaitan");
 | 
				
			||||||
 | 
					        intent.putExtra("merchant_location", "Jakarta, Indonesia");
 | 
				
			||||||
 | 
					        intent.putExtra("acquirer", transaction.getActualIssuer() != null ? transaction.getActualIssuer() : transaction.getAcquirer());
 | 
				
			||||||
 | 
					        intent.putExtra("mid", "71000026521");
 | 
				
			||||||
 | 
					        intent.putExtra("tid", "73001500");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Enhanced data
 | 
				
			||||||
 | 
					        intent.putExtra("detected_provider", transaction.getDetectedProvider());
 | 
				
			||||||
 | 
					        intent.putExtra("qr_expiration_minutes", transaction.getQrExpirationMinutes());
 | 
				
			||||||
 | 
					        intent.putExtra("was_qr_refresh_transaction", transaction.isQrRefreshTransaction());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // QR string
 | 
				
			||||||
 | 
					        if (transaction.getQrString() != null && !transaction.getQrString().isEmpty()) {
 | 
				
			||||||
 | 
					            intent.putExtra("qr_string", transaction.getQrString());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Add flag to automatically return to main after receipt
 | 
				
			||||||
 | 
					        intent.putExtra("auto_return_to_main", true);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        startActivityForResult(intent, 1001);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void navigateToMain() {
 | 
				
			||||||
 | 
					        Intent intent = new Intent(this, com.example.bdkipoc.MainActivity.class);
 | 
				
			||||||
 | 
					        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
 | 
				
			||||||
 | 
					        startActivity(intent);
 | 
				
			||||||
 | 
					        finishAffinity();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void finishActivity() {
 | 
				
			||||||
 | 
					        finish();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showToast(String message) {
 | 
				
			||||||
 | 
					        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void showError(String errorMessage) {
 | 
				
			||||||
 | 
					        Log.e(TAG, "❌ Error: " + errorMessage);
 | 
				
			||||||
 | 
					        Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void startSuccessAnimation() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🎬 Starting success animation");
 | 
				
			||||||
 | 
					        // Animation akan di-handle oleh showFullScreenSuccess
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void stopAllAnimations() {
 | 
				
			||||||
 | 
					        Log.d(TAG, "🛑 Stopping all animations");
 | 
				
			||||||
 | 
					        if (animationHandler != null) {
 | 
				
			||||||
 | 
					            animationHandler.removeCallbacksAndMessages(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // ========================================================================================
 | 
				
			||||||
 | 
					    // PRIVATE HELPER METHODS
 | 
				
			||||||
 | 
					    // ========================================================================================
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show full screen success dengan animations
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void showFullScreenSuccess(String providerName) {
 | 
				
			||||||
 | 
					        if (successScreen != null && !isFinishing()) {
 | 
				
			||||||
 | 
					            // Hide main UI components
 | 
				
			||||||
 | 
					            hideMainUIComponents();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Set success message
 | 
				
			||||||
 | 
					            if (successMessage != null) {
 | 
				
			||||||
 | 
					                successMessage.setText("Pembayaran " + providerName + " Berhasil");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Show success screen dengan fade in animation
 | 
				
			||||||
 | 
					            successScreen.setVisibility(View.VISIBLE);
 | 
				
			||||||
 | 
					            successScreen.setAlpha(0f);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Fade in background
 | 
				
			||||||
 | 
					            ObjectAnimator backgroundFadeIn = ObjectAnimator.ofFloat(successScreen, "alpha", 0f, 1f);
 | 
				
			||||||
 | 
					            backgroundFadeIn.setDuration(500);
 | 
				
			||||||
 | 
					            backgroundFadeIn.start();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Success icon animation
 | 
				
			||||||
 | 
					            if (successIcon != null) {
 | 
				
			||||||
 | 
					                successIcon.setScaleX(0f);
 | 
				
			||||||
 | 
					                successIcon.setScaleY(0f);
 | 
				
			||||||
 | 
					                successIcon.setAlpha(0f);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                ObjectAnimator scaleX = ObjectAnimator.ofFloat(successIcon, "scaleX", 0f, 1.2f, 1f);
 | 
				
			||||||
 | 
					                ObjectAnimator scaleY = ObjectAnimator.ofFloat(successIcon, "scaleY", 0f, 1.2f, 1f);
 | 
				
			||||||
 | 
					                ObjectAnimator iconFadeIn = ObjectAnimator.ofFloat(successIcon, "alpha", 0f, 1f);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                AnimatorSet iconAnimation = new AnimatorSet();
 | 
				
			||||||
 | 
					                iconAnimation.playTogether(scaleX, scaleY, iconFadeIn);
 | 
				
			||||||
 | 
					                iconAnimation.setDuration(800);
 | 
				
			||||||
 | 
					                iconAnimation.setStartDelay(300);
 | 
				
			||||||
 | 
					                iconAnimation.setInterpolator(new android.view.animation.OvershootInterpolator(1.2f));
 | 
				
			||||||
 | 
					                iconAnimation.start();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Success message animation
 | 
				
			||||||
 | 
					            if (successMessage != null) {
 | 
				
			||||||
 | 
					                successMessage.setAlpha(0f);
 | 
				
			||||||
 | 
					                successMessage.setTranslationY(50f);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                ObjectAnimator messageSlideUp = ObjectAnimator.ofFloat(successMessage, "translationY", 50f, 0f);
 | 
				
			||||||
 | 
					                ObjectAnimator messageFadeIn = ObjectAnimator.ofFloat(successMessage, "alpha", 0f, 1f);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                AnimatorSet messageAnimation = new AnimatorSet();
 | 
				
			||||||
 | 
					                messageAnimation.playTogether(messageSlideUp, messageFadeIn);
 | 
				
			||||||
 | 
					                messageAnimation.setDuration(600);
 | 
				
			||||||
 | 
					                messageAnimation.setStartDelay(600);
 | 
				
			||||||
 | 
					                messageAnimation.setInterpolator(new android.view.animation.DecelerateInterpolator());
 | 
				
			||||||
 | 
					                messageAnimation.start();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Hide main UI components untuk clean success screen
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void hideMainUIComponents() {
 | 
				
			||||||
 | 
					        if (mainCard != null) {
 | 
				
			||||||
 | 
					            mainCard.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (headerBackground != null) {
 | 
				
			||||||
 | 
					            headerBackground.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (backNavigation != null) {
 | 
				
			||||||
 | 
					            backNavigation.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (cancelButton != null) {
 | 
				
			||||||
 | 
					            cancelButton.setVisibility(View.GONE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Add click animation ke view
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private void addClickAnimation(View view) {
 | 
				
			||||||
 | 
					        ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f, 1f);
 | 
				
			||||||
 | 
					        ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f, 1f);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        AnimatorSet animatorSet = new AnimatorSet();
 | 
				
			||||||
 | 
					        animatorSet.playTogether(scaleX, scaleY);
 | 
				
			||||||
 | 
					        animatorSet.setDuration(150);
 | 
				
			||||||
 | 
					        animatorSet.start();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get current date time untuk receipt
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private String getCurrentDateTime() {
 | 
				
			||||||
 | 
					        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", new java.util.Locale("id", "ID"));
 | 
				
			||||||
 | 
					        return sdf.format(new java.util.Date());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // ========================================================================================
 | 
				
			||||||
 | 
					    // LIFECYCLE METHODS
 | 
				
			||||||
 | 
					    // ========================================================================================
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onDestroy() {
 | 
				
			||||||
 | 
					        super.onDestroy();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Cleanup presenter
 | 
				
			||||||
 | 
					        if (presenter != null) {
 | 
				
			||||||
 | 
					            presenter.onDestroy();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Cleanup animation handler
 | 
				
			||||||
 | 
					        if (animationHandler != null) {
 | 
				
			||||||
 | 
					            animationHandler.removeCallbacksAndMessages(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "💀 QrisResultActivity destroyed");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onPause() {
 | 
				
			||||||
 | 
					        super.onPause();
 | 
				
			||||||
 | 
					        Log.d(TAG, "⏸️ QrisResultActivity paused");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onResume() {
 | 
				
			||||||
 | 
					        super.onResume();
 | 
				
			||||||
 | 
					        Log.d(TAG, "▶️ QrisResultActivity resumed");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onBackPressed() {
 | 
				
			||||||
 | 
					        // Prevent back press during success screen animation
 | 
				
			||||||
 | 
					        if (successScreen != null && successScreen.getVisibility() == View.VISIBLE) {
 | 
				
			||||||
 | 
					            Log.d(TAG, "⬅️ Back press blocked during success screen");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Show confirmation dialog before leaving
 | 
				
			||||||
 | 
					        androidx.appcompat.app.AlertDialog.Builder builder = new androidx.appcompat.app.AlertDialog.Builder(this);
 | 
				
			||||||
 | 
					        builder.setTitle("Batalkan Transaksi");
 | 
				
			||||||
 | 
					        builder.setMessage("Apakah Anda yakin ingin membatalkan transaksi ini?");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        builder.setPositiveButton("Ya, Batalkan", (dialog, which) -> {
 | 
				
			||||||
 | 
					            if (presenter != null) {
 | 
				
			||||||
 | 
					                presenter.onBackPressed();
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                super.onBackPressed();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        builder.setNegativeButton("Tidak", (dialog, which) -> {
 | 
				
			||||||
 | 
					            dialog.dismiss();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        builder.show();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.qris.view;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.qris.model.QrisTransaction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Contract interface untuk QrisResult module
 | 
				
			||||||
 | 
					 * Mendefinisikan komunikasi antara View dan Presenter
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public interface QrisResultContract {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * View interface - apa yang bisa dilakukan oleh View (Activity)
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    interface View {
 | 
				
			||||||
 | 
					        // UI Display methods
 | 
				
			||||||
 | 
					        void showQrImage(String qrImageUrl);
 | 
				
			||||||
 | 
					        void showAmount(String formattedAmount);
 | 
				
			||||||
 | 
					        void showTimer(String timeDisplay);
 | 
				
			||||||
 | 
					        void showStatus(String status);
 | 
				
			||||||
 | 
					        void showProviderName(String providerName);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // QR Management
 | 
				
			||||||
 | 
					        void updateQrImage(String newQrImageUrl);
 | 
				
			||||||
 | 
					        void updateQrUrl(String newQrUrl);
 | 
				
			||||||
 | 
					        void showQrExpired();
 | 
				
			||||||
 | 
					        void showQrRefreshing();
 | 
				
			||||||
 | 
					        void showQrRefreshFailed(String errorMessage);
 | 
				
			||||||
 | 
					        void showQrRefreshSuccess();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Payment Status
 | 
				
			||||||
 | 
					        void showPaymentSuccess(String providerName);
 | 
				
			||||||
 | 
					        void showPaymentFailed(String reason);
 | 
				
			||||||
 | 
					        void showPaymentPending();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Loading states
 | 
				
			||||||
 | 
					        void showLoading();
 | 
				
			||||||
 | 
					        void hideLoading();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Navigation
 | 
				
			||||||
 | 
					        void navigateToReceipt(QrisTransaction transaction);
 | 
				
			||||||
 | 
					        void navigateToMain();
 | 
				
			||||||
 | 
					        void finishActivity();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // User feedback
 | 
				
			||||||
 | 
					        void showToast(String message);
 | 
				
			||||||
 | 
					        void showError(String errorMessage);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Animation
 | 
				
			||||||
 | 
					        void startSuccessAnimation();
 | 
				
			||||||
 | 
					        void stopAllAnimations();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Presenter interface - apa yang bisa dilakukan oleh Presenter
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    interface Presenter {
 | 
				
			||||||
 | 
					        // Lifecycle
 | 
				
			||||||
 | 
					        void attachView(View view);
 | 
				
			||||||
 | 
					        void detachView();
 | 
				
			||||||
 | 
					        void onDestroy();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Initialization
 | 
				
			||||||
 | 
					        void initializeTransaction(String orderId, String transactionId, String amount, 
 | 
				
			||||||
 | 
					                                 String qrImageUrl, String qrString, String acquirer);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // QR Management
 | 
				
			||||||
 | 
					        void startQrManagement();
 | 
				
			||||||
 | 
					        void stopQrManagement();
 | 
				
			||||||
 | 
					        void refreshQrCode();
 | 
				
			||||||
 | 
					        void onQrExpired();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Payment Monitoring
 | 
				
			||||||
 | 
					        void startPaymentMonitoring();
 | 
				
			||||||
 | 
					        void stopPaymentMonitoring();
 | 
				
			||||||
 | 
					        void checkPaymentStatus();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // User Actions
 | 
				
			||||||
 | 
					        void onCancelClicked();
 | 
				
			||||||
 | 
					        void onBackPressed();
 | 
				
			||||||
 | 
					        void onSimulatePayment(); // For testing
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Timer
 | 
				
			||||||
 | 
					        void startTimer();
 | 
				
			||||||
 | 
					        void stopTimer();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,381 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.settlement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.os.AsyncTask;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.view.LayoutInflater;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.view.ViewGroup;
 | 
				
			||||||
 | 
					import android.widget.ImageView;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					import android.widget.Button;
 | 
				
			||||||
 | 
					import android.content.Intent;
 | 
				
			||||||
 | 
					import androidx.annotation.NonNull;
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.LinearLayoutManager;
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||||
 | 
					import org.json.JSONArray;
 | 
				
			||||||
 | 
					import org.json.JSONException;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat;
 | 
				
			||||||
 | 
					import java.text.NumberFormat;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Date;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.BuildConfig;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class SettlementActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private TextView tvTotalAmount;
 | 
				
			||||||
 | 
					    private TextView tvTotalTransactions;
 | 
				
			||||||
 | 
					    private RecyclerView recyclerView;
 | 
				
			||||||
 | 
					    private SettlementAdapter adapter;
 | 
				
			||||||
 | 
					    private List<SettlementItem> settlementList;
 | 
				
			||||||
 | 
					    private LinearLayout backNavigation;
 | 
				
			||||||
 | 
					    private Button btnContinue;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_settlement);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        initViews();
 | 
				
			||||||
 | 
					        setupRecyclerView();
 | 
				
			||||||
 | 
					        fetchApiData();
 | 
				
			||||||
 | 
					        setupClickListeners();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void fetchApiData() {
 | 
				
			||||||
 | 
					        // Get current date in yyyy-MM-dd format
 | 
				
			||||||
 | 
					        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
 | 
				
			||||||
 | 
					        String currentDate = sdf.format(new Date());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Build API URL with current date and credentials from BuildConfig
 | 
				
			||||||
 | 
					        String apiUrl = BuildConfig.BACKEND_BASE_URL + "/transactions/performa-chanel-pembayaran" +
 | 
				
			||||||
 | 
					                "?from_date=" + currentDate + 
 | 
				
			||||||
 | 
					                "&to_date=" + currentDate + 
 | 
				
			||||||
 | 
					                "&location_id=0&merchant_id=0";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Execute network call in background thread
 | 
				
			||||||
 | 
					        new ApiTask().execute(apiUrl);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void processApiData(JSONArray dataArray) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            settlementList.clear();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            final long[] totalAmountArray = {0}; // Using array to make it effectively final
 | 
				
			||||||
 | 
					            final int[] totalTransactionsArray = {0}; // Using array to make it effectively final
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Process each channel individually (no grouping)
 | 
				
			||||||
 | 
					            for (int i = 0; i < dataArray.length(); i++) {
 | 
				
			||||||
 | 
					                JSONObject item = dataArray.getJSONObject(i);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String channelCode = item.getString("channel_code");
 | 
				
			||||||
 | 
					                int transactions = item.getInt("total_transactions");
 | 
				
			||||||
 | 
					                long maxAmount = item.getLong("max_transastions");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Use channel code directly as display name with some formatting
 | 
				
			||||||
 | 
					                String displayName = formatChannelName(channelCode);
 | 
				
			||||||
 | 
					                int iconResource = getChannelIcon(channelCode);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                settlementList.add(new SettlementItem(
 | 
				
			||||||
 | 
					                    displayName,
 | 
				
			||||||
 | 
					                    maxAmount,
 | 
				
			||||||
 | 
					                    transactions,
 | 
				
			||||||
 | 
					                    iconResource
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                totalAmountArray[0] += maxAmount;
 | 
				
			||||||
 | 
					                totalTransactionsArray[0] += transactions;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Update UI on main thread
 | 
				
			||||||
 | 
					            runOnUiThread(new Runnable() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void run() {
 | 
				
			||||||
 | 
					                    updateSummary(totalAmountArray[0], totalTransactionsArray[0]);
 | 
				
			||||||
 | 
					                    adapter.notifyDataSetChanged();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (JSONException e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					            runOnUiThread(new Runnable() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void run() {
 | 
				
			||||||
 | 
					                    Toast.makeText(SettlementActivity.this, "Error parsing data", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    loadSampleData(); // Fallback to sample data
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void updateSummary(long totalAmount, int totalTransactions) {
 | 
				
			||||||
 | 
					        tvTotalAmount.setText(formatCurrency(totalAmount));
 | 
				
			||||||
 | 
					        tvTotalTransactions.setText(String.valueOf(totalTransactions));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void initViews() {
 | 
				
			||||||
 | 
					        tvTotalAmount = findViewById(R.id.tv_total_amount);
 | 
				
			||||||
 | 
					        tvTotalTransactions = findViewById(R.id.tv_total_transactions);
 | 
				
			||||||
 | 
					        recyclerView = findViewById(R.id.recycler_view);
 | 
				
			||||||
 | 
					        backNavigation = findViewById(R.id.back_navigation);
 | 
				
			||||||
 | 
					        btnContinue = findViewById(R.id.btn_continue);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        settlementList = new ArrayList<>();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupRecyclerView() {
 | 
				
			||||||
 | 
					        adapter = new SettlementAdapter(settlementList);
 | 
				
			||||||
 | 
					        recyclerView.setLayoutManager(new LinearLayoutManager(this));
 | 
				
			||||||
 | 
					        recyclerView.setAdapter(adapter);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupClickListeners() {
 | 
				
			||||||
 | 
					        // Updated to use backNavigation instead of btnBack
 | 
				
			||||||
 | 
					        backNavigation.setOnClickListener(new View.OnClickListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onClick(View v) {
 | 
				
			||||||
 | 
					                finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        btnContinue.setOnClickListener(new View.OnClickListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onClick(View v) {
 | 
				
			||||||
 | 
					                Intent intent = new Intent(SettlementActivity.this, SettlementDetailActivity.class);
 | 
				
			||||||
 | 
					                startActivity(intent);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void loadSampleData() {
 | 
				
			||||||
 | 
					        // Sample data as fallback
 | 
				
			||||||
 | 
					        settlementList.clear();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        settlementList.add(new SettlementItem("Kartu Kredit", 200000, 13, android.R.drawable.ic_menu_recent_history));
 | 
				
			||||||
 | 
					        settlementList.add(new SettlementItem("Kartu Debit", 200000, 13, android.R.drawable.ic_menu_manage));
 | 
				
			||||||
 | 
					        settlementList.add(new SettlementItem("Transfer", 200000, 13, android.R.drawable.ic_menu_send));
 | 
				
			||||||
 | 
					        settlementList.add(new SettlementItem("Uang Elektronik", 200000, 13, android.R.drawable.ic_menu_gallery));
 | 
				
			||||||
 | 
					        settlementList.add(new SettlementItem("QRIS", 200000, 13, android.R.drawable.ic_menu_camera));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Update summary
 | 
				
			||||||
 | 
					        tvTotalAmount.setText(formatCurrency(3506500));
 | 
				
			||||||
 | 
					        tvTotalTransactions.setText("65");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        adapter.notifyDataSetChanged();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatChannelName(String channelCode) {
 | 
				
			||||||
 | 
					        // Format channel code to be more readable
 | 
				
			||||||
 | 
					        switch (channelCode) {
 | 
				
			||||||
 | 
					            case "GO-PAY":
 | 
				
			||||||
 | 
					                return "GoPay";
 | 
				
			||||||
 | 
					            case "SHOPEEPAY":
 | 
				
			||||||
 | 
					                return "ShopeePay";
 | 
				
			||||||
 | 
					            case "LINKAJA":
 | 
				
			||||||
 | 
					                return "LinkAja";
 | 
				
			||||||
 | 
					            case "MASTERCARD":
 | 
				
			||||||
 | 
					                return "Mastercard";
 | 
				
			||||||
 | 
					            case "VISA":
 | 
				
			||||||
 | 
					                return "Visa";
 | 
				
			||||||
 | 
					            case "QRIS":
 | 
				
			||||||
 | 
					                return "QRIS";
 | 
				
			||||||
 | 
					            case "DANA":
 | 
				
			||||||
 | 
					                return "Dana";
 | 
				
			||||||
 | 
					            case "OVO":
 | 
				
			||||||
 | 
					                return "OVO";
 | 
				
			||||||
 | 
					            case "DEBIT":
 | 
				
			||||||
 | 
					                return "Kartu Debit";
 | 
				
			||||||
 | 
					            case "GPN":
 | 
				
			||||||
 | 
					                return "GPN";
 | 
				
			||||||
 | 
					            case "OTHER":
 | 
				
			||||||
 | 
					                return "Lainnya";
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                // Capitalize first letter and make rest lowercase
 | 
				
			||||||
 | 
					                return channelCode.substring(0, 1).toUpperCase() + 
 | 
				
			||||||
 | 
					                       channelCode.substring(1).toLowerCase();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String getChannelDisplayName(String channelCode) {
 | 
				
			||||||
 | 
					        // Deprecated - keeping for backward compatibility
 | 
				
			||||||
 | 
					        return formatChannelName(channelCode);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private int getChannelIcon(String channelCode) {
 | 
				
			||||||
 | 
					        // Dynamic icon assignment based on channel type
 | 
				
			||||||
 | 
					        switch (channelCode) {
 | 
				
			||||||
 | 
					            case "DEBIT":
 | 
				
			||||||
 | 
					                return android.R.drawable.ic_menu_manage;
 | 
				
			||||||
 | 
					            case "VISA":
 | 
				
			||||||
 | 
					            case "MASTERCARD":
 | 
				
			||||||
 | 
					                return android.R.drawable.ic_menu_recent_history;
 | 
				
			||||||
 | 
					            case "QRIS":
 | 
				
			||||||
 | 
					                return android.R.drawable.ic_menu_camera;
 | 
				
			||||||
 | 
					            case "DANA":
 | 
				
			||||||
 | 
					            case "GO-PAY":
 | 
				
			||||||
 | 
					            case "OVO":
 | 
				
			||||||
 | 
					            case "SHOPEEPAY":
 | 
				
			||||||
 | 
					            case "LINKAJA":
 | 
				
			||||||
 | 
					                return android.R.drawable.ic_menu_gallery;
 | 
				
			||||||
 | 
					            case "GPN":
 | 
				
			||||||
 | 
					                return android.R.drawable.ic_menu_send;
 | 
				
			||||||
 | 
					            case "OTHER":
 | 
				
			||||||
 | 
					                return android.R.drawable.ic_menu_info_details;
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                return android.R.drawable.ic_menu_help;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatCurrency(long amount) {
 | 
				
			||||||
 | 
					        NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
 | 
				
			||||||
 | 
					        return formatter.format(amount);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // AsyncTask for API call
 | 
				
			||||||
 | 
					    private class ApiTask extends AsyncTask<String, Void, String> {
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected String doInBackground(String... urls) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                URL url = new URL(urls[0]);
 | 
				
			||||||
 | 
					                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                connection.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(5000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(5000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Add authorization header if needed
 | 
				
			||||||
 | 
					                // connection.setRequestProperty("Authorization", BuildConfig.MIDTRANS_SANDBOX_AUTH);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                if (responseCode == HttpURLConnection.HTTP_OK) {
 | 
				
			||||||
 | 
					                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(line);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    reader.close();
 | 
				
			||||||
 | 
					                    return response.toString();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (IOException e) {
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected void onPostExecute(String result) {
 | 
				
			||||||
 | 
					            if (result != null) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    JSONObject jsonResponse = new JSONObject(result);
 | 
				
			||||||
 | 
					                    if (jsonResponse.getInt("status") == 200) {
 | 
				
			||||||
 | 
					                        JSONArray dataArray = jsonResponse.getJSONArray("data");
 | 
				
			||||||
 | 
					                        processApiData(dataArray);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        Toast.makeText(SettlementActivity.this, "API Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                        loadSampleData();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } catch (JSONException e) {
 | 
				
			||||||
 | 
					                    e.printStackTrace();
 | 
				
			||||||
 | 
					                    Toast.makeText(SettlementActivity.this, "JSON Parse Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    loadSampleData();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Toast.makeText(SettlementActivity.this, "Network Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                loadSampleData();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SettlementItem class - combined in same file
 | 
				
			||||||
 | 
					class SettlementItem {
 | 
				
			||||||
 | 
					    private String channelName;
 | 
				
			||||||
 | 
					    private long amount;
 | 
				
			||||||
 | 
					    private int transactionCount;
 | 
				
			||||||
 | 
					    private int iconResource;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public SettlementItem() {}
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public SettlementItem(String channelName, long amount, int transactionCount, int iconResource) {
 | 
				
			||||||
 | 
					        this.channelName = channelName;
 | 
				
			||||||
 | 
					        this.amount = amount;
 | 
				
			||||||
 | 
					        this.transactionCount = transactionCount;
 | 
				
			||||||
 | 
					        this.iconResource = iconResource;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Getters and Setters
 | 
				
			||||||
 | 
					    public String getChannelName() { return channelName; }
 | 
				
			||||||
 | 
					    public void setChannelName(String channelName) { this.channelName = channelName; }
 | 
				
			||||||
 | 
					    public long getAmount() { return amount; }
 | 
				
			||||||
 | 
					    public void setAmount(long amount) { this.amount = amount; }
 | 
				
			||||||
 | 
					    public int getTransactionCount() { return transactionCount; }
 | 
				
			||||||
 | 
					    public void setTransactionCount(int transactionCount) { this.transactionCount = transactionCount; }
 | 
				
			||||||
 | 
					    public int getIconResource() { return iconResource; }
 | 
				
			||||||
 | 
					    public void setIconResource(int iconResource) { this.iconResource = iconResource; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SettlementAdapter class - combined in same file
 | 
				
			||||||
 | 
					class SettlementAdapter extends RecyclerView.Adapter<SettlementAdapter.SettlementViewHolder> {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private List<SettlementItem> settlementList;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public SettlementAdapter(List<SettlementItem> settlementList) {
 | 
				
			||||||
 | 
					        this.settlementList = settlementList;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @NonNull
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public SettlementViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 | 
				
			||||||
 | 
					        View view = LayoutInflater.from(parent.getContext())
 | 
				
			||||||
 | 
					                .inflate(R.layout.item_settlement, parent, false);
 | 
				
			||||||
 | 
					        return new SettlementViewHolder(view);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onBindViewHolder(@NonNull SettlementViewHolder holder, int position) {
 | 
				
			||||||
 | 
					        SettlementItem item = settlementList.get(position);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        holder.ivIcon.setImageResource(item.getIconResource());
 | 
				
			||||||
 | 
					        holder.tvChannelName.setText(item.getChannelName());
 | 
				
			||||||
 | 
					        holder.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
 | 
				
			||||||
 | 
					        holder.tvTransactionCount.setText(item.getTransactionCount() + " Transaksi");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public int getItemCount() {
 | 
				
			||||||
 | 
					        return settlementList.size();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatCurrency(long amount) {
 | 
				
			||||||
 | 
					        NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
 | 
				
			||||||
 | 
					        return formatter.format(amount);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    static class SettlementViewHolder extends RecyclerView.ViewHolder {
 | 
				
			||||||
 | 
					        ImageView ivIcon;
 | 
				
			||||||
 | 
					        TextView tvChannelName;
 | 
				
			||||||
 | 
					        TextView tvAmount;
 | 
				
			||||||
 | 
					        TextView tvTransactionCount;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public SettlementViewHolder(@NonNull View itemView) {
 | 
				
			||||||
 | 
					            super(itemView);
 | 
				
			||||||
 | 
					            ivIcon = itemView.findViewById(R.id.iv_icon);
 | 
				
			||||||
 | 
					            tvChannelName = itemView.findViewById(R.id.tv_channel_name);
 | 
				
			||||||
 | 
					            tvAmount = itemView.findViewById(R.id.tv_amount);
 | 
				
			||||||
 | 
					            tvTransactionCount = itemView.findViewById(R.id.tv_transaction_count);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,403 @@
 | 
				
			|||||||
 | 
					package com.example.bdkipoc.settlement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.os.AsyncTask;
 | 
				
			||||||
 | 
					import android.os.Bundle;
 | 
				
			||||||
 | 
					import android.view.LayoutInflater;
 | 
				
			||||||
 | 
					import android.view.View;
 | 
				
			||||||
 | 
					import android.view.ViewGroup;
 | 
				
			||||||
 | 
					import android.widget.Button;
 | 
				
			||||||
 | 
					import android.widget.LinearLayout;
 | 
				
			||||||
 | 
					import android.widget.TextView;
 | 
				
			||||||
 | 
					import android.widget.Toast;
 | 
				
			||||||
 | 
					import androidx.annotation.NonNull;
 | 
				
			||||||
 | 
					import androidx.appcompat.app.AppCompatActivity;
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.LinearLayoutManager;
 | 
				
			||||||
 | 
					import androidx.recyclerview.widget.RecyclerView;
 | 
				
			||||||
 | 
					import org.json.JSONArray;
 | 
				
			||||||
 | 
					import org.json.JSONException;
 | 
				
			||||||
 | 
					import org.json.JSONObject;
 | 
				
			||||||
 | 
					import java.io.BufferedReader;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.InputStreamReader;
 | 
				
			||||||
 | 
					import java.net.HttpURLConnection;
 | 
				
			||||||
 | 
					import java.net.URL;
 | 
				
			||||||
 | 
					import java.text.NumberFormat;
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Date;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Locale;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.example.bdkipoc.BuildConfig;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.LoginActivity;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.R;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class SettlementDetailActivity extends AppCompatActivity {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private TextView tvStoreName, tvStoreLocation, tvMid, tvTid;
 | 
				
			||||||
 | 
					    private TextView tvSettlementDate, tvSettlementTime;
 | 
				
			||||||
 | 
					    private TextView tvTotalMasuk, tvTotalKeluar, tvBiayaAdmin, tvGrandTotal;
 | 
				
			||||||
 | 
					    private RecyclerView recyclerView;
 | 
				
			||||||
 | 
					    private SettlementDetailAdapter adapter;
 | 
				
			||||||
 | 
					    private List<SettlementDetailItem> settlementDetailList;
 | 
				
			||||||
 | 
					    private LinearLayout backNavigation;
 | 
				
			||||||
 | 
					    private Button btnSendSettlement;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Summary totals
 | 
				
			||||||
 | 
					    private long totalMasuk = 0; // Set to 0
 | 
				
			||||||
 | 
					    private long totalKeluar = 0;
 | 
				
			||||||
 | 
					    private long biayaAdmin = 0; // Set to 0
 | 
				
			||||||
 | 
					    private long grandTotal = 0;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // User data
 | 
				
			||||||
 | 
					    private JSONObject userData;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void onCreate(Bundle savedInstanceState) {
 | 
				
			||||||
 | 
					        super.onCreate(savedInstanceState);
 | 
				
			||||||
 | 
					        setContentView(R.layout.activity_settlement_detail);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Load user data from login session
 | 
				
			||||||
 | 
					        loadUserData();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        initViews();
 | 
				
			||||||
 | 
					        setupRecyclerView();
 | 
				
			||||||
 | 
					        fetchApiData();
 | 
				
			||||||
 | 
					        setupClickListeners();
 | 
				
			||||||
 | 
					        updateDateTime();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void loadUserData() {
 | 
				
			||||||
 | 
					        userData = LoginActivity.getUserDataAsJson(this);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void fetchApiData() {
 | 
				
			||||||
 | 
					        // Get current date in yyyy-MM-dd format
 | 
				
			||||||
 | 
					        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
 | 
				
			||||||
 | 
					        String currentDate = sdf.format(new Date());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Build API URL with current date
 | 
				
			||||||
 | 
					        String apiUrl = BuildConfig.BACKEND_BASE_URL + "/transactions/performa-chanel-pembayaran" +
 | 
				
			||||||
 | 
					                "?from_date=" + currentDate + 
 | 
				
			||||||
 | 
					                "&to_date=" + currentDate + 
 | 
				
			||||||
 | 
					                "&location_id=0&merchant_id=0";
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        new ApiTask().execute(apiUrl);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void processApiData(JSONArray dataArray) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            settlementDetailList.clear();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            long totalAmount = 0;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for (int i = 0; i < dataArray.length(); i++) {
 | 
				
			||||||
 | 
					                JSONObject item = dataArray.getJSONObject(i);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String channelCode = item.getString("channel_code");
 | 
				
			||||||
 | 
					                int transactions = item.getInt("total_transactions");
 | 
				
			||||||
 | 
					                long maxAmount = item.getLong("max_transastions");
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                String displayName = formatChannelName(channelCode);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                settlementDetailList.add(new SettlementDetailItem(
 | 
				
			||||||
 | 
					                    displayName,
 | 
				
			||||||
 | 
					                    maxAmount,
 | 
				
			||||||
 | 
					                    transactions
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                totalAmount += maxAmount;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            calculateTotalsFromApiData(totalAmount);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            runOnUiThread(new Runnable() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void run() {
 | 
				
			||||||
 | 
					                    adapter.notifyDataSetChanged();
 | 
				
			||||||
 | 
					                    updateSummary();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (JSONException e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					            runOnUiThread(new Runnable() {
 | 
				
			||||||
 | 
					                @Override
 | 
				
			||||||
 | 
					                public void run() {
 | 
				
			||||||
 | 
					                    Toast.makeText(SettlementDetailActivity.this, "Error parsing data", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    loadSampleData();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void calculateTotalsFromApiData(long totalAmount) {
 | 
				
			||||||
 | 
					        totalMasuk = 0; // Set to 0 temporarily
 | 
				
			||||||
 | 
					        totalKeluar = totalAmount;
 | 
				
			||||||
 | 
					        biayaAdmin = 0; // Set to 0 temporarily
 | 
				
			||||||
 | 
					        grandTotal = totalKeluar - totalMasuk - biayaAdmin;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatChannelName(String channelCode) {
 | 
				
			||||||
 | 
					        switch (channelCode) {
 | 
				
			||||||
 | 
					            case "GO-PAY": return "GoPay";
 | 
				
			||||||
 | 
					            case "SHOPEEPAY": return "ShopeePay";
 | 
				
			||||||
 | 
					            case "LINKAJA": return "LinkAja";
 | 
				
			||||||
 | 
					            case "MASTERCARD": return "Mastercard";
 | 
				
			||||||
 | 
					            case "VISA": return "Visa";
 | 
				
			||||||
 | 
					            case "QRIS": return "QRIS";
 | 
				
			||||||
 | 
					            case "DANA": return "Dana";
 | 
				
			||||||
 | 
					            case "OVO": return "OVO";
 | 
				
			||||||
 | 
					            case "DEBIT": return "Kartu Debit";
 | 
				
			||||||
 | 
					            case "GPN": return "GPN";
 | 
				
			||||||
 | 
					            case "OTHER": return "Lainnya";
 | 
				
			||||||
 | 
					            case "CREDIT": return "Kartu Kredit";
 | 
				
			||||||
 | 
					            case "TRANSFER": return "Transfer";
 | 
				
			||||||
 | 
					            case "E_MONEY": return "Uang Elektronik";
 | 
				
			||||||
 | 
					            case "CASH_DEPOSIT": return "Setoran Tunai";
 | 
				
			||||||
 | 
					            case "BILL_PAYMENT": return "Pembayaran Tagihan";
 | 
				
			||||||
 | 
					            case "CASH_WITHDRAWAL": return "Penarikan Tunai";
 | 
				
			||||||
 | 
					            case "TOP_UP": return "Top-up Saldo";
 | 
				
			||||||
 | 
					            case "REFUND": return "Refund (void)";
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                return channelCode.substring(0, 1).toUpperCase() + 
 | 
				
			||||||
 | 
					                       channelCode.substring(1).toLowerCase();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void initViews() {
 | 
				
			||||||
 | 
					        tvStoreName = findViewById(R.id.tv_store_name);
 | 
				
			||||||
 | 
					        tvStoreLocation = findViewById(R.id.tv_store_location);
 | 
				
			||||||
 | 
					        tvMid = findViewById(R.id.tv_mid);
 | 
				
			||||||
 | 
					        tvTid = findViewById(R.id.tv_tid);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        tvSettlementDate = findViewById(R.id.tv_settlement_date);
 | 
				
			||||||
 | 
					        tvSettlementTime = findViewById(R.id.tv_settlement_time);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        tvTotalMasuk = findViewById(R.id.tv_total_masuk);
 | 
				
			||||||
 | 
					        tvTotalKeluar = findViewById(R.id.tv_total_keluar);
 | 
				
			||||||
 | 
					        tvBiayaAdmin = findViewById(R.id.tv_biaya_admin);
 | 
				
			||||||
 | 
					        tvGrandTotal = findViewById(R.id.tv_grand_total);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        recyclerView = findViewById(R.id.recycler_settlement_details);
 | 
				
			||||||
 | 
					        backNavigation = findViewById(R.id.back_navigation);
 | 
				
			||||||
 | 
					        btnSendSettlement = findViewById(R.id.btn_send_settlement);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        settlementDetailList = new ArrayList<>();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupRecyclerView() {
 | 
				
			||||||
 | 
					        adapter = new SettlementDetailAdapter(settlementDetailList);
 | 
				
			||||||
 | 
					        recyclerView.setLayoutManager(new LinearLayoutManager(this));
 | 
				
			||||||
 | 
					        recyclerView.setAdapter(adapter);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void loadSampleData() {
 | 
				
			||||||
 | 
					        settlementDetailList.clear();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("Kartu Kredit", 200000, 14));
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("Kartu Debit", 200000, 14));
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("QRIS", 200000, 14));
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("Transfer", 200000, 14));
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("Uang Elektronik", 200000, 14));
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("Setoran Tunai", 200000, 14));
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("Pembayaran Tagihan", 200000, 14));
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("Penarikan Tunai", 200000, 14));
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("Top-up Saldo", 200000, 14));
 | 
				
			||||||
 | 
					        settlementDetailList.add(new SettlementDetailItem("Refund (void)", 200000, 14));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        totalMasuk = 0; // Set to 0 temporarily
 | 
				
			||||||
 | 
					        totalKeluar = 2000000; // Total from sample data
 | 
				
			||||||
 | 
					        biayaAdmin = 0; // Set to 0 temporarily
 | 
				
			||||||
 | 
					        grandTotal = totalKeluar - totalMasuk - biayaAdmin;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        adapter.notifyDataSetChanged();
 | 
				
			||||||
 | 
					        updateSummary();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void updateSummary() {
 | 
				
			||||||
 | 
					        tvTotalMasuk.setText("Rp " + formatCurrency(totalMasuk));
 | 
				
			||||||
 | 
					        tvTotalKeluar.setText("Rp " + formatCurrency(totalKeluar));
 | 
				
			||||||
 | 
					        tvBiayaAdmin.setText("Rp " + formatCurrency(biayaAdmin));
 | 
				
			||||||
 | 
					        tvGrandTotal.setText(formatCurrency(grandTotal));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void updateDateTime() {
 | 
				
			||||||
 | 
					        SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy", new Locale("id", "ID"));
 | 
				
			||||||
 | 
					        SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Date now = new Date();
 | 
				
			||||||
 | 
					        tvSettlementDate.setText(dateFormat.format(now));
 | 
				
			||||||
 | 
					        tvSettlementTime.setText(timeFormat.format(now));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Set store info from user data
 | 
				
			||||||
 | 
					        if (userData != null) {
 | 
				
			||||||
 | 
					            String storeName = userData.optString("store_name", "TOKO KLONTONG PAK EKO");
 | 
				
			||||||
 | 
					            String storeAddress = userData.optString("store_address", "Ciputat Baru, Tangsel");
 | 
				
			||||||
 | 
					            String mid = userData.optString("mid", "12345678901");
 | 
				
			||||||
 | 
					            String tid = userData.optString("tid", "12345678901");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            tvStoreName.setText(storeName);
 | 
				
			||||||
 | 
					            tvStoreLocation.setText(storeAddress);
 | 
				
			||||||
 | 
					            tvMid.setText(mid);
 | 
				
			||||||
 | 
					            tvTid.setText(tid);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Fallback to default values
 | 
				
			||||||
 | 
					            tvStoreName.setText("TOKO KLONTONG PAK EKO");
 | 
				
			||||||
 | 
					            tvStoreLocation.setText("Ciputat Baru, Tangsel");
 | 
				
			||||||
 | 
					            tvMid.setText("12345678901");
 | 
				
			||||||
 | 
					            tvTid.setText("12345678901");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void setupClickListeners() {
 | 
				
			||||||
 | 
					        backNavigation.setOnClickListener(new View.OnClickListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onClick(View v) {
 | 
				
			||||||
 | 
					                finish();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        btnSendSettlement.setOnClickListener(new View.OnClickListener() {
 | 
				
			||||||
 | 
					            @Override
 | 
				
			||||||
 | 
					            public void onClick(View v) {
 | 
				
			||||||
 | 
					                sendSettlement();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private void sendSettlement() {
 | 
				
			||||||
 | 
					        Toast.makeText(this, "Mengirim settlement...", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // TODO: Implement actual settlement sending logic
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Toast.makeText(this, "Settlement berhasil dikirim!", Toast.LENGTH_LONG).show();
 | 
				
			||||||
 | 
					        finish();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatCurrency(long amount) {
 | 
				
			||||||
 | 
					        NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
 | 
				
			||||||
 | 
					        return formatter.format(amount);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private class ApiTask extends AsyncTask<String, Void, String> {
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected String doInBackground(String... urls) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                URL url = new URL(urls[0]);
 | 
				
			||||||
 | 
					                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 | 
				
			||||||
 | 
					                connection.setRequestMethod("GET");
 | 
				
			||||||
 | 
					                connection.setConnectTimeout(5000);
 | 
				
			||||||
 | 
					                connection.setReadTimeout(5000);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                int responseCode = connection.getResponseCode();
 | 
				
			||||||
 | 
					                if (responseCode == HttpURLConnection.HTTP_OK) {
 | 
				
			||||||
 | 
					                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
 | 
				
			||||||
 | 
					                    StringBuilder response = new StringBuilder();
 | 
				
			||||||
 | 
					                    String line;
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    while ((line = reader.readLine()) != null) {
 | 
				
			||||||
 | 
					                        response.append(line);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    reader.close();
 | 
				
			||||||
 | 
					                    return response.toString();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (IOException e) {
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        protected void onPostExecute(String result) {
 | 
				
			||||||
 | 
					            if (result != null) {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    JSONObject jsonResponse = new JSONObject(result);
 | 
				
			||||||
 | 
					                    if (jsonResponse.getInt("status") == 200) {
 | 
				
			||||||
 | 
					                        JSONArray dataArray = jsonResponse.getJSONArray("data");
 | 
				
			||||||
 | 
					                        processApiData(dataArray);
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        Toast.makeText(SettlementDetailActivity.this, "API Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                        loadSampleData();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } catch (JSONException e) {
 | 
				
			||||||
 | 
					                    e.printStackTrace();
 | 
				
			||||||
 | 
					                    Toast.makeText(SettlementDetailActivity.this, "JSON Parse Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                    loadSampleData();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                Toast.makeText(SettlementDetailActivity.this, "Network Error", Toast.LENGTH_SHORT).show();
 | 
				
			||||||
 | 
					                loadSampleData();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SettlementDetailItem {
 | 
				
			||||||
 | 
					    private String paymentMethod;
 | 
				
			||||||
 | 
					    private long totalNominal;
 | 
				
			||||||
 | 
					    private int jumlahTransaksi;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public SettlementDetailItem(String paymentMethod, long totalNominal, int jumlahTransaksi) {
 | 
				
			||||||
 | 
					        this.paymentMethod = paymentMethod;
 | 
				
			||||||
 | 
					        this.totalNominal = totalNominal;
 | 
				
			||||||
 | 
					        this.jumlahTransaksi = jumlahTransaksi;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public String getPaymentMethod() { return paymentMethod; }
 | 
				
			||||||
 | 
					    public long getTotalNominal() { return totalNominal; }
 | 
				
			||||||
 | 
					    public int getJumlahTransaksi() { return jumlahTransaksi; }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public void setPaymentMethod(String paymentMethod) { this.paymentMethod = paymentMethod; }
 | 
				
			||||||
 | 
					    public void setTotalNominal(long totalNominal) { this.totalNominal = totalNominal; }
 | 
				
			||||||
 | 
					    public void setJumlahTransaksi(int jumlahTransaksi) { this.jumlahTransaksi = jumlahTransaksi; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SettlementDetailAdapter extends RecyclerView.Adapter<SettlementDetailAdapter.SettlementDetailViewHolder> {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private List<SettlementDetailItem> settlementDetailList;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public SettlementDetailAdapter(List<SettlementDetailItem> settlementDetailList) {
 | 
				
			||||||
 | 
					        this.settlementDetailList = settlementDetailList;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @NonNull
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public SettlementDetailViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 | 
				
			||||||
 | 
					        View view = LayoutInflater.from(parent.getContext())
 | 
				
			||||||
 | 
					                .inflate(R.layout.item_settlement_detail, parent, false);
 | 
				
			||||||
 | 
					        return new SettlementDetailViewHolder(view);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public void onBindViewHolder(@NonNull SettlementDetailViewHolder holder, int position) {
 | 
				
			||||||
 | 
					        SettlementDetailItem item = settlementDetailList.get(position);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        holder.tvPaymentMethod.setText(item.getPaymentMethod());
 | 
				
			||||||
 | 
					        holder.tvTotalNominal.setText(formatCurrency(item.getTotalNominal()));
 | 
				
			||||||
 | 
					        holder.tvJumlahTransaksi.setText(String.valueOf(item.getJumlahTransaksi()));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public int getItemCount() {
 | 
				
			||||||
 | 
					        return settlementDetailList.size();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    private String formatCurrency(long amount) {
 | 
				
			||||||
 | 
					        NumberFormat formatter = NumberFormat.getNumberInstance(new Locale("id", "ID"));
 | 
				
			||||||
 | 
					        return formatter.format(amount);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    static class SettlementDetailViewHolder extends RecyclerView.ViewHolder {
 | 
				
			||||||
 | 
					        TextView tvPaymentMethod;
 | 
				
			||||||
 | 
					        TextView tvTotalNominal;
 | 
				
			||||||
 | 
					        TextView tvJumlahTransaksi;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public SettlementDetailViewHolder(@NonNull View itemView) {
 | 
				
			||||||
 | 
					            super(itemView);
 | 
				
			||||||
 | 
					            tvPaymentMethod = itemView.findViewById(R.id.tv_payment_method);
 | 
				
			||||||
 | 
					            tvTotalNominal = itemView.findViewById(R.id.tv_total_nominal);
 | 
				
			||||||
 | 
					            tvJumlahTransaksi = itemView.findViewById(R.id.tv_jumlah_transaksi);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,832 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.sunmi.peripheral.printer.InnerResultCallback;
 | 
				
			||||||
 | 
					import android.os.RemoteException;
 | 
				
			||||||
 | 
					import com.example.bdkipoc.MyApplication;
 | 
				
			||||||
 | 
					import com.sunmi.peripheral.printer.SunmiPrinterService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final InnerResultCallback printCallback = new InnerResultCallback() {
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public void onRunResult(boolean isSuccess) throws RemoteException {
 | 
				
			||||||
 | 
					            runOnUiThread(() -> {
 | 
				
			||||||
 | 
					                if (isSuccess) {
 | 
				
			||||||
 | 
					                    showToast("Struk berhasil dicetak");
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    showToast("Gagal mencetak struk");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public void onReturnString(String result) throws RemoteException {
 | 
				
			||||||
 | 
					            Log.d(TAG, "Print result: " + result);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public void onRaiseException(int code, String msg) throws RemoteException {
 | 
				
			||||||
 | 
					            runOnUiThread(() -> showToast("Printer error: " + msg));
 | 
				
			||||||
 | 
					            Log.e(TAG, "Printer exception: " + code + " - " + msg);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @Override
 | 
				
			||||||
 | 
					        public void onPrintResult(int code, String msg) throws RemoteException {
 | 
				
			||||||
 | 
					            Log.d(TAG, "Print result code: " + code + ", msg: " + msg);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @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<String> 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() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Check if printer service is available
 | 
				
			||||||
 | 
					            if (com.example.bdkipoc.MyApplication.app.sunmiPrinterService == null) {
 | 
				
			||||||
 | 
					                showToast("Printer tidak tersedia");
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            SunmiPrinterService printerService = com.example.bdkipoc.MyApplication.app.sunmiPrinterService;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Get all the data from the views
 | 
				
			||||||
 | 
					            String merchantNameText = merchantName.getText().toString();
 | 
				
			||||||
 | 
					            String merchantLocationText = merchantLocation.getText().toString();
 | 
				
			||||||
 | 
					            String midTextValue = midText.getText().toString();
 | 
				
			||||||
 | 
					            String tidTextValue = tidText.getText().toString();
 | 
				
			||||||
 | 
					            String transactionNumberText = transactionNumber.getText().toString();
 | 
				
			||||||
 | 
					            String transactionDateText = transactionDate.getText().toString();
 | 
				
			||||||
 | 
					            String paymentMethodText = paymentMethod.getText().toString();
 | 
				
			||||||
 | 
					            String cardTypeText = cardType.getText().toString();
 | 
				
			||||||
 | 
					            String transactionTotalText = transactionTotal.getText().toString();
 | 
				
			||||||
 | 
					            String taxPercentageText = taxPercentage.getText().toString();
 | 
				
			||||||
 | 
					            String serviceFeeText = serviceFee.getText().toString();
 | 
				
			||||||
 | 
					            String finalTotalText = finalTotal.getText().toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            showToast("Mencetak struk...");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                // Start printing
 | 
				
			||||||
 | 
					                printerService.enterPrinterBuffer(true);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Set alignment to center
 | 
				
			||||||
 | 
					                printerService.setAlignment(1, null);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Print header
 | 
				
			||||||
 | 
					                printerService.printText("# Payvora PRO\n\n", null);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Set alignment to left
 | 
				
			||||||
 | 
					                printerService.setAlignment(0, null);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Print merchant info
 | 
				
			||||||
 | 
					                printerService.printText(merchantNameText + "\n", null);
 | 
				
			||||||
 | 
					                printerService.printText(merchantLocationText + "\n\n", null);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Print MID/TID
 | 
				
			||||||
 | 
					                printerService.printText(midTextValue + " | " + tidTextValue + "\n\n", null);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Print transaction details
 | 
				
			||||||
 | 
					                printerService.printText("Nomor transaksi    " + transactionNumberText + "\n", null);
 | 
				
			||||||
 | 
					                printerService.printText("Tanggal transaksi  " + transactionDateText + "\n", null);
 | 
				
			||||||
 | 
					                printerService.printText("Metode pembayaran    " + paymentMethodText + "\n", null);
 | 
				
			||||||
 | 
					                printerService.printText("Jenis Kartu    " + cardTypeText + "\n\n", null);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Print amounts
 | 
				
			||||||
 | 
					                printerService.printText("Total transaksi    " + transactionTotalText + "\n", null);
 | 
				
			||||||
 | 
					                printerService.printText("Pajak (%)    " + taxPercentageText + "\n", null);
 | 
				
			||||||
 | 
					                printerService.printText("Biaya Layanan    " + serviceFeeText + "\n\n", null);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Print total in bold
 | 
				
			||||||
 | 
					                printerService.printText("**TOTAL** **" + finalTotalText + "**\n", null);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Add EMV specific details if available
 | 
				
			||||||
 | 
					                // if (emvMode && emvCardholderName != null) {
 | 
				
			||||||
 | 
					                //     printerService.printText("\nDETAIL EMV:\n", null);
 | 
				
			||||||
 | 
					                //     printerService.printText("Cardholder: " + emvCardholderName + "\n", null);
 | 
				
			||||||
 | 
					                //     if (cardNo != null) {
 | 
				
			||||||
 | 
					                //         printerService.printText("Card: " + maskCardNumber(cardNo) + "\n", null);
 | 
				
			||||||
 | 
					                //     }
 | 
				
			||||||
 | 
					                //     if (emvAid != null) {
 | 
				
			||||||
 | 
					                //         printerService.printText("AID: " + emvAid + "\n", null);
 | 
				
			||||||
 | 
					                //     }
 | 
				
			||||||
 | 
					                //     if (emvExpiry != null) {
 | 
				
			||||||
 | 
					                //         printerService.printText("Expiry: " + emvExpiry + "\n", null);
 | 
				
			||||||
 | 
					                //     }
 | 
				
			||||||
 | 
					                // }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Add some line feeds
 | 
				
			||||||
 | 
					                printerService.lineWrap(4, null);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                // Exit buffer mode
 | 
				
			||||||
 | 
					                printerService.exitPrinterBuffer(true);
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					            } catch (RemoteException e) {
 | 
				
			||||||
 | 
					                e.printStackTrace();
 | 
				
			||||||
 | 
					                showToast("Error printer: " + e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Print error", e);
 | 
				
			||||||
 | 
					            showToast("Error saat mencetak: " + e.getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,427 @@
 | 
				
			|||||||
 | 
					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) {
 | 
				
			||||||
 | 
					        // Force reset jika masih ada proses berjalan
 | 
				
			||||||
 | 
					        if (mProcessStep > 0) {
 | 
				
			||||||
 | 
					            Log.w(TAG, "Forcing EMV reset - previous step: " + mProcessStep);
 | 
				
			||||||
 | 
					            resetEMVProcess();
 | 
				
			||||||
 | 
					            try { Thread.sleep(1000); } catch (InterruptedException e) {}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        Log.d(TAG, "Starting fresh EMV transaction");
 | 
				
			||||||
 | 
					        mProcessStep = 1;
 | 
				
			||||||
 | 
					        mCardType = cardType;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Extended initialization
 | 
				
			||||||
 | 
					            mEMVOptV2.initEmvProcess();
 | 
				
			||||||
 | 
					            Thread.sleep(500);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            new Handler(Looper.getMainLooper()).postDelayed(() -> {
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    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 EMV with reset state");
 | 
				
			||||||
 | 
					                    mEMVOptV2.transactProcessEx(bundle, mEMVListener);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                } catch (Exception e) {
 | 
				
			||||||
 | 
					                    Log.e(TAG, "Error starting EMV: " + e.getMessage());
 | 
				
			||||||
 | 
					                    if (callback != null) {
 | 
				
			||||||
 | 
					                        callback.onTransactionFailed(-1, "EMV start error: " + e.getMessage());
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }, 800); // Longer delay
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        } catch (Exception e) {
 | 
				
			||||||
 | 
					            Log.e(TAG, "Error in EMV transaction setup: " + e.getMessage());
 | 
				
			||||||
 | 
					            if (callback != null) {
 | 
				
			||||||
 | 
					                callback.onTransactionFailed(-1, e.getMessage());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    public void resetEMVProcess() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            Log.d(TAG, "Resetting EMV process - current step: " + mProcessStep);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // Reset semua state variables FIRST
 | 
				
			||||||
 | 
					            mProcessStep = 0;
 | 
				
			||||||
 | 
					            mCardNo = null;
 | 
				
			||||||
 | 
					            mPinType = 0;
 | 
				
			||||||
 | 
					            mCertInfo = null;
 | 
				
			||||||
 | 
					            mCardType = 0;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (mEMVOptV2 != null) {
 | 
				
			||||||
 | 
					                // Double reset untuk memastikan
 | 
				
			||||||
 | 
					                mEMVOptV2.initEmvProcess();
 | 
				
			||||||
 | 
					                Thread.sleep(300);
 | 
				
			||||||
 | 
					                mEMVOptV2.initEmvProcess();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Log.d(TAG, "EMV process reset completed");
 | 
				
			||||||
 | 
					        } 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<EMVCandidateV2> 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<EMVCandidateV2> 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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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");
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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<Void, Void, Boolean> {
 | 
				
			||||||
 | 
					        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, "==============================");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										265
									
								
								app/src/main/java/com/example/bdkipoc/utils/ByteUtil.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -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<byte[]> 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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										89
									
								
								app/src/main/java/com/example/bdkipoc/utils/LogUtil.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										107
									
								
								app/src/main/java/com/example/bdkipoc/utils/Utility.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -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<String> 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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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 {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								app/src/main/res/anim/fade_in.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					    <alpha xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromAlpha="0.0"
 | 
				
			||||||
 | 
					        android:toAlpha="1.0" />
 | 
				
			||||||
							
								
								
									
										4
									
								
								app/src/main/res/anim/fade_out.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					<alpha xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    android:duration="300"
 | 
				
			||||||
 | 
					    android:fromAlpha="1.0"
 | 
				
			||||||
 | 
					    android:toAlpha="0.0" />
 | 
				
			||||||
							
								
								
									
										14
									
								
								app/src/main/res/anim/scale_in.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <scale
 | 
				
			||||||
 | 
					        android:duration="200"
 | 
				
			||||||
 | 
					        android:fromXScale="0.8"
 | 
				
			||||||
 | 
					        android:fromYScale="0.8"
 | 
				
			||||||
 | 
					        android:pivotX="50%"
 | 
				
			||||||
 | 
					        android:pivotY="50%"
 | 
				
			||||||
 | 
					        android:toXScale="1.0"
 | 
				
			||||||
 | 
					        android:toYScale="1.0" />
 | 
				
			||||||
 | 
					    <alpha
 | 
				
			||||||
 | 
					        android:duration="200"
 | 
				
			||||||
 | 
					        android:fromAlpha="0.0"
 | 
				
			||||||
 | 
					        android:toAlpha="1.0" />
 | 
				
			||||||
 | 
					</set>
 | 
				
			||||||
							
								
								
									
										14
									
								
								app/src/main/res/anim/scale_out.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <scale
 | 
				
			||||||
 | 
					        android:duration="200"
 | 
				
			||||||
 | 
					        android:fromXScale="1.0"
 | 
				
			||||||
 | 
					        android:fromYScale="1.0"
 | 
				
			||||||
 | 
					        android:pivotX="50%"
 | 
				
			||||||
 | 
					        android:pivotY="50%"
 | 
				
			||||||
 | 
					        android:toXScale="0.8"
 | 
				
			||||||
 | 
					        android:toYScale="0.8" />
 | 
				
			||||||
 | 
					    <alpha
 | 
				
			||||||
 | 
					        android:duration="200"
 | 
				
			||||||
 | 
					        android:fromAlpha="1.0"
 | 
				
			||||||
 | 
					        android:toAlpha="0.0" />
 | 
				
			||||||
 | 
					</set>
 | 
				
			||||||
							
								
								
									
										10
									
								
								app/src/main/res/anim/slide_down.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <translate
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromYDelta="0%p"
 | 
				
			||||||
 | 
					        android:toYDelta="50%p" />
 | 
				
			||||||
 | 
					    <alpha
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromAlpha="1.0"
 | 
				
			||||||
 | 
					        android:toAlpha="0.0" />
 | 
				
			||||||
 | 
					</set>
 | 
				
			||||||
							
								
								
									
										10
									
								
								app/src/main/res/anim/slide_in_left.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <translate
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromXDelta="-100%p"
 | 
				
			||||||
 | 
					        android:toXDelta="0" />
 | 
				
			||||||
 | 
					    <alpha
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromAlpha="0.0"
 | 
				
			||||||
 | 
					        android:toAlpha="1.0" />
 | 
				
			||||||
 | 
					</set>
 | 
				
			||||||
							
								
								
									
										10
									
								
								app/src/main/res/anim/slide_in_right.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <translate
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromXDelta="100%p"
 | 
				
			||||||
 | 
					        android:toXDelta="0" />
 | 
				
			||||||
 | 
					    <alpha
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromAlpha="0.0"
 | 
				
			||||||
 | 
					        android:toAlpha="1.0" />
 | 
				
			||||||
 | 
					</set>
 | 
				
			||||||
							
								
								
									
										10
									
								
								app/src/main/res/anim/slide_out_left.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <translate
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromXDelta="0"
 | 
				
			||||||
 | 
					        android:toXDelta="-100%p" />
 | 
				
			||||||
 | 
					    <alpha
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromAlpha="1.0"
 | 
				
			||||||
 | 
					        android:toAlpha="0.0" />
 | 
				
			||||||
 | 
					</set>
 | 
				
			||||||
							
								
								
									
										10
									
								
								app/src/main/res/anim/slide_out_right.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <translate
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromXDelta="0"
 | 
				
			||||||
 | 
					        android:toXDelta="100%p" />
 | 
				
			||||||
 | 
					    <alpha
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromAlpha="1.0"
 | 
				
			||||||
 | 
					        android:toAlpha="0.0" />
 | 
				
			||||||
 | 
					</set>
 | 
				
			||||||
							
								
								
									
										10
									
								
								app/src/main/res/anim/slide_up.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<set xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <translate
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromYDelta="50%p"
 | 
				
			||||||
 | 
					        android:toYDelta="0%p" />
 | 
				
			||||||
 | 
					    <alpha
 | 
				
			||||||
 | 
					        android:duration="300"
 | 
				
			||||||
 | 
					        android:fromAlpha="0.0"
 | 
				
			||||||
 | 
					        android:toAlpha="1.0" />
 | 
				
			||||||
 | 
					</set>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/banner.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 112 KiB  | 
							
								
								
									
										6
									
								
								app/src/main/res/drawable/bg_status.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<shape xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <solid android:color="#F0F0F0"/>
 | 
				
			||||||
 | 
					    <corners android:radius="8dp"/>
 | 
				
			||||||
 | 
					    <stroke android:width="1dp" android:color="#E0E0E0"/>
 | 
				
			||||||
 | 
					</shape>
 | 
				
			||||||
							
								
								
									
										6
									
								
								app/src/main/res/drawable/border_button_red.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    android:shape="rectangle">
 | 
				
			||||||
 | 
					    <solid android:color="@android:color/white"/>
 | 
				
			||||||
 | 
					    <stroke android:width="1dp" android:color="#DE0701"/>
 | 
				
			||||||
 | 
					    <corners android:radius="8dp"/>
 | 
				
			||||||
 | 
					</shape>
 | 
				
			||||||
							
								
								
									
										6
									
								
								app/src/main/res/drawable/button_active_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    android:shape="rectangle">
 | 
				
			||||||
 | 
					    <solid android:color="#DE0701" />
 | 
				
			||||||
 | 
					    <corners android:radius="8dp" />
 | 
				
			||||||
 | 
					</shape>
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/button_cancel_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<shape xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <stroke android:width="2dp" android:color="#E31937"/>
 | 
				
			||||||
 | 
					    <corners android:radius="8dp"/>
 | 
				
			||||||
 | 
					    <solid android:color="@android:color/transparent"/>
 | 
				
			||||||
 | 
					</shape>
 | 
				
			||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<selector xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <item android:drawable="@drawable/button_active_background" android:state_enabled="true"/>
 | 
				
			||||||
 | 
					    <item android:drawable="@drawable/button_inactive_background" android:state_enabled="false"/>
 | 
				
			||||||
 | 
					</selector>
 | 
				
			||||||
							
								
								
									
										9
									
								
								app/src/main/res/drawable/button_finish_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    android:shape="rectangle">
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <solid android:color="#3498DB" />
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <corners android:radius="8dp" />
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					</shape>
 | 
				
			||||||
							
								
								
									
										6
									
								
								app/src/main/res/drawable/button_inactive_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    android:shape="rectangle">
 | 
				
			||||||
 | 
					    <solid android:color="#ECEFF0" />
 | 
				
			||||||
 | 
					    <corners android:radius="8dp" />
 | 
				
			||||||
 | 
					</shape>
 | 
				
			||||||
							
								
								
									
										13
									
								
								app/src/main/res/drawable/button_secondary_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    android:shape="rectangle">
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <solid android:color="#F5F5F5" />
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <stroke 
 | 
				
			||||||
 | 
					        android:width="1dp"
 | 
				
			||||||
 | 
					        android:color="#E0E0E0" />
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <corners android:radius="8dp" />
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					</shape>
 | 
				
			||||||
							
								
								
									
										12
									
								
								app/src/main/res/drawable/card_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    android:shape="rectangle">
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <!-- Warna solid (biru cerah seperti #4299E1) -->
 | 
				
			||||||
 | 
					    <solid android:color="#4299E1" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Sudut melengkung -->
 | 
				
			||||||
 | 
					    <corners android:radius="16dp" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Hilangkan stroke jika tidak dibutuhkan -->
 | 
				
			||||||
 | 
					    <stroke android:width="0dp" android:color="#00000000" />
 | 
				
			||||||
 | 
					</shape>
 | 
				
			||||||
							
								
								
									
										6
									
								
								app/src/main/res/drawable/copyable_text_background.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<shape xmlns:android="http://schemas.android.com/apk/res/android">
 | 
				
			||||||
 | 
					    <solid android:color="#F0F0F0" />
 | 
				
			||||||
 | 
					    <corners android:radius="8dp" />
 | 
				
			||||||
 | 
					    <stroke android:width="1dp" android:color="#DDDDDD" />
 | 
				
			||||||
 | 
					</shape>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/ic_arrow_back.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 220 B  | 
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_backspace.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<!-- res/drawable/icons/ic_backspace.xml -->
 | 
				
			||||||
 | 
					<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    android:width="24dp"
 | 
				
			||||||
 | 
					    android:height="24dp"
 | 
				
			||||||
 | 
					    android:viewportWidth="24"
 | 
				
			||||||
 | 
					    android:viewportHeight="24">
 | 
				
			||||||
 | 
					    <path
 | 
				
			||||||
 | 
					        android:fillColor="#333333"
 | 
				
			||||||
 | 
					        android:pathData="M22,3H7c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM19,15.59L17.59,17 14,13.41 10.41,17 9,15.59 12.59,12 9,8.41 10.41,7 14,10.59 17.59,7 19,8.41 15.41,12 19,15.59z"/>
 | 
				
			||||||
 | 
					</vector>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/ic_card_insert.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.3 KiB  | 
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_check_circle.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    android:width="24dp"
 | 
				
			||||||
 | 
					    android:height="24dp"
 | 
				
			||||||
 | 
					    android:viewportWidth="24"
 | 
				
			||||||
 | 
					    android:viewportHeight="24"
 | 
				
			||||||
 | 
					    android:tint="?attr/colorOnPrimary">
 | 
				
			||||||
 | 
					    <path
 | 
				
			||||||
 | 
					        android:fillColor="@android:color/white"
 | 
				
			||||||
 | 
					        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
 | 
				
			||||||
 | 
					</vector>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/ic_credit_card.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 606 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/ic_debit_card.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/ic_e_money.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/ic_email.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/ic_help.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable/ic_history.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.3 KiB  |