Compare commits
62 Commits
729bdddad4
...
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 |
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
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ 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
|
||||||
@@ -22,20 +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.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"
|
||||||
@@ -29,32 +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" />
|
|
||||||
<activity
|
|
||||||
android:name=".PinActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ReceiptActivity"
|
android:name=".ReceiptActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".QrisActivity"
|
android:name=".QrisActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<activity android:name=".QrisResultActivity" />
|
|
||||||
|
<!-- FIXED: Updated to correct package path -->
|
||||||
|
<activity
|
||||||
|
android:name=".qris.view.QrisResultActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettlementActivity"
|
android:name=".SettlementActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".HistoryActivity"
|
android:name=".LoginActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".HistoryDetailActivity"
|
android:name=".histori.HistoryActivity"
|
||||||
android:exported="false" />
|
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";
|
||||||
|
}
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
package com.example.bdkipoc;
|
|
||||||
|
|
||||||
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 androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
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;
|
|
||||||
|
|
||||||
public class HistoryDetailActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
private RecyclerView recyclerView;
|
|
||||||
private HistoryDetailAdapter adapter;
|
|
||||||
private List<HistoryItem> detailList;
|
|
||||||
private ImageView btnBack;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_history_detail);
|
|
||||||
|
|
||||||
initViews();
|
|
||||||
setupRecyclerView();
|
|
||||||
loadData();
|
|
||||||
setupClickListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initViews() {
|
|
||||||
recyclerView = findViewById(R.id.recycler_view);
|
|
||||||
btnBack = findViewById(R.id.btn_back);
|
|
||||||
detailList = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupRecyclerView() {
|
|
||||||
adapter = new HistoryDetailAdapter(detailList);
|
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupClickListeners() {
|
|
||||||
btnBack.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadData() {
|
|
||||||
try {
|
|
||||||
// Get data from HistoryActivity
|
|
||||||
List<HistoryItem> fullData = HistoryActivity.getFullHistoryData();
|
|
||||||
|
|
||||||
if (fullData != null && !fullData.isEmpty()) {
|
|
||||||
detailList.clear();
|
|
||||||
detailList.addAll(fullData);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
} else {
|
|
||||||
loadSampleDetailData();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
loadSampleDetailData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadSampleDetailData() {
|
|
||||||
detailList.clear();
|
|
||||||
|
|
||||||
// Create sample detail data
|
|
||||||
HistoryItem[] sampleData = {
|
|
||||||
new HistoryItem("03:44", "11-05-2025", 2018619, "Kredit", "FAILED", "197870"),
|
|
||||||
new HistoryItem("03:10", "12-05-2025", 3974866, "QRIS", "SUCCESS", "053059"),
|
|
||||||
new HistoryItem("15:17", "13-05-2025", 2418167, "QRIS", "FAILED", "668320"),
|
|
||||||
new HistoryItem("12:09", "11-05-2025", 3429230, "Debit", "FAILED", "454790"),
|
|
||||||
new HistoryItem("08:39", "10-05-2025", 4656447, "QRIS", "FAILED", "454248"),
|
|
||||||
new HistoryItem("00:35", "12-05-2025", 3507704, "QRIS", "FAILED", "301644"),
|
|
||||||
new HistoryItem("22:43", "13-05-2025", 4277904, "Debit", "SUCCESS", "388709"),
|
|
||||||
new HistoryItem("18:16", "11-05-2025", 4456904, "Debit", "FAILED", "986861"),
|
|
||||||
new HistoryItem("12:51", "10-05-2025", 3027953, "Kredit", "SUCCESS", "771339"),
|
|
||||||
new HistoryItem("19:50", "14-05-2025", 4399035, "QRIS", "FAILED", "103478")
|
|
||||||
};
|
|
||||||
|
|
||||||
for (HistoryItem item : sampleData) {
|
|
||||||
detailList.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HistoryDetailAdapter class - simplified for stability
|
|
||||||
class HistoryDetailAdapter extends RecyclerView.Adapter<HistoryDetailAdapter.DetailViewHolder> {
|
|
||||||
|
|
||||||
private List<HistoryItem> detailList;
|
|
||||||
|
|
||||||
public HistoryDetailAdapter(List<HistoryItem> detailList) {
|
|
||||||
this.detailList = detailList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public DetailViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_history_detail, parent, false);
|
|
||||||
return new DetailViewHolder(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBindViewHolder(@NonNull DetailViewHolder holder, int position) {
|
|
||||||
HistoryItem item = detailList.get(position);
|
|
||||||
|
|
||||||
try {
|
|
||||||
holder.tvReferenceId.setText("Ref: " + item.getReferenceId());
|
|
||||||
holder.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
|
|
||||||
holder.tvChannel.setText(item.getChannelName());
|
|
||||||
holder.tvMerchant.setText("TEST MERCHANT");
|
|
||||||
holder.tvTime.setText(formatDateTime(item.getTime(), item.getDate()));
|
|
||||||
holder.tvIssuer.setText("BANK MANDIRI");
|
|
||||||
|
|
||||||
// Set status color
|
|
||||||
String status = item.getStatus();
|
|
||||||
if ("SUCCESS".equals(status)) {
|
|
||||||
holder.tvStatus.setText("Berhasil");
|
|
||||||
holder.tvStatus.setTextColor(0xFF4CAF50); // Green
|
|
||||||
} else if ("FAILED".equals(status)) {
|
|
||||||
holder.tvStatus.setText("Gagal");
|
|
||||||
holder.tvStatus.setTextColor(0xFFF44336); // Red
|
|
||||||
} else {
|
|
||||||
holder.tvStatus.setText("Tertunda");
|
|
||||||
holder.tvStatus.setTextColor(0xFFFF9800); // Orange
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
// Set default values if error occurs
|
|
||||||
holder.tvReferenceId.setText("Ref: " + position);
|
|
||||||
holder.tvAmount.setText("Rp. 0");
|
|
||||||
holder.tvChannel.setText("Unknown");
|
|
||||||
holder.tvMerchant.setText("TEST MERCHANT");
|
|
||||||
holder.tvTime.setText("00:00, 01-01-2025");
|
|
||||||
holder.tvIssuer.setText("UNKNOWN");
|
|
||||||
holder.tvStatus.setText("Tidak Diketahui");
|
|
||||||
holder.tvStatus.setTextColor(0xFF666666);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemCount() {
|
|
||||||
return detailList != null ? detailList.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formatDateTime(String time, String date) {
|
|
||||||
try {
|
|
||||||
return time + ", " + date;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "00:00, 01-01-2025";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class DetailViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
TextView tvReferenceId;
|
|
||||||
TextView tvAmount;
|
|
||||||
TextView tvChannel;
|
|
||||||
TextView tvMerchant;
|
|
||||||
TextView tvTime;
|
|
||||||
TextView tvIssuer;
|
|
||||||
TextView tvStatus;
|
|
||||||
|
|
||||||
public DetailViewHolder(@NonNull View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
try {
|
|
||||||
tvReferenceId = itemView.findViewById(R.id.tv_reference_id);
|
|
||||||
tvAmount = itemView.findViewById(R.id.tv_amount);
|
|
||||||
tvChannel = itemView.findViewById(R.id.tv_channel);
|
|
||||||
tvMerchant = itemView.findViewById(R.id.tv_merchant);
|
|
||||||
tvTime = itemView.findViewById(R.id.tv_time);
|
|
||||||
tvIssuer = itemView.findViewById(R.id.tv_issuer);
|
|
||||||
tvStatus = itemView.findViewById(R.id.tv_status);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,12 @@ import android.view.View;
|
|||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
import android.view.WindowManager;
|
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;
|
||||||
@@ -18,11 +22,34 @@ import androidx.core.view.ViewCompat;
|
|||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
import com.google.android.material.button.MaterialButton;
|
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 boolean isExpanded = false; // False = showing only 9 main menus, True = showing all 15 menus
|
||||||
private MaterialButton btnLainnya;
|
private MaterialButton btnLainnya;
|
||||||
|
private MaterialButton logoutButton;
|
||||||
|
private TextView tvUserName, tvUserRole;
|
||||||
|
private LinearLayout userInfoSection;
|
||||||
|
private String authToken;
|
||||||
|
private JSONObject userData;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onWindowFocusChanged(boolean hasFocus) {
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
@@ -47,6 +74,17 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -56,8 +94,20 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
return insets;
|
return insets;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load user data
|
||||||
|
loadUserData();
|
||||||
|
|
||||||
// Initialize views
|
// Initialize views
|
||||||
btnLainnya = findViewById(R.id.btn_lainnya);
|
btnLainnya = findViewById(R.id.btn_lainnya);
|
||||||
|
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
|
// Check if we're returning from a completed transaction
|
||||||
checkTransactionCompletion();
|
checkTransactionCompletion();
|
||||||
@@ -67,6 +117,144 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
// Setup menu listeners
|
// Setup menu listeners
|
||||||
setupMenuListeners();
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set negative button (Batal)
|
||||||
|
builder.setNegativeButton("Batal", (dialog, which) -> {
|
||||||
|
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() {
|
private void setupInitialMenuState() {
|
||||||
@@ -83,12 +271,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
// 6 dummy menus should be hidden initially
|
// 6 dummy menus should be hidden initially
|
||||||
CardView[] dummyCards = {
|
CardView[] dummyCards = {
|
||||||
findViewById(R.id.card_dummy_menu_1),
|
findViewById(R.id.card_bantuan),
|
||||||
findViewById(R.id.card_dummy_menu_2),
|
findViewById(R.id.card_info_toko),
|
||||||
findViewById(R.id.card_dummy_menu_3),
|
findViewById(R.id.card_pengaturan),
|
||||||
findViewById(R.id.card_dummy_menu_4),
|
|
||||||
findViewById(R.id.card_dummy_menu_5),
|
|
||||||
findViewById(R.id.card_dummy_menu_6)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (CardView card : dummyCards) {
|
for (CardView card : dummyCards) {
|
||||||
@@ -135,21 +320,17 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
R.id.card_kartu_debit,
|
R.id.card_kartu_debit,
|
||||||
R.id.card_qris,
|
R.id.card_qris,
|
||||||
// Row 2 (Always visible - 3 items)
|
// Row 2 (Always visible - 3 items)
|
||||||
|
R.id.card_transfer,
|
||||||
R.id.card_uang_elektronik,
|
R.id.card_uang_elektronik,
|
||||||
R.id.card_cetak_ulang,
|
R.id.card_cetak_ulang,
|
||||||
R.id.card_settlement,
|
|
||||||
// Row 3 (Always visible - 3 items)
|
// Row 3 (Always visible - 3 items)
|
||||||
|
R.id.card_refund,
|
||||||
|
R.id.card_settlement,
|
||||||
R.id.card_histori,
|
R.id.card_histori,
|
||||||
|
// Row 4 (Hidden initially - 3 items)
|
||||||
R.id.card_bantuan,
|
R.id.card_bantuan,
|
||||||
R.id.card_info_toko,
|
R.id.card_info_toko,
|
||||||
// Row 4 (Hidden initially - 3 items)
|
R.id.card_pengaturan,
|
||||||
R.id.card_dummy_menu_1,
|
|
||||||
R.id.card_dummy_menu_2,
|
|
||||||
R.id.card_dummy_menu_3,
|
|
||||||
// Row 5 (Hidden initially - 3 items)
|
|
||||||
R.id.card_dummy_menu_4,
|
|
||||||
R.id.card_dummy_menu_5,
|
|
||||||
R.id.card_dummy_menu_6
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up click listeners for all cards
|
// Set up click listeners for all cards
|
||||||
@@ -157,39 +338,37 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
CardView cardView = findViewById(cardId);
|
CardView cardView = findViewById(cardId);
|
||||||
if (cardView != null) {
|
if (cardView != null) {
|
||||||
cardView.setOnClickListener(v -> {
|
cardView.setOnClickListener(v -> {
|
||||||
|
// ✅ ENHANCED: Navigate with payment type information and auth token
|
||||||
if (cardId == R.id.card_kartu_kredit) {
|
if (cardId == R.id.card_kartu_kredit) {
|
||||||
startActivity(new Intent(MainActivity.this, PaymentActivity.class));
|
navigateToCreateTransaction("credit_card", cardId, "Kartu Kredit");
|
||||||
} else if (cardId == R.id.card_kartu_debit) {
|
} else if (cardId == R.id.card_kartu_debit) {
|
||||||
startActivity(new Intent(MainActivity.this, PaymentActivity.class));
|
navigateToCreateTransaction("debit_card", cardId, "Kartu Debit");
|
||||||
} else if (cardId == R.id.card_qris) {
|
} else if (cardId == R.id.card_qris) {
|
||||||
startActivity(new Intent(MainActivity.this, QrisActivity.class));
|
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) {
|
} else if (cardId == R.id.card_uang_elektronik) {
|
||||||
startActivity(new Intent(MainActivity.this, PaymentActivity.class));
|
navigateToCreateTransaction("e_money", cardId, "Uang Elektronik");
|
||||||
} else if (cardId == R.id.card_cetak_ulang) {
|
} else if (cardId == R.id.card_cetak_ulang) {
|
||||||
startActivity(new Intent(MainActivity.this, TransactionActivity.class));
|
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) {
|
} else if (cardId == R.id.card_settlement) {
|
||||||
Toast.makeText(this, "Settlement - Coming Soon", Toast.LENGTH_SHORT).show();
|
startActivityWithAuth(new Intent(MainActivity.this, SettlementActivity.class));
|
||||||
} else if (cardId == R.id.card_histori) {
|
} else if (cardId == R.id.card_histori) {
|
||||||
Toast.makeText(this, "Histori - Coming Soon", Toast.LENGTH_SHORT).show();
|
startActivityWithAuth(new Intent(MainActivity.this, HistoryActivity.class));
|
||||||
|
// Col-4
|
||||||
} else if (cardId == R.id.card_bantuan) {
|
} else if (cardId == R.id.card_bantuan) {
|
||||||
Toast.makeText(this, "Bantuan - Coming Soon", Toast.LENGTH_SHORT).show();
|
startActivityWithAuth(new Intent(MainActivity.this, BantuanActivity.class));
|
||||||
} else if (cardId == R.id.card_info_toko) {
|
} else if (cardId == R.id.card_info_toko) {
|
||||||
Toast.makeText(this, "Info Toko - Coming Soon", Toast.LENGTH_SHORT).show();
|
startActivityWithAuth(new Intent(MainActivity.this, InfoTokoActivity.class));
|
||||||
} else if (cardId == R.id.card_dummy_menu_1) {
|
} else if (cardId == R.id.card_pengaturan) {
|
||||||
Toast.makeText(this, "Dummy Menu 1 - Coming Soon", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "Pengaturan - Coming Soon", Toast.LENGTH_SHORT).show();
|
||||||
} else if (cardId == R.id.card_dummy_menu_2) {
|
|
||||||
Toast.makeText(this, "Dummy Menu 2 - Coming Soon", Toast.LENGTH_SHORT).show();
|
|
||||||
} else if (cardId == R.id.card_dummy_menu_3) {
|
|
||||||
Toast.makeText(this, "Dummy Menu 3 - Coming Soon", Toast.LENGTH_SHORT).show();
|
|
||||||
} else if (cardId == R.id.card_dummy_menu_4) {
|
|
||||||
Toast.makeText(this, "Dummy Menu 4 - Coming Soon", Toast.LENGTH_SHORT).show();
|
|
||||||
} else if (cardId == R.id.card_dummy_menu_5) {
|
|
||||||
Toast.makeText(this, "Dummy Menu 5 - Coming Soon", Toast.LENGTH_SHORT).show();
|
|
||||||
} else if (cardId == R.id.card_dummy_menu_6) {
|
|
||||||
Toast.makeText(this, "Dummy Menu 6 - Coming Soon", Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback for any other cards
|
// Fallback for any other cards
|
||||||
Toast.makeText(this, "Menu Diklik: " + getResources().getResourceEntryName(cardId), Toast.LENGTH_SHORT).show();
|
navigateToCreateTransaction("credit_card", cardId, "Unknown");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -197,12 +376,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
// Get references to ONLY the dummy cards that need to be toggled
|
// Get references to ONLY the dummy cards that need to be toggled
|
||||||
CardView[] toggleableCards = {
|
CardView[] toggleableCards = {
|
||||||
findViewById(R.id.card_dummy_menu_1),
|
findViewById(R.id.card_bantuan),
|
||||||
findViewById(R.id.card_dummy_menu_2),
|
findViewById(R.id.card_info_toko),
|
||||||
findViewById(R.id.card_dummy_menu_3),
|
findViewById(R.id.card_pengaturan),
|
||||||
findViewById(R.id.card_dummy_menu_4),
|
|
||||||
findViewById(R.id.card_dummy_menu_5),
|
|
||||||
findViewById(R.id.card_dummy_menu_6)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up "Lainnya" button click listener
|
// Set up "Lainnya" button click listener
|
||||||
@@ -239,14 +415,158 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up scan dan bayar card click listener
|
// Setup Banner Image
|
||||||
LinearLayout scanBayarContent = findViewById(R.id.scan_bayar_content);
|
ImageView bannerQris = findViewById(R.id.banner_qris);
|
||||||
if (scanBayarContent != null) {
|
if (bannerQris != null) {
|
||||||
scanBayarContent.setOnClickListener(v -> {
|
bannerQris.setOnClickListener(v -> {
|
||||||
// Navigate to QRIS payment activity
|
// Pindah ke halaman QRIS
|
||||||
startActivity(new Intent(MainActivity.this, QrisActivity.class));
|
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
|
@Override
|
||||||
@@ -260,8 +580,96 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.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
|
// Clear any transaction completion flags to avoid repeated messages
|
||||||
getIntent().removeExtra("transaction_completed");
|
getIntent().removeExtra("transaction_completed");
|
||||||
getIntent().removeExtra("transaction_amount");
|
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,530 +0,0 @@
|
|||||||
package com.example.bdkipoc;
|
|
||||||
|
|
||||||
import android.animation.AnimatorSet;
|
|
||||||
import android.animation.ObjectAnimator;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.RadioGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
|
|
||||||
public class PaymentActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
// Views
|
|
||||||
private EditText editTextAmount;
|
|
||||||
private Button confirmButton;
|
|
||||||
private LinearLayout backNavigation;
|
|
||||||
private ImageView backArrow;
|
|
||||||
private TextView toolbarTitle;
|
|
||||||
|
|
||||||
// Numpad buttons
|
|
||||||
private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000;
|
|
||||||
private ImageView btnDelete;
|
|
||||||
|
|
||||||
// Modal components
|
|
||||||
private Dialog paymentModal;
|
|
||||||
|
|
||||||
// Data
|
|
||||||
private StringBuilder currentAmount = new StringBuilder();
|
|
||||||
private static final int MAX_AMOUNT_LENGTH = 12;
|
|
||||||
|
|
||||||
// Animation
|
|
||||||
private Handler animationHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// Set status bar color programmatically
|
|
||||||
setStatusBarColor();
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_payment);
|
|
||||||
|
|
||||||
initializeViews();
|
|
||||||
setupClickListeners();
|
|
||||||
setupInitialStates();
|
|
||||||
setupModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setStatusBarColor() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
Window window = getWindow();
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
||||||
window.setStatusBarColor(Color.parseColor("#E31937")); // Red color
|
|
||||||
|
|
||||||
// Make status bar icons white (for dark red background)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
View decorView = window.getDecorView();
|
|
||||||
decorView.setSystemUiVisibility(0); // Clear light status bar flag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeViews() {
|
|
||||||
// Main views
|
|
||||||
editTextAmount = findViewById(R.id.editTextAmount);
|
|
||||||
confirmButton = findViewById(R.id.confirmButton);
|
|
||||||
backNavigation = findViewById(R.id.back_navigation);
|
|
||||||
backArrow = findViewById(R.id.backArrow);
|
|
||||||
toolbarTitle = findViewById(R.id.toolbarTitle);
|
|
||||||
|
|
||||||
// Numpad buttons
|
|
||||||
btn1 = findViewById(R.id.btn1);
|
|
||||||
btn2 = findViewById(R.id.btn2);
|
|
||||||
btn3 = findViewById(R.id.btn3);
|
|
||||||
btn4 = findViewById(R.id.btn4);
|
|
||||||
btn5 = findViewById(R.id.btn5);
|
|
||||||
btn6 = findViewById(R.id.btn6);
|
|
||||||
btn7 = findViewById(R.id.btn7);
|
|
||||||
btn8 = findViewById(R.id.btn8);
|
|
||||||
btn9 = findViewById(R.id.btn9);
|
|
||||||
btn0 = findViewById(R.id.btn0);
|
|
||||||
btn000 = findViewById(R.id.btn000);
|
|
||||||
btnDelete = findViewById(R.id.btnDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupModal() {
|
|
||||||
// Create modal dialog
|
|
||||||
paymentModal = new Dialog(this);
|
|
||||||
paymentModal.setContentView(R.layout.modal_layout);
|
|
||||||
|
|
||||||
// Remove background dimming - make it fully transparent
|
|
||||||
if (paymentModal.getWindow() != null) {
|
|
||||||
paymentModal.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup modal listeners
|
|
||||||
setupModalListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupModalListeners() {
|
|
||||||
// Make modal non-cancelable by touching outside
|
|
||||||
paymentModal.setCanceledOnTouchOutside(false);
|
|
||||||
|
|
||||||
// Auto dismiss after 3 seconds (simulate card processing)
|
|
||||||
Handler modalHandler = new Handler(Looper.getMainLooper());
|
|
||||||
paymentModal.setOnShowListener(dialog -> {
|
|
||||||
modalHandler.postDelayed(() -> {
|
|
||||||
if (paymentModal != null && paymentModal.isShowing()) {
|
|
||||||
// First dismiss modal, then navigate
|
|
||||||
paymentModal.dismiss();
|
|
||||||
|
|
||||||
// Add small delay to ensure modal is fully dismissed
|
|
||||||
animationHandler.postDelayed(() -> {
|
|
||||||
navigateToPinActivity();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupClickListeners() {
|
|
||||||
// Back navigation - entire LinearLayout is clickable
|
|
||||||
backNavigation.setOnClickListener(v -> {
|
|
||||||
addClickAnimation(v);
|
|
||||||
navigateBack();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Individual back arrow (for additional touch area)
|
|
||||||
backArrow.setOnClickListener(v -> {
|
|
||||||
addClickAnimation(v);
|
|
||||||
navigateBack();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Toolbar title (also clickable for back navigation)
|
|
||||||
toolbarTitle.setOnClickListener(v -> {
|
|
||||||
addClickAnimation(v);
|
|
||||||
navigateBack();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Numpad listeners
|
|
||||||
btn1.setOnClickListener(v -> handleNumpadClick(v, "1"));
|
|
||||||
btn2.setOnClickListener(v -> handleNumpadClick(v, "2"));
|
|
||||||
btn3.setOnClickListener(v -> handleNumpadClick(v, "3"));
|
|
||||||
btn4.setOnClickListener(v -> handleNumpadClick(v, "4"));
|
|
||||||
btn5.setOnClickListener(v -> handleNumpadClick(v, "5"));
|
|
||||||
btn6.setOnClickListener(v -> handleNumpadClick(v, "6"));
|
|
||||||
btn7.setOnClickListener(v -> handleNumpadClick(v, "7"));
|
|
||||||
btn8.setOnClickListener(v -> handleNumpadClick(v, "8"));
|
|
||||||
btn9.setOnClickListener(v -> handleNumpadClick(v, "9"));
|
|
||||||
btn0.setOnClickListener(v -> handleNumpadClick(v, "0"));
|
|
||||||
btn000.setOnClickListener(v -> handleNumpadClick(v, "000"));
|
|
||||||
|
|
||||||
// Delete button
|
|
||||||
btnDelete.setOnClickListener(v -> {
|
|
||||||
addClickAnimation(v);
|
|
||||||
deleteLastDigit();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Confirm button - NOW SHOWS MODAL INSTEAD OF DIRECT PAYMENT
|
|
||||||
confirmButton.setOnClickListener(v -> {
|
|
||||||
if (confirmButton.isEnabled()) {
|
|
||||||
addButtonClickAnimation(v);
|
|
||||||
showPaymentModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void navigateBack() {
|
|
||||||
// Simple back navigation without card animation
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleNumpadClick(View view, String digit) {
|
|
||||||
addClickAnimation(view);
|
|
||||||
addDigit(digit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupInitialStates() {
|
|
||||||
// Set initial amount display
|
|
||||||
editTextAmount.setText("");
|
|
||||||
|
|
||||||
// Set initial button state
|
|
||||||
updateButtonState();
|
|
||||||
|
|
||||||
// Disable EditText input (only numpad input allowed)
|
|
||||||
editTextAmount.setFocusable(false);
|
|
||||||
editTextAmount.setClickable(false);
|
|
||||||
editTextAmount.setCursorVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addDigit(String digit) {
|
|
||||||
// Validate input length
|
|
||||||
if (currentAmount.length() >= MAX_AMOUNT_LENGTH) {
|
|
||||||
showToast("Maksimal " + MAX_AMOUNT_LENGTH + " digit");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle leading zeros
|
|
||||||
if (currentAmount.length() == 0) {
|
|
||||||
if (digit.equals("000")) {
|
|
||||||
// Don't allow 000 as first input
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentAmount.append(digit);
|
|
||||||
} else if (currentAmount.length() == 1 && currentAmount.toString().equals("0")) {
|
|
||||||
if (!digit.equals("000")) {
|
|
||||||
// Replace single 0 with new digit
|
|
||||||
currentAmount = new StringBuilder(digit);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentAmount.append(digit);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAmountDisplay();
|
|
||||||
updateButtonState();
|
|
||||||
addInputFeedback();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteLastDigit() {
|
|
||||||
if (currentAmount.length() > 0) {
|
|
||||||
String current = currentAmount.toString();
|
|
||||||
|
|
||||||
// If current ends with 000, remove all three digits
|
|
||||||
if (current.endsWith("000") && current.length() >= 3) {
|
|
||||||
currentAmount.delete(currentAmount.length() - 3, currentAmount.length());
|
|
||||||
} else {
|
|
||||||
currentAmount.deleteCharAt(currentAmount.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAmountDisplay();
|
|
||||||
updateButtonState();
|
|
||||||
addDeleteFeedback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateAmountDisplay() {
|
|
||||||
String amount = currentAmount.toString();
|
|
||||||
|
|
||||||
if (amount.isEmpty() || amount.equals("0")) {
|
|
||||||
editTextAmount.setText("");
|
|
||||||
} else {
|
|
||||||
String formattedAmount = formatCurrency(amount);
|
|
||||||
editTextAmount.setText(formattedAmount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formatCurrency(String amount) {
|
|
||||||
if (TextUtils.isEmpty(amount) || amount.equals("0")) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
long number = Long.parseLong(amount);
|
|
||||||
return String.format("%,d", number).replace(',', '.');
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateButtonState() {
|
|
||||||
boolean hasValidAmount = currentAmount.length() > 0 &&
|
|
||||||
!currentAmount.toString().equals("0") &&
|
|
||||||
!currentAmount.toString().isEmpty();
|
|
||||||
|
|
||||||
confirmButton.setEnabled(hasValidAmount);
|
|
||||||
|
|
||||||
if (hasValidAmount) {
|
|
||||||
// Active state
|
|
||||||
confirmButton.setBackgroundResource(R.drawable.button_active_background);
|
|
||||||
confirmButton.setTextColor(Color.WHITE);
|
|
||||||
confirmButton.setAlpha(1.0f);
|
|
||||||
} else {
|
|
||||||
// Inactive state
|
|
||||||
confirmButton.setBackgroundResource(R.drawable.button_inactive_background);
|
|
||||||
confirmButton.setTextColor(Color.parseColor("#999999"));
|
|
||||||
confirmButton.setAlpha(0.6f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEW METHOD: Show payment modal instead of direct payment processing
|
|
||||||
private void showPaymentModal() {
|
|
||||||
String amount = currentAmount.toString();
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(amount) || amount.equals("0")) {
|
|
||||||
showToast("Masukkan jumlah pembayaran");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
long amountValue = Long.parseLong(amount);
|
|
||||||
|
|
||||||
// Validate minimum amount
|
|
||||||
if (amountValue < 1000) {
|
|
||||||
showToast("Minimal pembayaran Rp 1.000");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate maximum amount
|
|
||||||
if (amountValue > 999999999L) {
|
|
||||||
showToast("Maksimal pembayaran Rp 999.999.999");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show modal with animation
|
|
||||||
showModalWithAnimation();
|
|
||||||
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
showToast("Format jumlah tidak valid");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showModalWithAnimation() {
|
|
||||||
// Add debug log
|
|
||||||
showToast("Showing card modal...");
|
|
||||||
|
|
||||||
paymentModal.show();
|
|
||||||
|
|
||||||
// Add slide-up animation
|
|
||||||
View modalView = paymentModal.findViewById(android.R.id.content);
|
|
||||||
if (modalView != null) {
|
|
||||||
ObjectAnimator slideUp = ObjectAnimator.ofFloat(modalView, "translationY", 300f, 0f);
|
|
||||||
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(modalView, "alpha", 0f, 1f);
|
|
||||||
|
|
||||||
AnimatorSet animatorSet = new AnimatorSet();
|
|
||||||
animatorSet.playTogether(slideUp, fadeIn);
|
|
||||||
animatorSet.setDuration(300);
|
|
||||||
animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
|
|
||||||
animatorSet.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dismissModal() {
|
|
||||||
if (paymentModal != null && paymentModal.isShowing()) {
|
|
||||||
// Add slide-down animation before dismissing
|
|
||||||
View modalView = paymentModal.findViewById(android.R.id.content);
|
|
||||||
if (modalView != null) {
|
|
||||||
ObjectAnimator slideDown = ObjectAnimator.ofFloat(modalView, "translationY", 0f, 300f);
|
|
||||||
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(modalView, "alpha", 1f, 0f);
|
|
||||||
|
|
||||||
AnimatorSet animatorSet = new AnimatorSet();
|
|
||||||
animatorSet.playTogether(slideDown, fadeOut);
|
|
||||||
animatorSet.setDuration(200);
|
|
||||||
animatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
|
|
||||||
|
|
||||||
animatorSet.addListener(new android.animation.AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(android.animation.Animator animation) {
|
|
||||||
paymentModal.dismiss();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
animatorSet.start();
|
|
||||||
} else {
|
|
||||||
paymentModal.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processModalConfirmation() {
|
|
||||||
// This method is no longer needed since modal auto-dismisses
|
|
||||||
}
|
|
||||||
|
|
||||||
private void navigateToPinActivity() {
|
|
||||||
String amount = currentAmount.toString();
|
|
||||||
|
|
||||||
// Add debug log
|
|
||||||
showToast("Navigating to PIN Activity...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
long amountValue = Long.parseLong(amount);
|
|
||||||
|
|
||||||
// Launch PIN Activity with amount data
|
|
||||||
Intent intent = new Intent(this, PinActivity.class);
|
|
||||||
intent.putExtra(PinActivity.EXTRA_SOURCE_ACTIVITY, "PaymentActivity");
|
|
||||||
intent.putExtra(PinActivity.EXTRA_AMOUNT, String.valueOf(amountValue));
|
|
||||||
startActivityForResult(intent, 100);
|
|
||||||
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
showToast("Format jumlah tidak valid: " + e.getMessage());
|
|
||||||
} catch (Exception e) {
|
|
||||||
showToast("Error navigating to PIN: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processCardPayment() {
|
|
||||||
// This method is called after PIN verification is successful
|
|
||||||
// Now process the actual payment
|
|
||||||
String amount = currentAmount.toString();
|
|
||||||
|
|
||||||
try {
|
|
||||||
long amountValue = Long.parseLong(amount);
|
|
||||||
|
|
||||||
// Show processing message
|
|
||||||
showToast("PIN berhasil diverifikasi! Memproses pembayaran...");
|
|
||||||
|
|
||||||
// Process the final payment
|
|
||||||
processPayment(amountValue);
|
|
||||||
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
showToast("Format jumlah tidak valid");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processPayment(long amount) {
|
|
||||||
// Show loading state
|
|
||||||
confirmButton.setText("Memproses...");
|
|
||||||
confirmButton.setEnabled(false);
|
|
||||||
|
|
||||||
// Simulate payment processing
|
|
||||||
animationHandler.postDelayed(() -> {
|
|
||||||
// Show success message
|
|
||||||
showToast("Pembayaran berhasil! Jumlah: Rp " + formatCurrency(String.valueOf(amount)));
|
|
||||||
|
|
||||||
// Reset state and go back (this is final step after PIN verification)
|
|
||||||
resetPaymentState();
|
|
||||||
navigateBack();
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetPaymentState() {
|
|
||||||
currentAmount = new StringBuilder();
|
|
||||||
updateAmountDisplay();
|
|
||||||
updateButtonState();
|
|
||||||
confirmButton.setText("Konfirmasi");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation methods (only for numpad interactions)
|
|
||||||
private void addClickAnimation(View view) {
|
|
||||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f, 1f);
|
|
||||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f, 1f);
|
|
||||||
|
|
||||||
AnimatorSet animatorSet = new AnimatorSet();
|
|
||||||
animatorSet.playTogether(scaleX, scaleY);
|
|
||||||
animatorSet.setDuration(150);
|
|
||||||
animatorSet.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addButtonClickAnimation(View view) {
|
|
||||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.98f, 1f);
|
|
||||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.98f, 1f);
|
|
||||||
|
|
||||||
AnimatorSet animatorSet = new AnimatorSet();
|
|
||||||
animatorSet.playTogether(scaleX, scaleY);
|
|
||||||
animatorSet.setDuration(200);
|
|
||||||
animatorSet.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addInputFeedback() {
|
|
||||||
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(editTextAmount, "alpha", 0.7f, 1f);
|
|
||||||
fadeIn.setDuration(200);
|
|
||||||
fadeIn.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addDeleteFeedback() {
|
|
||||||
ObjectAnimator shake = ObjectAnimator.ofFloat(editTextAmount, "translationX", 0f, -10f, 10f, 0f);
|
|
||||||
shake.setDuration(300);
|
|
||||||
shake.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
private void showToast(String message) {
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
// Check if modal is showing, dismiss it first
|
|
||||||
if (paymentModal != null && paymentModal.isShowing()) {
|
|
||||||
dismissModal();
|
|
||||||
} else {
|
|
||||||
navigateBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
|
|
||||||
// Handle result from PIN Activity
|
|
||||||
if (resultCode == RESULT_OK && data != null) {
|
|
||||||
boolean pinVerified = data.getBooleanExtra("pin_verified", false);
|
|
||||||
if (pinVerified) {
|
|
||||||
// PIN verification successful, process payment
|
|
||||||
processCardPayment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
if (animationHandler != null) {
|
|
||||||
animationHandler.removeCallbacksAndMessages(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up modal
|
|
||||||
if (paymentModal != null && paymentModal.isShowing()) {
|
|
||||||
paymentModal.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public methods for testing
|
|
||||||
public String getCurrentAmount() {
|
|
||||||
return currentAmount.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isConfirmButtonEnabled() {
|
|
||||||
return confirmButton.isEnabled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,558 +0,0 @@
|
|||||||
package com.example.bdkipoc;
|
|
||||||
|
|
||||||
import android.animation.AnimatorSet;
|
|
||||||
import android.animation.ObjectAnimator;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
|
|
||||||
public class PinActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
// Intent Extra Keys
|
|
||||||
public static final String EXTRA_TITLE = "extra_title";
|
|
||||||
public static final String EXTRA_SUBTITLE = "extra_subtitle";
|
|
||||||
public static final String EXTRA_AMOUNT = "extra_amount";
|
|
||||||
public static final String EXTRA_SOURCE_ACTIVITY = "extra_source_activity";
|
|
||||||
|
|
||||||
// Views
|
|
||||||
private EditText editTextPin;
|
|
||||||
private Button confirmButton;
|
|
||||||
private LinearLayout backNavigation;
|
|
||||||
private ImageView backArrow;
|
|
||||||
private TextView toolbarTitle;
|
|
||||||
|
|
||||||
// Success screen views
|
|
||||||
private View successScreen;
|
|
||||||
private ImageView successIcon;
|
|
||||||
private TextView successMessage;
|
|
||||||
|
|
||||||
// Numpad buttons
|
|
||||||
private TextView btn1, btn2, btn3, btn4, btn5, btn6, btn7, btn8, btn9, btn0, btn000;
|
|
||||||
private ImageView btnDelete;
|
|
||||||
|
|
||||||
// Data
|
|
||||||
private StringBuilder currentPin = new StringBuilder();
|
|
||||||
private static final int MAX_PIN_LENGTH = 6;
|
|
||||||
private static final int MIN_PIN_LENGTH = 4;
|
|
||||||
|
|
||||||
// Extra data from intent
|
|
||||||
private String sourceActivity;
|
|
||||||
private String amount;
|
|
||||||
|
|
||||||
// Animation
|
|
||||||
private Handler animationHandler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
// Set status bar color programmatically
|
|
||||||
setStatusBarColor();
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_pin);
|
|
||||||
|
|
||||||
// Get intent extras
|
|
||||||
getIntentExtras();
|
|
||||||
|
|
||||||
initializeViews();
|
|
||||||
setupClickListeners();
|
|
||||||
setupInitialStates();
|
|
||||||
setupSuccessScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setStatusBarColor() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
Window window = getWindow();
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
|
||||||
window.setStatusBarColor(Color.parseColor("#E31937")); // Red color
|
|
||||||
|
|
||||||
// Make status bar icons white (for dark red background)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
View decorView = window.getDecorView();
|
|
||||||
decorView.setSystemUiVisibility(0); // Clear light status bar flag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getIntentExtras() {
|
|
||||||
Intent intent = getIntent();
|
|
||||||
if (intent != null) {
|
|
||||||
sourceActivity = intent.getStringExtra(EXTRA_SOURCE_ACTIVITY);
|
|
||||||
amount = intent.getStringExtra(EXTRA_AMOUNT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeViews() {
|
|
||||||
// Main views
|
|
||||||
editTextPin = findViewById(R.id.editTextPin);
|
|
||||||
confirmButton = findViewById(R.id.confirmButton);
|
|
||||||
backNavigation = findViewById(R.id.back_navigation);
|
|
||||||
backArrow = findViewById(R.id.backArrow);
|
|
||||||
toolbarTitle = findViewById(R.id.toolbarTitle);
|
|
||||||
|
|
||||||
// Success screen views
|
|
||||||
successScreen = findViewById(R.id.success_screen);
|
|
||||||
successIcon = findViewById(R.id.success_icon);
|
|
||||||
successMessage = findViewById(R.id.success_message);
|
|
||||||
|
|
||||||
// Numpad buttons
|
|
||||||
btn1 = findViewById(R.id.btn1);
|
|
||||||
btn2 = findViewById(R.id.btn2);
|
|
||||||
btn3 = findViewById(R.id.btn3);
|
|
||||||
btn4 = findViewById(R.id.btn4);
|
|
||||||
btn5 = findViewById(R.id.btn5);
|
|
||||||
btn6 = findViewById(R.id.btn6);
|
|
||||||
btn7 = findViewById(R.id.btn7);
|
|
||||||
btn8 = findViewById(R.id.btn8);
|
|
||||||
btn9 = findViewById(R.id.btn9);
|
|
||||||
btn0 = findViewById(R.id.btn0);
|
|
||||||
btn000 = findViewById(R.id.btn000);
|
|
||||||
btnDelete = findViewById(R.id.btnDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupSuccessScreen() {
|
|
||||||
// Initially hide success screen
|
|
||||||
if (successScreen != null) {
|
|
||||||
successScreen.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupClickListeners() {
|
|
||||||
// Back navigation - entire LinearLayout is clickable
|
|
||||||
backNavigation.setOnClickListener(v -> {
|
|
||||||
addClickAnimation(v);
|
|
||||||
navigateBack();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Individual back arrow (for additional touch area)
|
|
||||||
backArrow.setOnClickListener(v -> {
|
|
||||||
addClickAnimation(v);
|
|
||||||
navigateBack();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Toolbar title (also clickable for back navigation)
|
|
||||||
toolbarTitle.setOnClickListener(v -> {
|
|
||||||
addClickAnimation(v);
|
|
||||||
navigateBack();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Numpad listeners
|
|
||||||
btn1.setOnClickListener(v -> handleNumpadClick(v, "1"));
|
|
||||||
btn2.setOnClickListener(v -> handleNumpadClick(v, "2"));
|
|
||||||
btn3.setOnClickListener(v -> handleNumpadClick(v, "3"));
|
|
||||||
btn4.setOnClickListener(v -> handleNumpadClick(v, "4"));
|
|
||||||
btn5.setOnClickListener(v -> handleNumpadClick(v, "5"));
|
|
||||||
btn6.setOnClickListener(v -> handleNumpadClick(v, "6"));
|
|
||||||
btn7.setOnClickListener(v -> handleNumpadClick(v, "7"));
|
|
||||||
btn8.setOnClickListener(v -> handleNumpadClick(v, "8"));
|
|
||||||
btn9.setOnClickListener(v -> handleNumpadClick(v, "9"));
|
|
||||||
btn0.setOnClickListener(v -> handleNumpadClick(v, "0"));
|
|
||||||
btn000.setOnClickListener(v -> handleNumpadClick(v, "000"));
|
|
||||||
|
|
||||||
// Delete button
|
|
||||||
btnDelete.setOnClickListener(v -> {
|
|
||||||
addClickAnimation(v);
|
|
||||||
deleteLastDigit();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Confirm button
|
|
||||||
confirmButton.setOnClickListener(v -> {
|
|
||||||
if (confirmButton.isEnabled()) {
|
|
||||||
addButtonClickAnimation(v);
|
|
||||||
handleConfirmPin();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void navigateBack() {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleNumpadClick(View view, String digit) {
|
|
||||||
addClickAnimation(view);
|
|
||||||
addDigit(digit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupInitialStates() {
|
|
||||||
// Set initial PIN display
|
|
||||||
editTextPin.setText("");
|
|
||||||
|
|
||||||
// Set initial button state
|
|
||||||
updateButtonState();
|
|
||||||
|
|
||||||
// Disable EditText input (only numpad input allowed)
|
|
||||||
editTextPin.setFocusable(false);
|
|
||||||
editTextPin.setClickable(false);
|
|
||||||
editTextPin.setCursorVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addDigit(String digit) {
|
|
||||||
// Validate input length
|
|
||||||
if (currentPin.length() >= MAX_PIN_LENGTH) {
|
|
||||||
showToast("Maksimal " + MAX_PIN_LENGTH + " digit");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle special case for 000
|
|
||||||
if (digit.equals("000")) {
|
|
||||||
if (currentPin.length() + 3 <= MAX_PIN_LENGTH) {
|
|
||||||
currentPin.append("000");
|
|
||||||
} else {
|
|
||||||
int remainingLength = MAX_PIN_LENGTH - currentPin.length();
|
|
||||||
if (remainingLength > 0) {
|
|
||||||
currentPin.append("0".repeat(remainingLength));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentPin.append(digit);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePinDisplay();
|
|
||||||
updateButtonState();
|
|
||||||
addInputFeedback();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteLastDigit() {
|
|
||||||
if (currentPin.length() > 0) {
|
|
||||||
String current = currentPin.toString();
|
|
||||||
|
|
||||||
// If current ends with 000, remove all three digits
|
|
||||||
if (current.endsWith("000") && current.length() >= 3) {
|
|
||||||
currentPin.delete(currentPin.length() - 3, currentPin.length());
|
|
||||||
} else {
|
|
||||||
currentPin.deleteCharAt(currentPin.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePinDisplay();
|
|
||||||
updateButtonState();
|
|
||||||
addDeleteFeedback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updatePinDisplay() {
|
|
||||||
String pin = currentPin.toString();
|
|
||||||
|
|
||||||
if (pin.isEmpty()) {
|
|
||||||
editTextPin.setText("");
|
|
||||||
} else {
|
|
||||||
// Convert digits to asterisks for security
|
|
||||||
String maskedPin = "*".repeat(pin.length());
|
|
||||||
editTextPin.setText(maskedPin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateButtonState() {
|
|
||||||
boolean hasValidPin = currentPin.length() >= MIN_PIN_LENGTH;
|
|
||||||
|
|
||||||
confirmButton.setEnabled(hasValidPin);
|
|
||||||
|
|
||||||
if (hasValidPin) {
|
|
||||||
// Active state
|
|
||||||
confirmButton.setBackgroundResource(R.drawable.button_active_background);
|
|
||||||
confirmButton.setTextColor(Color.WHITE);
|
|
||||||
confirmButton.setAlpha(1.0f);
|
|
||||||
} else {
|
|
||||||
// Inactive state
|
|
||||||
confirmButton.setBackgroundResource(R.drawable.button_inactive_background);
|
|
||||||
confirmButton.setTextColor(Color.parseColor("#999999"));
|
|
||||||
confirmButton.setAlpha(0.6f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleConfirmPin() {
|
|
||||||
String pin = currentPin.toString();
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(pin)) {
|
|
||||||
showToast("Masukkan PIN");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pin.length() < MIN_PIN_LENGTH) {
|
|
||||||
showToast("PIN minimal " + MIN_PIN_LENGTH + " digit");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process PIN verification
|
|
||||||
verifyPin(pin);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void verifyPin(String pin) {
|
|
||||||
// Show loading state
|
|
||||||
confirmButton.setText("Memverifikasi...");
|
|
||||||
confirmButton.setEnabled(false);
|
|
||||||
|
|
||||||
// Simulate PIN verification
|
|
||||||
animationHandler.postDelayed(() -> {
|
|
||||||
// For demo purposes, accept any PIN with length >= 4
|
|
||||||
// In real implementation, this would call backend API
|
|
||||||
|
|
||||||
if (isValidPin(pin)) {
|
|
||||||
// Show success screen instead of toast
|
|
||||||
handleSuccessfulVerification();
|
|
||||||
} else {
|
|
||||||
showToast("PIN tidak valid. Silakan coba lagi.");
|
|
||||||
resetPinState();
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isValidPin(String pin) {
|
|
||||||
// Demo validation - in real app, this would validate against backend
|
|
||||||
// For now, reject simple patterns like "1111", "1234", etc.
|
|
||||||
return !pin.equals("1111") &&
|
|
||||||
!pin.equals("1234") &&
|
|
||||||
!pin.equals("0000") &&
|
|
||||||
pin.length() >= MIN_PIN_LENGTH;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleSuccessfulVerification() {
|
|
||||||
// Show full screen success message
|
|
||||||
showSuccessScreen();
|
|
||||||
|
|
||||||
// Navigate to receipt page after 2.5 seconds
|
|
||||||
animationHandler.postDelayed(() -> {
|
|
||||||
navigateToReceiptPage();
|
|
||||||
}, 2500);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showSuccessScreen() {
|
|
||||||
if (successScreen != null) {
|
|
||||||
// Hide all other UI components first
|
|
||||||
hideMainUIComponents();
|
|
||||||
|
|
||||||
// Set success message
|
|
||||||
if (successMessage != null) {
|
|
||||||
successMessage.setText("Pembayaran Berhasil");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show success screen with fade in animation
|
|
||||||
successScreen.setVisibility(View.VISIBLE);
|
|
||||||
successScreen.setAlpha(0f);
|
|
||||||
|
|
||||||
// Fade in the background
|
|
||||||
ObjectAnimator backgroundFadeIn = ObjectAnimator.ofFloat(successScreen, "alpha", 0f, 1f);
|
|
||||||
backgroundFadeIn.setDuration(500);
|
|
||||||
backgroundFadeIn.start();
|
|
||||||
|
|
||||||
// Add scale and bounce animation to success icon
|
|
||||||
if (successIcon != null) {
|
|
||||||
// Start with invisible icon
|
|
||||||
successIcon.setScaleX(0f);
|
|
||||||
successIcon.setScaleY(0f);
|
|
||||||
successIcon.setAlpha(0f);
|
|
||||||
|
|
||||||
// Scale animation with bounce effect
|
|
||||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(successIcon, "scaleX", 0f, 1.2f, 1f);
|
|
||||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(successIcon, "scaleY", 0f, 1.2f, 1f);
|
|
||||||
ObjectAnimator iconFadeIn = ObjectAnimator.ofFloat(successIcon, "alpha", 0f, 1f);
|
|
||||||
|
|
||||||
AnimatorSet iconAnimation = new AnimatorSet();
|
|
||||||
iconAnimation.playTogether(scaleX, scaleY, iconFadeIn);
|
|
||||||
iconAnimation.setDuration(800);
|
|
||||||
iconAnimation.setStartDelay(300);
|
|
||||||
iconAnimation.setInterpolator(new android.view.animation.OvershootInterpolator(1.2f));
|
|
||||||
iconAnimation.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add slide up animation to success message
|
|
||||||
if (successMessage != null) {
|
|
||||||
successMessage.setAlpha(0f);
|
|
||||||
successMessage.setTranslationY(50f);
|
|
||||||
|
|
||||||
ObjectAnimator messageSlideUp = ObjectAnimator.ofFloat(successMessage, "translationY", 50f, 0f);
|
|
||||||
ObjectAnimator messageFadeIn = ObjectAnimator.ofFloat(successMessage, "alpha", 0f, 1f);
|
|
||||||
|
|
||||||
AnimatorSet messageAnimation = new AnimatorSet();
|
|
||||||
messageAnimation.playTogether(messageSlideUp, messageFadeIn);
|
|
||||||
messageAnimation.setDuration(600);
|
|
||||||
messageAnimation.setStartDelay(600);
|
|
||||||
messageAnimation.setInterpolator(new android.view.animation.DecelerateInterpolator());
|
|
||||||
messageAnimation.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideMainUIComponents() {
|
|
||||||
// Hide all main UI components to create clean full screen success
|
|
||||||
if (backNavigation != null) {
|
|
||||||
backNavigation.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide the red header backgrounds
|
|
||||||
View redStatusBar = findViewById(R.id.red_status_bar);
|
|
||||||
View redHeaderBackground = findViewById(R.id.red_header_background);
|
|
||||||
if (redStatusBar != null) {
|
|
||||||
redStatusBar.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
if (redHeaderBackground != null) {
|
|
||||||
redHeaderBackground.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide PIN card
|
|
||||||
View pinCard = findViewById(R.id.pin_card);
|
|
||||||
if (pinCard != null) {
|
|
||||||
pinCard.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide numpad
|
|
||||||
View numpadGrid = findViewById(R.id.numpad_grid);
|
|
||||||
if (numpadGrid != null) {
|
|
||||||
numpadGrid.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide confirm button
|
|
||||||
if (confirmButton != null) {
|
|
||||||
confirmButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void navigateToReceiptPage() {
|
|
||||||
// Create intent to navigate to receipt/struk page
|
|
||||||
Intent intent = new Intent(this, ReceiptActivity.class);
|
|
||||||
|
|
||||||
// Pass transaction data
|
|
||||||
intent.putExtra("transaction_amount", amount);
|
|
||||||
intent.putExtra("pin_verified", true);
|
|
||||||
intent.putExtra("source_activity", sourceActivity);
|
|
||||||
|
|
||||||
// Add transaction details (you can customize these)
|
|
||||||
intent.putExtra("merchant_name", "TOKO KLONTONG PAK EKO");
|
|
||||||
intent.putExtra("merchant_location", "Ciputat Baru, Tangsel");
|
|
||||||
intent.putExtra("transaction_id", generateTransactionId());
|
|
||||||
intent.putExtra("transaction_date", getCurrentDateTime());
|
|
||||||
intent.putExtra("payment_method", "Kartu Kredit");
|
|
||||||
intent.putExtra("card_type", "BCA");
|
|
||||||
intent.putExtra("tax_percentage", "11%");
|
|
||||||
intent.putExtra("service_fee", "500");
|
|
||||||
|
|
||||||
startActivity(intent);
|
|
||||||
|
|
||||||
// Set result for calling activity
|
|
||||||
Intent resultIntent = new Intent();
|
|
||||||
resultIntent.putExtra("pin_verified", true);
|
|
||||||
resultIntent.putExtra("pin_length", currentPin.length());
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(amount)) {
|
|
||||||
resultIntent.putExtra(EXTRA_AMOUNT, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
setResult(RESULT_OK, resultIntent);
|
|
||||||
|
|
||||||
// Finish this activity
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateTransactionId() {
|
|
||||||
// Generate a simple transaction ID (in real app, this would come from backend)
|
|
||||||
return String.valueOf(System.currentTimeMillis() % 1000000000L);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getCurrentDateTime() {
|
|
||||||
// Get current date and time (in real app, use proper date formatting)
|
|
||||||
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd MMMM yyyy HH:mm", java.util.Locale.getDefault());
|
|
||||||
return sdf.format(new java.util.Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resetPinState() {
|
|
||||||
currentPin = new StringBuilder();
|
|
||||||
updatePinDisplay();
|
|
||||||
updateButtonState();
|
|
||||||
confirmButton.setText("Konfirmasi");
|
|
||||||
confirmButton.setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation methods
|
|
||||||
private void addClickAnimation(View view) {
|
|
||||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.95f, 1f);
|
|
||||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.95f, 1f);
|
|
||||||
|
|
||||||
AnimatorSet animatorSet = new AnimatorSet();
|
|
||||||
animatorSet.playTogether(scaleX, scaleY);
|
|
||||||
animatorSet.setDuration(150);
|
|
||||||
animatorSet.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addButtonClickAnimation(View view) {
|
|
||||||
ObjectAnimator scaleX = ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.98f, 1f);
|
|
||||||
ObjectAnimator scaleY = ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.98f, 1f);
|
|
||||||
|
|
||||||
AnimatorSet animatorSet = new AnimatorSet();
|
|
||||||
animatorSet.playTogether(scaleX, scaleY);
|
|
||||||
animatorSet.setDuration(200);
|
|
||||||
animatorSet.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addInputFeedback() {
|
|
||||||
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(editTextPin, "alpha", 0.7f, 1f);
|
|
||||||
fadeIn.setDuration(200);
|
|
||||||
fadeIn.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addDeleteFeedback() {
|
|
||||||
ObjectAnimator shake = ObjectAnimator.ofFloat(editTextPin, "translationX", 0f, -10f, 10f, 0f);
|
|
||||||
shake.setDuration(300);
|
|
||||||
shake.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility methods
|
|
||||||
private void showToast(String message) {
|
|
||||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
// Prevent back press when success screen is showing
|
|
||||||
if (successScreen != null && successScreen.getVisibility() == View.VISIBLE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigateBack();
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
if (animationHandler != null) {
|
|
||||||
animationHandler.removeCallbacksAndMessages(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Public methods for testing
|
|
||||||
public String getCurrentPin() {
|
|
||||||
return currentPin.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isConfirmButtonEnabled() {
|
|
||||||
return confirmButton.isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static helper method to launch PinActivity
|
|
||||||
public static void launch(android.content.Context context, String sourceActivity, String amount) {
|
|
||||||
Intent intent = new Intent(context, PinActivity.class);
|
|
||||||
intent.putExtra(EXTRA_SOURCE_ACTIVITY, sourceActivity);
|
|
||||||
if (!TextUtils.isEmpty(amount)) {
|
|
||||||
intent.putExtra(EXTRA_AMOUNT, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launch for result if context is an Activity
|
|
||||||
if (context instanceof AppCompatActivity) {
|
|
||||||
((AppCompatActivity) context).startActivityForResult(intent, 100);
|
|
||||||
} else {
|
|
||||||
context.startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,689 +0,0 @@
|
|||||||
package com.example.bdkipoc;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.*;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.text.NumberFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class QrisResultActivity extends AppCompatActivity {
|
|
||||||
// UI Components
|
|
||||||
private ImageView qrImageView;
|
|
||||||
private TextView amountTextView, referenceTextView, statusTextView;
|
|
||||||
private TextView timerTextView, qrStatusTextView;
|
|
||||||
private Button downloadQrisButton, checkStatusButton, returnMainButton;
|
|
||||||
private ProgressBar progressBar;
|
|
||||||
|
|
||||||
// QR Refresh Components
|
|
||||||
private Handler qrRefreshHandler;
|
|
||||||
private Runnable qrRefreshRunnable;
|
|
||||||
private int countdownSeconds = 60;
|
|
||||||
private boolean isQrRefreshActive = true;
|
|
||||||
|
|
||||||
// Transaction Data
|
|
||||||
private String orderId, grossAmount, referenceId, transactionId;
|
|
||||||
private String transactionTime, acquirer, merchantId, currentQrImageUrl;
|
|
||||||
private int originalAmount;
|
|
||||||
private List<String> allOrderIds = new ArrayList<>();
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
private static final String BACKEND_BASE = "https://be-edc.msvc.app";
|
|
||||||
private static final String WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
|
|
||||||
private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM=";
|
|
||||||
private static final String MIDTRANS_CHARGE_URL = "https://api.sandbox.midtrans.com/v2/charge";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.activity_qris_result);
|
|
||||||
|
|
||||||
initializeViews();
|
|
||||||
extractIntentData();
|
|
||||||
validateAndSetupUI();
|
|
||||||
startMonitoring();
|
|
||||||
setupClickListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeViews() {
|
|
||||||
qrImageView = findViewById(R.id.qrImageView);
|
|
||||||
amountTextView = findViewById(R.id.amountTextView);
|
|
||||||
referenceTextView = findViewById(R.id.referenceTextView);
|
|
||||||
downloadQrisButton = findViewById(R.id.downloadQrisButton);
|
|
||||||
checkStatusButton = findViewById(R.id.checkStatusButton);
|
|
||||||
statusTextView = findViewById(R.id.statusTextView);
|
|
||||||
returnMainButton = findViewById(R.id.returnMainButton);
|
|
||||||
progressBar = findViewById(R.id.progressBar);
|
|
||||||
timerTextView = findViewById(R.id.timerTextView);
|
|
||||||
qrStatusTextView = findViewById(R.id.qrStatusTextView);
|
|
||||||
qrRefreshHandler = new Handler(Looper.getMainLooper());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void extractIntentData() {
|
|
||||||
Intent intent = getIntent();
|
|
||||||
currentQrImageUrl = intent.getStringExtra("qrImageUrl");
|
|
||||||
originalAmount = intent.getIntExtra("amount", 0);
|
|
||||||
referenceId = intent.getStringExtra("referenceId");
|
|
||||||
orderId = intent.getStringExtra("orderId");
|
|
||||||
grossAmount = intent.getStringExtra("grossAmount");
|
|
||||||
transactionId = intent.getStringExtra("transactionId");
|
|
||||||
transactionTime = intent.getStringExtra("transactionTime");
|
|
||||||
acquirer = intent.getStringExtra("acquirer");
|
|
||||||
merchantId = intent.getStringExtra("merchantId");
|
|
||||||
|
|
||||||
allOrderIds.add(orderId);
|
|
||||||
Log.d("QrisResult", "Initialized with Order ID: " + orderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateAndSetupUI() {
|
|
||||||
if (orderId == null || transactionId == null) {
|
|
||||||
Toast.makeText(this, "Missing transaction details!", Toast.LENGTH_LONG).show();
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String formattedAmount = formatCurrency(grossAmount != null ? grossAmount : String.valueOf(originalAmount));
|
|
||||||
amountTextView.setText(formattedAmount);
|
|
||||||
referenceTextView.setText("Reference ID: " + referenceId);
|
|
||||||
|
|
||||||
loadQrImage(currentQrImageUrl);
|
|
||||||
|
|
||||||
checkStatusButton.setEnabled(false);
|
|
||||||
statusTextView.setText("Waiting for payment...");
|
|
||||||
qrStatusTextView.setText("QR Code akan refresh dalam");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startMonitoring() {
|
|
||||||
startQrRefreshTimer();
|
|
||||||
startPaymentMonitoring();
|
|
||||||
pollPendingPaymentLog(orderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startQrRefreshTimer() {
|
|
||||||
countdownSeconds = 60;
|
|
||||||
isQrRefreshActive = true;
|
|
||||||
|
|
||||||
qrRefreshRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (!isQrRefreshActive) return;
|
|
||||||
|
|
||||||
if (countdownSeconds > 0) {
|
|
||||||
timerTextView.setText(String.valueOf(countdownSeconds));
|
|
||||||
countdownSeconds--;
|
|
||||||
qrRefreshHandler.postDelayed(this, 1000);
|
|
||||||
} else {
|
|
||||||
refreshQrCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
qrRefreshHandler.post(qrRefreshRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshQrCode() {
|
|
||||||
if (!isQrRefreshActive) return;
|
|
||||||
|
|
||||||
timerTextView.setText("...");
|
|
||||||
qrStatusTextView.setText("Generating new QR Code...");
|
|
||||||
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
String newQrUrl = generateNewQrCode();
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
if (newQrUrl != null) {
|
|
||||||
currentQrImageUrl = newQrUrl;
|
|
||||||
loadQrImage(newQrUrl);
|
|
||||||
allOrderIds.add(orderId);
|
|
||||||
updateUIAfterRefresh();
|
|
||||||
countdownSeconds = 60;
|
|
||||||
qrStatusTextView.setText("QR Code akan refresh dalam");
|
|
||||||
Toast.makeText(this, "QR Code refreshed", Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
qrStatusTextView.setText("Failed to refresh QR - trying again in 30s");
|
|
||||||
countdownSeconds = 30;
|
|
||||||
}
|
|
||||||
qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000);
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResult", "QR refresh error: " + e.getMessage(), e);
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
qrStatusTextView.setText("QR refresh error - retrying in 30s");
|
|
||||||
countdownSeconds = 30;
|
|
||||||
qrRefreshHandler.postDelayed(qrRefreshRunnable, 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateNewQrCode() {
|
|
||||||
try {
|
|
||||||
String newOrderId = java.util.UUID.randomUUID().toString();
|
|
||||||
|
|
||||||
JSONObject customField = new JSONObject();
|
|
||||||
customField.put("refresh_of", orderId);
|
|
||||||
customField.put("refresh_time", getCurrentISOTime());
|
|
||||||
customField.put("original_reference", referenceId);
|
|
||||||
|
|
||||||
JSONObject payload = createQrisPayload(newOrderId, customField);
|
|
||||||
String response = sendMidtransRequest(payload);
|
|
||||||
|
|
||||||
if (response != null) {
|
|
||||||
JSONObject jsonResponse = new JSONObject(response);
|
|
||||||
if (jsonResponse.has("actions")) {
|
|
||||||
JSONArray actions = jsonResponse.getJSONArray("actions");
|
|
||||||
if (actions.length() > 0) {
|
|
||||||
String newQrUrl = actions.getJSONObject(0).getString("url");
|
|
||||||
|
|
||||||
// Update transaction info
|
|
||||||
this.transactionId = jsonResponse.optString("transaction_id", transactionId);
|
|
||||||
this.transactionTime = jsonResponse.optString("transaction_time", transactionTime);
|
|
||||||
this.orderId = newOrderId;
|
|
||||||
|
|
||||||
return newQrUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResult", "Generate QR error: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JSONObject createQrisPayload(String orderIdParam, JSONObject customField) throws Exception {
|
|
||||||
JSONObject payload = new JSONObject();
|
|
||||||
payload.put("payment_type", "qris");
|
|
||||||
|
|
||||||
JSONObject transactionDetails = new JSONObject();
|
|
||||||
transactionDetails.put("order_id", orderIdParam);
|
|
||||||
transactionDetails.put("gross_amount", originalAmount);
|
|
||||||
payload.put("transaction_details", transactionDetails);
|
|
||||||
|
|
||||||
JSONObject customerDetails = new JSONObject();
|
|
||||||
customerDetails.put("first_name", "Test");
|
|
||||||
customerDetails.put("last_name", "Customer");
|
|
||||||
customerDetails.put("email", "test@example.com");
|
|
||||||
customerDetails.put("phone", "081234567890");
|
|
||||||
payload.put("customer_details", customerDetails);
|
|
||||||
|
|
||||||
JSONArray itemDetails = new JSONArray();
|
|
||||||
JSONObject item = new JSONObject();
|
|
||||||
item.put("id", "item1_refresh_" + System.currentTimeMillis());
|
|
||||||
item.put("price", originalAmount);
|
|
||||||
item.put("quantity", 1);
|
|
||||||
item.put("name", "QRIS Payment - Refreshed (Ref: " + referenceId + ")");
|
|
||||||
itemDetails.put(item);
|
|
||||||
payload.put("item_details", itemDetails);
|
|
||||||
|
|
||||||
payload.put("custom_field1", customField.toString());
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String sendMidtransRequest(JSONObject payload) {
|
|
||||||
try {
|
|
||||||
URL url = new URI(MIDTRANS_CHARGE_URL).toURL();
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
|
|
||||||
conn.setRequestProperty("X-Override-Notification", WEBHOOK_URL);
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
conn.setConnectTimeout(30000);
|
|
||||||
conn.setReadTimeout(30000);
|
|
||||||
|
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
|
||||||
byte[] input = payload.toString().getBytes("utf-8");
|
|
||||||
os.write(input, 0, input.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
if (responseCode == 200 || responseCode == 201) {
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = br.readLine()) != null) {
|
|
||||||
response.append(line.trim());
|
|
||||||
}
|
|
||||||
return response.toString();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResult", "Midtrans request error: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startPaymentMonitoring() {
|
|
||||||
Handler paymentHandler = new Handler(Looper.getMainLooper());
|
|
||||||
Runnable paymentRunnable = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
checkAllOrderIdsStatus();
|
|
||||||
if (!isFinishing() && isQrRefreshActive) {
|
|
||||||
paymentHandler.postDelayed(this, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
paymentHandler.post(paymentRunnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkAllOrderIdsStatus() {
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
for (String checkOrderId : allOrderIds) {
|
|
||||||
if (checkOrderId == null || checkOrderId.isEmpty()) continue;
|
|
||||||
|
|
||||||
if (checkPaymentStatus(checkOrderId)) {
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
stopQrRefresh();
|
|
||||||
syncTransactionStatusToBackend("PAID");
|
|
||||||
showPaymentSuccess();
|
|
||||||
Toast.makeText(this, "Payment Successful! 🎉", Toast.LENGTH_LONG).show();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResult", "Payment status check error: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkPaymentStatus(String checkOrderId) {
|
|
||||||
try {
|
|
||||||
String urlStr = BACKEND_BASE + "/api-logs?request_body_search_strict=" +
|
|
||||||
java.net.URLEncoder.encode("{\"order_id\":\"" + checkOrderId + "\"}", "UTF-8");
|
|
||||||
|
|
||||||
URL url = new URL(urlStr);
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setConnectTimeout(5000);
|
|
||||||
conn.setReadTimeout(5000);
|
|
||||||
|
|
||||||
if (conn.getResponseCode() == 200) {
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = br.readLine()) != null) {
|
|
||||||
response.append(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject json = new JSONObject(response.toString());
|
|
||||||
JSONArray results = json.optJSONArray("results");
|
|
||||||
|
|
||||||
if (results != null && results.length() > 0) {
|
|
||||||
for (int i = 0; i < results.length(); i++) {
|
|
||||||
JSONObject log = results.getJSONObject(i);
|
|
||||||
JSONObject reqBody = log.optJSONObject("request_body");
|
|
||||||
|
|
||||||
if (reqBody != null) {
|
|
||||||
String transactionStatus = reqBody.optString("transaction_status");
|
|
||||||
String logOrderId = reqBody.optString("order_id");
|
|
||||||
|
|
||||||
if (checkOrderId.equals(logOrderId) &&
|
|
||||||
(transactionStatus.equals("settlement") ||
|
|
||||||
transactionStatus.equals("capture") ||
|
|
||||||
transactionStatus.equals("success"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResult", "Payment check error: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadQrImage(String qrImageUrl) {
|
|
||||||
if (qrImageUrl != null && !qrImageUrl.isEmpty()) {
|
|
||||||
new DownloadImageTask(qrImageView).execute(qrImageUrl);
|
|
||||||
} else {
|
|
||||||
qrImageView.setVisibility(View.GONE);
|
|
||||||
downloadQrisButton.setEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupClickListeners() {
|
|
||||||
downloadQrisButton.setOnClickListener(v -> downloadQrCode());
|
|
||||||
checkStatusButton.setOnClickListener(v -> {
|
|
||||||
stopQrRefresh();
|
|
||||||
simulateWebhook();
|
|
||||||
});
|
|
||||||
returnMainButton.setOnClickListener(v -> returnToMain());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopQrRefresh() {
|
|
||||||
isQrRefreshActive = false;
|
|
||||||
if (qrRefreshHandler != null && qrRefreshRunnable != null) {
|
|
||||||
qrRefreshHandler.removeCallbacks(qrRefreshRunnable);
|
|
||||||
}
|
|
||||||
timerTextView.setVisibility(View.GONE);
|
|
||||||
qrStatusTextView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showPaymentSuccess() {
|
|
||||||
stopQrRefresh();
|
|
||||||
|
|
||||||
qrImageView.setVisibility(View.GONE);
|
|
||||||
amountTextView.setVisibility(View.GONE);
|
|
||||||
referenceTextView.setVisibility(View.GONE);
|
|
||||||
downloadQrisButton.setVisibility(View.GONE);
|
|
||||||
checkStatusButton.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
statusTextView.setText("✅ Payment Successful!\n\nTransaction ID: " + transactionId +
|
|
||||||
"\nReference: " + referenceId +
|
|
||||||
"\nAmount: " + formatCurrency(grossAmount));
|
|
||||||
|
|
||||||
returnMainButton.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
new Handler(Looper.getMainLooper()).postDelayed(this::launchReceiptActivity, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void syncTransactionStatusToBackend(String status) {
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
JSONObject updatePayload = new JSONObject();
|
|
||||||
updatePayload.put("status", status);
|
|
||||||
updatePayload.put("transaction_status", status);
|
|
||||||
updatePayload.put("updated_at", getCurrentISOTime());
|
|
||||||
updatePayload.put("settlement_time", getCurrentISOTime());
|
|
||||||
|
|
||||||
JSONObject body = new JSONObject();
|
|
||||||
body.put("reference_id", referenceId);
|
|
||||||
body.put("update_data", updatePayload);
|
|
||||||
|
|
||||||
String updateUrl = BACKEND_BASE + "/transactions/update-by-reference";
|
|
||||||
URL url = new URI(updateUrl).toURL();
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
|
||||||
byte[] input = body.toString().getBytes("utf-8");
|
|
||||||
os.write(input, 0, input.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
Log.d("QrisResult", "Backend sync response: " + responseCode);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResult", "Backend sync error: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void simulateWebhook() {
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
statusTextView.setText("Simulating payment...");
|
|
||||||
checkStatusButton.setEnabled(false);
|
|
||||||
stopQrRefresh();
|
|
||||||
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
JSONObject payload = createWebhookPayload();
|
|
||||||
sendWebhookRequest(payload);
|
|
||||||
Thread.sleep(2000);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResult", "Webhook simulation error: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
showPaymentSuccess();
|
|
||||||
});
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private JSONObject createWebhookPayload() throws Exception {
|
|
||||||
String serverKey = getServerKey();
|
|
||||||
String signatureKey = generateSignature(orderId, "200", grossAmount, serverKey);
|
|
||||||
|
|
||||||
JSONObject payload = new JSONObject();
|
|
||||||
payload.put("transaction_type", "on-us");
|
|
||||||
payload.put("transaction_time", transactionTime != null ? transactionTime : getCurrentISOTime());
|
|
||||||
payload.put("transaction_status", "settlement");
|
|
||||||
payload.put("transaction_id", transactionId);
|
|
||||||
payload.put("status_message", "midtrans payment notification");
|
|
||||||
payload.put("status_code", "200");
|
|
||||||
payload.put("signature_key", signatureKey);
|
|
||||||
payload.put("payment_type", "qris");
|
|
||||||
payload.put("order_id", orderId);
|
|
||||||
payload.put("gross_amount", grossAmount);
|
|
||||||
payload.put("reference_id", referenceId);
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendWebhookRequest(JSONObject payload) {
|
|
||||||
try {
|
|
||||||
URL url = new URL(WEBHOOK_URL);
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
|
||||||
os.write(payload.toString().getBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d("QrisResult", "Webhook response: " + conn.getResponseCode());
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResult", "Webhook request error: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility Methods
|
|
||||||
private String formatCurrency(String amount) {
|
|
||||||
try {
|
|
||||||
double amountDouble = Double.parseDouble(amount);
|
|
||||||
NumberFormat formatter = NumberFormat.getCurrencyInstance(new Locale("id", "ID"));
|
|
||||||
return formatter.format(amountDouble);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return "IDR " + amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getCurrentISOTime() {
|
|
||||||
return new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
|
||||||
.format(new java.util.Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getServerKey() {
|
|
||||||
try {
|
|
||||||
String base64 = MIDTRANS_AUTH.replace("Basic ", "");
|
|
||||||
byte[] decoded = android.util.Base64.decode(base64, android.util.Base64.DEFAULT);
|
|
||||||
return new String(decoded).replace(":", "");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateSignature(String orderId, String statusCode, String grossAmount, String serverKey) {
|
|
||||||
String input = orderId + statusCode + grossAmount + serverKey;
|
|
||||||
try {
|
|
||||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-512");
|
|
||||||
byte[] messageDigest = md.digest(input.getBytes());
|
|
||||||
StringBuilder hexString = new StringBuilder();
|
|
||||||
for (byte b : messageDigest) {
|
|
||||||
String hex = Integer.toHexString(0xff & b);
|
|
||||||
if (hex.length() == 1) hexString.append('0');
|
|
||||||
hexString.append(hex);
|
|
||||||
}
|
|
||||||
return hexString.toString();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "dummy_signature";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation and Lifecycle
|
|
||||||
private void returnToMain() {
|
|
||||||
stopQrRefresh();
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
startActivity(intent);
|
|
||||||
finishAffinity();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void launchReceiptActivity() {
|
|
||||||
Intent intent = new Intent(this, ReceiptActivity.class);
|
|
||||||
intent.putExtra("calling_activity", "QrisResultActivity");
|
|
||||||
intent.putExtra("transaction_id", transactionId);
|
|
||||||
intent.putExtra("reference_id", referenceId);
|
|
||||||
intent.putExtra("order_id", orderId);
|
|
||||||
intent.putExtra("transaction_amount", String.valueOf(originalAmount));
|
|
||||||
intent.putExtra("gross_amount", grossAmount != null ? grossAmount : String.valueOf(originalAmount));
|
|
||||||
intent.putExtra("created_at", getCurrentISOTime());
|
|
||||||
intent.putExtra("payment_method", "QRIS");
|
|
||||||
intent.putExtra("acquirer", acquirer != null ? acquirer : "qris");
|
|
||||||
intent.putExtra("mid", "71000026521");
|
|
||||||
intent.putExtra("tid", "73001500");
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadQrCode() {
|
|
||||||
try {
|
|
||||||
qrImageView.setDrawingCacheEnabled(true);
|
|
||||||
qrImageView.buildDrawingCache();
|
|
||||||
Bitmap bitmap = qrImageView.getDrawingCache();
|
|
||||||
if (bitmap != null) {
|
|
||||||
saveImageToGallery(bitmap, "qris_code_" + System.currentTimeMillis());
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Unable to capture QR code image", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Toast.makeText(this, "Error downloading QR code", Toast.LENGTH_LONG).show();
|
|
||||||
} finally {
|
|
||||||
qrImageView.setDrawingCacheEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveImageToGallery(Bitmap bitmap, String fileName) {
|
|
||||||
try {
|
|
||||||
String savedImageURL = android.provider.MediaStore.Images.Media.insertImage(
|
|
||||||
getContentResolver(), bitmap, fileName, "QRIS Payment QR Code");
|
|
||||||
if (savedImageURL != null) {
|
|
||||||
Toast.makeText(this, "QRIS saved to gallery", Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Failed to save QRIS", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Toast.makeText(this, "Error saving QRIS", Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUIAfterRefresh() {
|
|
||||||
String refreshTime = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date());
|
|
||||||
referenceTextView.setText("Reference ID: " + referenceId + " (Refreshed at " + refreshTime + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void pollPendingPaymentLog(String orderId) {
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
statusTextView.setText("Checking payment status...");
|
|
||||||
|
|
||||||
new Thread(() -> {
|
|
||||||
int maxAttempts = 12;
|
|
||||||
boolean found = false;
|
|
||||||
|
|
||||||
for (int attempt = 0; attempt < maxAttempts && !found; attempt++) {
|
|
||||||
try {
|
|
||||||
found = checkPaymentStatus(orderId);
|
|
||||||
if (!found && attempt < maxAttempts - 1) {
|
|
||||||
Thread.sleep(2000);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("QrisResult", "Polling error: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final boolean logFound = found;
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
if (logFound) {
|
|
||||||
checkStatusButton.setEnabled(true);
|
|
||||||
statusTextView.setText("Ready to simulate payment");
|
|
||||||
Toast.makeText(this, "Payment log found!", Toast.LENGTH_SHORT).show();
|
|
||||||
} else {
|
|
||||||
statusTextView.setText("Payment log not found");
|
|
||||||
checkStatusButton.setEnabled(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
stopQrRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
stopQrRefresh();
|
|
||||||
returnToMain();
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsyncTask for downloading QR image
|
|
||||||
private static class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
|
|
||||||
private ImageView imageView;
|
|
||||||
private String errorMessage;
|
|
||||||
|
|
||||||
DownloadImageTask(ImageView imageView) {
|
|
||||||
this.imageView = imageView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Bitmap doInBackground(String... urls) {
|
|
||||||
try {
|
|
||||||
URL url = new URI(urls[0]).toURL();
|
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
||||||
connection.setDoInput(true);
|
|
||||||
connection.setConnectTimeout(10000);
|
|
||||||
connection.setReadTimeout(10000);
|
|
||||||
connection.connect();
|
|
||||||
|
|
||||||
if (connection.getResponseCode() == 200) {
|
|
||||||
InputStream input = connection.getInputStream();
|
|
||||||
return BitmapFactory.decodeStream(input);
|
|
||||||
} else {
|
|
||||||
errorMessage = "Failed to download QR code";
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
errorMessage = "Error downloading QR code: " + e.getMessage();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Bitmap result) {
|
|
||||||
if (result != null) {
|
|
||||||
imageView.setImageBitmap(result);
|
|
||||||
} else {
|
|
||||||
imageView.setImageResource(android.R.drawable.ic_menu_report_image);
|
|
||||||
if (errorMessage != null && imageView.getContext() != null) {
|
|
||||||
Toast.makeText(imageView.getContext(), errorMessage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.bdkipoc;
|
package com.example.bdkipoc.cetakulang;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.R;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -50,9 +51,12 @@ import java.util.TimeZone;
|
|||||||
import android.app.DatePickerDialog;
|
import android.app.DatePickerDialog;
|
||||||
import android.widget.DatePicker;
|
import android.widget.DatePicker;
|
||||||
|
|
||||||
public class TransactionActivity extends AppCompatActivity implements TransactionAdapter.OnPrintClickListener {
|
import com.example.bdkipoc.ReceiptActivity;
|
||||||
|
import com.example.bdkipoc.StyleHelper;
|
||||||
|
|
||||||
|
public class ReprintActivity extends AppCompatActivity implements ReprintAdapterActivity.OnPrintClickListener {
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private TransactionAdapter adapter;
|
private ReprintAdapterActivity adapter;
|
||||||
private List<Transaction> transactionList;
|
private List<Transaction> transactionList;
|
||||||
private List<Transaction> filteredList;
|
private List<Transaction> filteredList;
|
||||||
private ProgressBar progressBar;
|
private ProgressBar progressBar;
|
||||||
@@ -89,7 +93,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_transaction);
|
setContentView(R.layout.activity_reprint);
|
||||||
|
|
||||||
// ✅ Initialize SharedPreferences for local tracking
|
// ✅ Initialize SharedPreferences for local tracking
|
||||||
prefs = getSharedPreferences("transaction_prefs", MODE_PRIVATE);
|
prefs = getSharedPreferences("transaction_prefs", MODE_PRIVATE);
|
||||||
@@ -98,7 +102,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
initViews();
|
initViews();
|
||||||
|
|
||||||
// Setup toolbar
|
// Setup toolbar
|
||||||
setupToolbar();
|
setupAppbar();
|
||||||
|
|
||||||
// Setup RecyclerView
|
// Setup RecyclerView
|
||||||
setupRecyclerView();
|
setupRecyclerView();
|
||||||
@@ -140,26 +144,25 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
StyleHelper.applyPaginationButtonStyle(btnLastPage, this, false);
|
StyleHelper.applyPaginationButtonStyle(btnLastPage, this, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupToolbar() {
|
private void setupAppbar() {
|
||||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
// Setup back navigation click listener
|
||||||
setSupportActionBar(toolbar);
|
LinearLayout backNavigation = findViewById(R.id.back_navigation);
|
||||||
|
if (backNavigation != null) {
|
||||||
// Hide default title since we're using custom layout
|
backNavigation.setOnClickListener(v -> finish());
|
||||||
if (getSupportActionBar() != null) {
|
|
||||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup custom back button
|
// Alternative: You can also set click listener on the back arrow directly
|
||||||
ImageView backButton = findViewById(R.id.backButton);
|
ImageView backArrow = findViewById(R.id.backArrow);
|
||||||
backButton.setOnClickListener(v -> finish());
|
if (backArrow != null) {
|
||||||
|
backArrow.setOnClickListener(v -> finish());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView() {
|
private void setupRecyclerView() {
|
||||||
transactionList = new ArrayList<>();
|
transactionList = new ArrayList<>();
|
||||||
filteredList = new ArrayList<>();
|
filteredList = new ArrayList<>();
|
||||||
|
|
||||||
adapter = new TransactionAdapter(filteredList);
|
adapter = new ReprintAdapterActivity(filteredList);
|
||||||
adapter.setPrintClickListener(this);
|
adapter.setPrintClickListener(this);
|
||||||
|
|
||||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||||
@@ -342,13 +345,13 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
filterButtonText.setTextColor(getResources().getColor(android.R.color.holo_blue_dark));
|
filterButtonText.setTextColor(getResources().getColor(android.R.color.holo_blue_dark));
|
||||||
filterButtonText.setTextSize(12); // Smaller text when filter is active
|
filterButtonText.setTextSize(12); // Smaller text when filter is active
|
||||||
|
|
||||||
Log.d("TransactionActivity", "🎨 Filter button updated: " + displayText);
|
Log.d("ReprintActivity", "🎨 Filter button updated: " + displayText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEW METHOD: Apply date filter
|
// ✅ NEW METHOD: Apply date filter
|
||||||
private void applyDateFilter() {
|
private void applyDateFilter() {
|
||||||
Log.d("TransactionActivity", "🗓️ Applying date filter: " + fromDate + " to " + toDate);
|
Log.d("ReprintActivity", "🗓️ Applying date filter: " + fromDate + " to " + toDate);
|
||||||
|
|
||||||
// Reset to first page and reload data
|
// Reset to first page and reload data
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
@@ -366,7 +369,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
filterButtonText.setTextSize(14); // Reset to normal size
|
filterButtonText.setTextSize(14); // Reset to normal size
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionActivity", "🗓️ Date filter cleared");
|
Log.d("ReprintActivity", "🗓️ Date filter cleared");
|
||||||
|
|
||||||
// Reload data without date filter
|
// Reload data without date filter
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
@@ -377,7 +380,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionActivity", "🔄 Navigating to page " + page);
|
Log.d("ReprintActivity", "🔄 Navigating to page " + page);
|
||||||
|
|
||||||
if (currentSearchQuery.isEmpty()) {
|
if (currentSearchQuery.isEmpty()) {
|
||||||
// Load from API
|
// Load from API
|
||||||
@@ -410,7 +413,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
// Scroll to top
|
// Scroll to top
|
||||||
recyclerView.scrollToPosition(0);
|
recyclerView.scrollToPosition(0);
|
||||||
|
|
||||||
Log.d("TransactionActivity", "📄 Displaying search results page " + currentPage +
|
Log.d("ReprintActivity", "📄 Displaying search results page " + currentPage +
|
||||||
" (items " + (startIndex + 1) + "-" + endIndex + " of " + filteredList.size() + ")");
|
" (items " + (startIndex + 1) + "-" + endIndex + " of " + filteredList.size() + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,10 +455,10 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
totalPages = (int) Math.ceil((double) totalRecords / itemsPerPage);
|
totalPages = (int) Math.ceil((double) totalRecords / itemsPerPage);
|
||||||
|
|
||||||
// ✅ PASTIKAN TIDAK PERLU SORT LAGI karena sudah sorted dari API response
|
// ✅ PASTIKAN TIDAK PERLU SORT LAGI karena sudah sorted dari API response
|
||||||
Log.d("TransactionActivity", "📋 FILTERED LIST ORDER (no search - maintaining API order):");
|
Log.d("ReprintActivity", "📋 FILTERED LIST ORDER (no search - maintaining API order):");
|
||||||
for (int i = 0; i < Math.min(5, filteredList.size()); i++) {
|
for (int i = 0; i < Math.min(5, filteredList.size()); i++) {
|
||||||
Transaction tx = filteredList.get(i);
|
Transaction tx = filteredList.get(i);
|
||||||
Log.d("TransactionActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
|
Log.d("ReprintActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ✅ SEARCH MODE: Filter all available data
|
// ✅ SEARCH MODE: Filter all available data
|
||||||
@@ -518,7 +521,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
paginationControls.setVisibility(View.GONE);
|
paginationControls.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionActivity", "📊 Pagination updated: " +
|
Log.d("ReprintActivity", "📊 Pagination updated: " +
|
||||||
"Page " + currentPage + "/" + totalPages + ", Total: " + totalRecords);
|
"Page " + currentPage + "/" + totalPages + ", Total: " + totalRecords);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +587,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
pageNumbersContainer.addView(pageButton);
|
pageNumbersContainer.addView(pageButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionActivity", "🔢 Page buttons created: " + startPage + " to " + endPage +
|
Log.d("ReprintActivity", "🔢 Page buttons created: " + startPage + " to " + endPage +
|
||||||
" with size: " + buttonSize + "px");
|
" with size: " + buttonSize + "px");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,9 +614,9 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
int apiPage = pageToLoad - 1; // API uses 0-based indexing
|
int apiPage = pageToLoad - 1; // API uses 0-based indexing
|
||||||
|
|
||||||
String urlString = "https://be-edc.msvc.app/transactions?page=" + apiPage +
|
String urlString = "https://be-edc.msvc.app/transactions?page=" + apiPage +
|
||||||
"&limit=" + itemsPerPage + "&sortOrder=DESC&from_date=" + fromDate + "&to_date=" + toDate + "&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=created_at";
|
"&limit=" + itemsPerPage + "&sortOrder=DESC&from_date=" + fromDate + "&to_date=" + toDate + "&location_id=0&merchant_id=0&tid=&mid=&sortColumn=created_at";
|
||||||
|
|
||||||
Log.d("TransactionActivity", "🔍 Fetching transactions page " + pageToLoad +
|
Log.d("ReprintActivity", "🔍 Fetching transactions page " + pageToLoad +
|
||||||
" (API page " + apiPage + ") with limit " + itemsPerPage + " - SORT: DESC by created_at" +
|
" (API page " + apiPage + ") with limit " + itemsPerPage + " - SORT: DESC by created_at" +
|
||||||
" - Date Filter: " + fromDate + " to " + toDate);
|
" - Date Filter: " + fromDate + " to " + toDate);
|
||||||
|
|
||||||
@@ -642,7 +645,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
apiTotal = results.getInt("total");
|
apiTotal = results.getInt("total");
|
||||||
JSONArray data = results.getJSONArray("data");
|
JSONArray data = results.getJSONArray("data");
|
||||||
|
|
||||||
Log.d("TransactionActivity", "📊 API response: " + data.length() +
|
Log.d("ReprintActivity", "📊 API response: " + data.length() +
|
||||||
" records, total: " + apiTotal);
|
" records, total: " + apiTotal);
|
||||||
|
|
||||||
// ✅ STEP 1: Parse all transactions from API
|
// ✅ STEP 1: Parse all transactions from API
|
||||||
@@ -667,14 +670,14 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
// ✅ STEP 2: Apply intelligent deduplication
|
// ✅ STEP 2: Apply intelligent deduplication
|
||||||
result = applyAdvancedDeduplication(rawTransactions);
|
result = applyAdvancedDeduplication(rawTransactions);
|
||||||
|
|
||||||
Log.d("TransactionActivity", "✅ After advanced deduplication: " + result.size() + " unique transactions");
|
Log.d("ReprintActivity", "✅ After advanced deduplication: " + result.size() + " unique transactions");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Log.e("TransactionActivity", "❌ HTTP Error: " + responseCode);
|
Log.e("ReprintActivity", "❌ HTTP Error: " + responseCode);
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
} catch (IOException | JSONException | URISyntaxException e) {
|
} catch (IOException | JSONException | URISyntaxException e) {
|
||||||
Log.e("TransactionActivity", "❌ Exception: " + e.getMessage(), e);
|
Log.e("ReprintActivity", "❌ Exception: " + e.getMessage(), e);
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -687,7 +690,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
progressBar.setVisibility(View.GONE);
|
progressBar.setVisibility(View.GONE);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
Toast.makeText(TransactionActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
|
Toast.makeText(ReprintActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
|
||||||
updatePaginationDisplay();
|
updatePaginationDisplay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -705,11 +708,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
|
|
||||||
if (date1 != null && date2 != null) {
|
if (date1 != null && date2 != null) {
|
||||||
int comparison = date2.compareTo(date1); // Newest first
|
int comparison = date2.compareTo(date1); // Newest first
|
||||||
Log.d("TransactionActivity", "🔄 Sorting: " + t2.createdAt + " vs " + t1.createdAt + " = " + comparison);
|
Log.d("ReprintActivity", "🔄 Sorting: " + t2.createdAt + " vs " + t1.createdAt + " = " + comparison);
|
||||||
return comparison;
|
return comparison;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w("TransactionActivity", "Date comparison error: " + e.getMessage());
|
Log.w("ReprintActivity", "Date comparison error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
return Integer.compare(t2.id, t1.id); // Fallback by ID (higher ID = newer)
|
return Integer.compare(t2.id, t1.id); // Fallback by ID (higher ID = newer)
|
||||||
});
|
});
|
||||||
@@ -718,14 +721,14 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
transactionList.clear();
|
transactionList.clear();
|
||||||
transactionList.addAll(transactions);
|
transactionList.addAll(transactions);
|
||||||
|
|
||||||
Log.d("TransactionActivity", "📋 Page " + currentPage + " loaded and sorted: " +
|
Log.d("ReprintActivity", "📋 Page " + currentPage + " loaded and sorted: " +
|
||||||
transactions.size() + " transactions. Total: " + totalRecords + "/" + totalPages + " pages");
|
transactions.size() + " transactions. Total: " + totalRecords + "/" + totalPages + " pages");
|
||||||
|
|
||||||
// ✅ VERIFIKASI SORTING ORDER
|
// ✅ VERIFIKASI SORTING ORDER
|
||||||
Log.d("TransactionActivity", "📋 SORTED ORDER VERIFICATION:");
|
Log.d("ReprintActivity", "📋 SORTED ORDER VERIFICATION:");
|
||||||
for (int i = 0; i < Math.min(5, transactionList.size()); i++) {
|
for (int i = 0; i < Math.min(5, transactionList.size()); i++) {
|
||||||
Transaction tx = transactionList.get(i);
|
Transaction tx = transactionList.get(i);
|
||||||
Log.d("TransactionActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
|
Log.d("ReprintActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update filtered list based on current search
|
// Update filtered list based on current search
|
||||||
@@ -761,7 +764,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
|
SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
|
||||||
sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // Handle timezone properly
|
sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // Handle timezone properly
|
||||||
Date parsed = sdf.parse(rawDate);
|
Date parsed = sdf.parse(rawDate);
|
||||||
Log.d("TransactionActivity", "✅ Date parsed successfully: " + rawDate + " -> " + parsed + " using format: " + format);
|
Log.d("ReprintActivity", "✅ Date parsed successfully: " + rawDate + " -> " + parsed + " using format: " + format);
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Continue to next format
|
// Continue to next format
|
||||||
@@ -779,10 +782,10 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
|
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||||
Date parsed = sdf.parse(cleanedDate);
|
Date parsed = sdf.parse(cleanedDate);
|
||||||
Log.d("TransactionActivity", "✅ Date parsed with fallback: " + rawDate + " -> " + parsed);
|
Log.d("ReprintActivity", "✅ Date parsed with fallback: " + rawDate + " -> " + parsed);
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w("TransactionActivity", "❌ Could not parse date: " + rawDate + " - Error: " + e.getMessage());
|
Log.w("ReprintActivity", "❌ Could not parse date: " + rawDate + " - Error: " + e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -791,11 +794,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
* ✅ ADVANCED DEDUPLICATION: Enhanced algorithm with multiple strategies
|
* ✅ ADVANCED DEDUPLICATION: Enhanced algorithm with multiple strategies
|
||||||
*/
|
*/
|
||||||
private List<Transaction> applyAdvancedDeduplication(List<Transaction> rawTransactions) {
|
private List<Transaction> applyAdvancedDeduplication(List<Transaction> rawTransactions) {
|
||||||
Log.d("TransactionActivity", "🧠 Starting advanced deduplication...");
|
Log.d("ReprintActivity", "🧠 Starting advanced deduplication...");
|
||||||
Log.d("TransactionActivity", "📥 Input transactions order (first 5):");
|
Log.d("ReprintActivity", "📥 Input transactions order (first 5):");
|
||||||
for (int i = 0; i < Math.min(5, rawTransactions.size()); i++) {
|
for (int i = 0; i < Math.min(5, rawTransactions.size()); i++) {
|
||||||
Transaction tx = rawTransactions.get(i);
|
Transaction tx = rawTransactions.get(i);
|
||||||
Log.d("TransactionActivity", " " + (i+1) + ". ID:" + tx.id + " Date:" + tx.createdAt + " Ref:" + tx.referenceId);
|
Log.d("ReprintActivity", " " + (i+1) + ". ID:" + tx.id + " Date:" + tx.createdAt + " Ref:" + tx.referenceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 1: Group by reference_id
|
// Strategy 1: Group by reference_id
|
||||||
@@ -823,7 +826,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
if (group.size() == 1) {
|
if (group.size() == 1) {
|
||||||
// No duplicates for this reference
|
// No duplicates for this reference
|
||||||
deduplicatedList.add(group.get(0));
|
deduplicatedList.add(group.get(0));
|
||||||
Log.d("TransactionActivity", "✅ Unique transaction: " + referenceId);
|
Log.d("ReprintActivity", "✅ Unique transaction: " + referenceId);
|
||||||
} else {
|
} else {
|
||||||
// Multiple transactions with same reference_id - sort group by date first
|
// Multiple transactions with same reference_id - sort group by date first
|
||||||
Collections.sort(group, (t1, t2) -> {
|
Collections.sort(group, (t1, t2) -> {
|
||||||
@@ -843,15 +846,15 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
deduplicatedList.add(bestTransaction);
|
deduplicatedList.add(bestTransaction);
|
||||||
duplicatesRemoved += (group.size() - 1);
|
duplicatesRemoved += (group.size() - 1);
|
||||||
|
|
||||||
Log.d("TransactionActivity", "🔄 Deduplicated " + group.size() + " → 1 for ref: " + referenceId +
|
Log.d("ReprintActivity", "🔄 Deduplicated " + group.size() + " → 1 for ref: " + referenceId +
|
||||||
" (kept ID: " + bestTransaction.id + ", status: " + bestTransaction.status + ", date: " + bestTransaction.createdAt + ")");
|
" (kept ID: " + bestTransaction.id + ", status: " + bestTransaction.status + ", date: " + bestTransaction.createdAt + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionActivity", "✅ Advanced deduplication complete:");
|
Log.d("ReprintActivity", "✅ Advanced deduplication complete:");
|
||||||
Log.d("TransactionActivity", " 📥 Input: " + rawTransactions.size() + " transactions");
|
Log.d("ReprintActivity", " 📥 Input: " + rawTransactions.size() + " transactions");
|
||||||
Log.d("TransactionActivity", " 📤 Output: " + deduplicatedList.size() + " unique transactions");
|
Log.d("ReprintActivity", " 📤 Output: " + deduplicatedList.size() + " unique transactions");
|
||||||
Log.d("TransactionActivity", " 🗑️ Removed: " + duplicatesRemoved + " duplicates");
|
Log.d("ReprintActivity", " 🗑️ Removed: " + duplicatesRemoved + " duplicates");
|
||||||
|
|
||||||
return deduplicatedList;
|
return deduplicatedList;
|
||||||
}
|
}
|
||||||
@@ -860,7 +863,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
* ✅ ENHANCED SELECTION: Advanced algorithm to pick the best transaction
|
* ✅ ENHANCED SELECTION: Advanced algorithm to pick the best transaction
|
||||||
*/
|
*/
|
||||||
private Transaction selectBestTransactionAdvanced(List<Transaction> duplicates, String referenceId) {
|
private Transaction selectBestTransactionAdvanced(List<Transaction> duplicates, String referenceId) {
|
||||||
Log.d("TransactionActivity", "🎯 Selecting best from " + duplicates.size() + " duplicates for: " + referenceId);
|
Log.d("ReprintActivity", "🎯 Selecting best from " + duplicates.size() + " duplicates for: " + referenceId);
|
||||||
|
|
||||||
Transaction bestTransaction = duplicates.get(0);
|
Transaction bestTransaction = duplicates.get(0);
|
||||||
int bestPriority = getStatusPriority(bestTransaction.status);
|
int bestPriority = getStatusPriority(bestTransaction.status);
|
||||||
@@ -871,7 +874,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
int currentPriority = getStatusPriority(tx.status);
|
int currentPriority = getStatusPriority(tx.status);
|
||||||
Date currentDate = parseCreatedAtDate(tx.createdAt);
|
Date currentDate = parseCreatedAtDate(tx.createdAt);
|
||||||
|
|
||||||
Log.d("TransactionActivity", " 📊 Candidate: ID=" + tx.id +
|
Log.d("ReprintActivity", " 📊 Candidate: ID=" + tx.id +
|
||||||
", Status=" + tx.status + " (priority=" + currentPriority + ")" +
|
", Status=" + tx.status + " (priority=" + currentPriority + ")" +
|
||||||
", Created=" + tx.createdAt);
|
", Created=" + tx.createdAt);
|
||||||
|
|
||||||
@@ -903,11 +906,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
bestTransaction = tx;
|
bestTransaction = tx;
|
||||||
bestPriority = currentPriority;
|
bestPriority = currentPriority;
|
||||||
bestDate = currentDate;
|
bestDate = currentDate;
|
||||||
Log.d("TransactionActivity", " ⭐ NEW BEST selected: " + reason);
|
Log.d("ReprintActivity", " ⭐ NEW BEST selected: " + reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionActivity", "🏆 FINAL SELECTION: ID=" + bestTransaction.id +
|
Log.d("ReprintActivity", "🏆 FINAL SELECTION: ID=" + bestTransaction.id +
|
||||||
", Status=" + bestTransaction.status + ", Created=" + bestTransaction.createdAt);
|
", Status=" + bestTransaction.status + ", Created=" + bestTransaction.createdAt);
|
||||||
|
|
||||||
return bestTransaction;
|
return bestTransaction;
|
||||||
@@ -925,7 +928,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
return date1.after(date2);
|
return date1.after(date2);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w("TransactionActivity", "Date comparison error, falling back to ID comparison");
|
Log.w("ReprintActivity", "Date comparison error, falling back to ID comparison");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: higher ID usually means newer
|
// Fallback: higher ID usually means newer
|
||||||
@@ -990,7 +993,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
|
|
||||||
// Tier 5: Unknown status
|
// Tier 5: Unknown status
|
||||||
default:
|
default:
|
||||||
Log.w("TransactionActivity", "Unknown status encountered: " + status);
|
Log.w("ReprintActivity", "Unknown status encountered: " + status);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -998,93 +1001,196 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
// ✅ REST OF THE CLASS: Existing methods remain the same
|
// ✅ REST OF THE CLASS: Existing methods remain the same
|
||||||
@Override
|
@Override
|
||||||
public void onPrintClick(Transaction transaction) {
|
public void onPrintClick(Transaction transaction) {
|
||||||
// Open ReceiptActivity with transaction data
|
Log.d("ReprintActivity", "=== OPENING RECEIPT FOR TRANSACTION ===");
|
||||||
|
Log.d("ReprintActivity", "📋 Transaction details:");
|
||||||
|
Log.d("ReprintActivity", " Reference ID: " + transaction.referenceId);
|
||||||
|
Log.d("ReprintActivity", " Channel Code: " + transaction.channelCode);
|
||||||
|
Log.d("ReprintActivity", " Channel Category: " + transaction.channelCategory);
|
||||||
|
Log.d("ReprintActivity", " Amount: " + transaction.amount);
|
||||||
|
Log.d("ReprintActivity", " Status: " + transaction.status);
|
||||||
|
|
||||||
|
// Open ReceiptActivity with enhanced transaction data
|
||||||
Intent intent = new Intent(this, ReceiptActivity.class);
|
Intent intent = new Intent(this, ReceiptActivity.class);
|
||||||
|
|
||||||
// Add calling activity information for proper back navigation
|
// Add calling activity information for proper back navigation
|
||||||
intent.putExtra("calling_activity", "TransactionActivity");
|
intent.putExtra("calling_activity", "ReprintActivity");
|
||||||
|
|
||||||
// Extract and send raw amount properly
|
// ✅ ENHANCED: Extract and send raw amount properly
|
||||||
String rawAmount = extractRawAmount(transaction.amount);
|
String rawAmount = extractRawAmount(transaction.amount);
|
||||||
|
Log.d("ReprintActivity", "💰 Amount processing: '" + transaction.amount + "' -> '" + rawAmount + "'");
|
||||||
|
|
||||||
Log.d("TransactionActivity", "Opening receipt for transaction: " + transaction.referenceId +
|
// ✅ ENHANCED: Get payment method using new improved logic
|
||||||
", channel: " + transaction.channelCode + ", original amount: '" + transaction.amount + "'");
|
String displayPaymentMethod = getPaymentMethodName(transaction.channelCode, transaction.channelCategory);
|
||||||
|
Log.d("ReprintActivity", "💳 Payment method: " + displayPaymentMethod);
|
||||||
|
|
||||||
// Send transaction data to ReceiptActivity
|
// ✅ ENHANCED: Determine card type for receipt using improved logic
|
||||||
|
String cardTypeForReceipt = determineCardTypeForReceipt(transaction.channelCode, transaction.channelCategory, transaction.referenceId);
|
||||||
|
Log.d("ReprintActivity", "🏷️ Card type for receipt: " + cardTypeForReceipt);
|
||||||
|
|
||||||
|
// ✅ ENHANCED: Get acquirer using improved logic
|
||||||
|
String acquirer = getRealAcquirerForQris(transaction.referenceId, transaction.channelCode);
|
||||||
|
Log.d("ReprintActivity", "🎯 Determined acquirer: " + acquirer);
|
||||||
|
|
||||||
|
// Send comprehensive transaction data to ReceiptActivity
|
||||||
intent.putExtra("transaction_id", transaction.referenceId);
|
intent.putExtra("transaction_id", transaction.referenceId);
|
||||||
intent.putExtra("reference_id", transaction.referenceId); // Nomor Transaksi
|
intent.putExtra("reference_id", transaction.referenceId); // Nomor Transaksi
|
||||||
intent.putExtra("transaction_amount", rawAmount); // Total transaksi
|
intent.putExtra("transaction_amount", rawAmount); // Total transaksi
|
||||||
intent.putExtra("gross_amount", rawAmount); // Consistent with transaction_amount
|
intent.putExtra("gross_amount", rawAmount); // Consistent with transaction_amount
|
||||||
intent.putExtra("created_at", transaction.createdAt); // Tanggal transaksi (will be formatted)
|
intent.putExtra("created_at", transaction.createdAt); // Tanggal transaksi (will be formatted)
|
||||||
intent.putExtra("transaction_date", formatDate(transaction.createdAt)); // Backup formatted date
|
intent.putExtra("transaction_date", formatDate(transaction.createdAt)); // Backup formatted date
|
||||||
intent.putExtra("payment_method", getPaymentMethodName(transaction.channelCode, transaction.channelCategory));
|
|
||||||
intent.putExtra("channel_code", transaction.channelCode); // Metode Pembayaran
|
// ✅ ENHANCED: Send improved payment method and card type
|
||||||
intent.putExtra("channel_category", transaction.channelCategory);
|
intent.putExtra("payment_method", displayPaymentMethod); // User-friendly payment method
|
||||||
intent.putExtra("card_type", transaction.channelCategory);
|
intent.putExtra("channel_code", transaction.channelCode); // Original channel code for processing
|
||||||
intent.putExtra("merchant_name", transaction.merchantName);
|
intent.putExtra("channel_category", transaction.channelCategory); // Original category for processing
|
||||||
|
intent.putExtra("card_type", cardTypeForReceipt); // Enhanced card type for display
|
||||||
|
|
||||||
|
// Merchant information
|
||||||
|
intent.putExtra("merchant_name", transaction.merchantName != null ? transaction.merchantName : "Marcel Panjaitan");
|
||||||
intent.putExtra("merchant_location", "Jakarta, Indonesia");
|
intent.putExtra("merchant_location", "Jakarta, Indonesia");
|
||||||
|
|
||||||
|
// ✅ ENHANCED: Send acquirer for proper card type detection in ReceiptActivity
|
||||||
|
intent.putExtra("acquirer", acquirer);
|
||||||
|
|
||||||
// Add MID and TID (default values since not available in Transaction model)
|
// Add MID and TID (default values since not available in Transaction model)
|
||||||
intent.putExtra("mid", "71000026521"); // MID
|
intent.putExtra("mid", "71000026521"); // MID
|
||||||
intent.putExtra("tid", "73001500"); // TID
|
intent.putExtra("tid", "73001500"); // TID
|
||||||
|
|
||||||
// ✅ ENHANCED: Use improved acquirer determination
|
// ✅ NEW: Add transaction status for receipt processing
|
||||||
String acquirer = getRealAcquirerForQris(transaction.referenceId, transaction.channelCode);
|
intent.putExtra("transaction_status", transaction.status);
|
||||||
intent.putExtra("acquirer", acquirer); // Jenis Kartu
|
|
||||||
|
|
||||||
Log.d("TransactionActivity", "🎯 Determined acquirer: " + acquirer + " for channel: " + transaction.channelCode);
|
// ✅ NEW: Add transaction type information
|
||||||
|
intent.putExtra("transaction_type", transaction.type);
|
||||||
|
|
||||||
|
Log.d("ReprintActivity", "🚀 LAUNCHING RECEIPT ACTIVITY");
|
||||||
|
Log.d("ReprintActivity", "📤 Sent data summary:");
|
||||||
|
Log.d("ReprintActivity", " Payment Method: " + displayPaymentMethod);
|
||||||
|
Log.d("ReprintActivity", " Card Type: " + cardTypeForReceipt);
|
||||||
|
Log.d("ReprintActivity", " Acquirer: " + acquirer);
|
||||||
|
Log.d("ReprintActivity", " Amount: " + rawAmount);
|
||||||
|
Log.d("ReprintActivity", "========================================");
|
||||||
|
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private String determineCardTypeForReceipt(String channelCode, String channelCategory, String referenceId) {
|
||||||
* ✅ ENHANCED: Dynamic acquirer determination instead of defaulting to GoPay
|
Log.d("ReprintActivity", "🔍 Determining card type for receipt:");
|
||||||
*/
|
Log.d("ReprintActivity", " channelCode: " + channelCode);
|
||||||
private String determineAcquirerFromChannelCode(String channelCode) {
|
Log.d("ReprintActivity", " channelCategory: " + channelCategory);
|
||||||
if (channelCode == null) return "unknown"; // ✅ CHANGED: unknown instead of gopay
|
Log.d("ReprintActivity", " referenceId: " + referenceId);
|
||||||
|
|
||||||
switch (channelCode.toLowerCase()) {
|
// Priority 1: Direct channel code mapping
|
||||||
case "qris":
|
if (channelCode != null && !channelCode.isEmpty()) {
|
||||||
// ✅ IMPROVED: For QRIS, try to get real acquirer or return generic
|
String code = channelCode.toUpperCase().trim();
|
||||||
return "qris"; // Will be processed by receipt activity to find real acquirer
|
|
||||||
case "bca":
|
switch (code) {
|
||||||
return "bca";
|
case "QRIS":
|
||||||
case "mandiri":
|
case "RETAIL_OUTLET":
|
||||||
return "mandiri";
|
// For QRIS, return the channel code so ReceiptActivity can detect real acquirer
|
||||||
case "bni":
|
Log.d("ReprintActivity", "✅ QRIS detected, returning for real acquirer detection");
|
||||||
return "bni";
|
return "QRIS";
|
||||||
case "bri":
|
case "DEBIT":
|
||||||
return "bri";
|
case "DEBIT_CARD":
|
||||||
case "permata":
|
return "Debit";
|
||||||
return "permata";
|
case "CREDIT":
|
||||||
case "cimb":
|
case "CREDIT_CARD":
|
||||||
return "cimb";
|
return "Credit";
|
||||||
case "danamon":
|
case "BCA":
|
||||||
return "danamon";
|
case "MANDIRI":
|
||||||
case "bsi":
|
case "BNI":
|
||||||
return "bsi";
|
case "BRI":
|
||||||
case "debit":
|
case "PERMATA":
|
||||||
return "visa"; // Default for debit cards
|
case "CIMB":
|
||||||
case "credit":
|
case "DANAMON":
|
||||||
return "mastercard"; // Default for credit cards
|
case "BSI":
|
||||||
|
return code;
|
||||||
default:
|
default:
|
||||||
return "unknown"; // ✅ CHANGED: unknown instead of gopay for unknown channels
|
Log.d("ReprintActivity", "🔍 Unknown channelCode, checking channelCategory");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Channel category mapping
|
||||||
|
if (channelCategory != null && !channelCategory.isEmpty()) {
|
||||||
|
String category = channelCategory.toUpperCase().trim();
|
||||||
|
|
||||||
|
switch (category) {
|
||||||
|
case "RETAIL_OUTLET":
|
||||||
|
return "QRIS";
|
||||||
|
case "DEBIT":
|
||||||
|
case "DEBIT_CARD":
|
||||||
|
return "Debit";
|
||||||
|
case "CREDIT":
|
||||||
|
case "CREDIT_CARD":
|
||||||
|
return "Credit";
|
||||||
|
case "E_MONEY":
|
||||||
|
case "EMONEY":
|
||||||
|
return "E-Money";
|
||||||
|
default:
|
||||||
|
return capitalizeFirstLetter(channelCategory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback
|
||||||
|
Log.w("ReprintActivity", "⚠️ No valid card type found, defaulting to Unknown");
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String determineAcquirerFromChannelCode(String channelCode) {
|
||||||
|
if (channelCode == null) return "unknown";
|
||||||
|
|
||||||
|
String code = channelCode.toLowerCase().trim();
|
||||||
|
Log.d("ReprintActivity", "🔍 Determining acquirer from channelCode: " + channelCode);
|
||||||
|
|
||||||
|
switch (code) {
|
||||||
|
case "qris":
|
||||||
|
case "retail_outlet":
|
||||||
|
// ✅ For QRIS/RETAIL_OUTLET, return qris to trigger real acquirer detection
|
||||||
|
Log.d("ReprintActivity", "🔍 QRIS/RETAIL_OUTLET detected, will search for real acquirer");
|
||||||
|
return "qris";
|
||||||
|
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 "debit":
|
||||||
|
case "debit_card":
|
||||||
|
return "Visa"; // Default for debit cards
|
||||||
|
case "credit":
|
||||||
|
case "credit_card":
|
||||||
|
return "Mastercard"; // Default for credit cards
|
||||||
|
default:
|
||||||
|
Log.d("ReprintActivity", "🔍 Unknown channelCode: " + channelCode + ", returning as-is");
|
||||||
|
return capitalizeFirstLetter(channelCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ✅ NEW METHOD: Try to get real acquirer for QRIS transactions from current transaction data
|
|
||||||
* This method can be enhanced to query webhook API for real acquirer
|
|
||||||
*/
|
|
||||||
private String getRealAcquirerForQris(String referenceId, String channelCode) {
|
private String getRealAcquirerForQris(String referenceId, String channelCode) {
|
||||||
// If not QRIS, return channel code
|
Log.d("ReprintActivity", "🔍 Getting real acquirer for referenceId: " + referenceId + ", channelCode: " + channelCode);
|
||||||
if (!"QRIS".equalsIgnoreCase(channelCode)) {
|
|
||||||
return determineAcquirerFromChannelCode(channelCode);
|
// If not QRIS-related, use standard mapping
|
||||||
|
if (!"QRIS".equalsIgnoreCase(channelCode) && !"RETAIL_OUTLET".equalsIgnoreCase(channelCode)) {
|
||||||
|
String acquirer = determineAcquirerFromChannelCode(channelCode);
|
||||||
|
Log.d("ReprintActivity", "🔍 Non-QRIS transaction, mapped to: " + acquirer);
|
||||||
|
return acquirer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For QRIS, we could implement real-time acquirer lookup here
|
// For QRIS transactions, try to get real acquirer
|
||||||
|
Log.d("ReprintActivity", "🔍 QRIS transaction detected, attempting real acquirer lookup");
|
||||||
|
|
||||||
|
// Try to get from cache first (if implemented)
|
||||||
// For now, return "qris" and let ReceiptActivity handle the detection
|
// For now, return "qris" and let ReceiptActivity handle the detection
|
||||||
Log.d("TransactionActivity", "🔍 QRIS transaction detected, deferring acquirer detection to ReceiptActivity");
|
|
||||||
return "qris";
|
return "qris";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1128,30 +1234,147 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
Long.parseLong(cleaned);
|
Long.parseLong(cleaned);
|
||||||
return cleaned;
|
return cleaned;
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
Log.e("TransactionActivity", "Invalid amount: " + formattedAmount);
|
Log.e("ReprintActivity", "Invalid amount: " + formattedAmount);
|
||||||
return "0";
|
return "0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPaymentMethodName(String channelCode, String channelCategory) {
|
private String getPaymentMethodName(String channelCode, String channelCategory) {
|
||||||
if (channelCode == null) return "Unknown";
|
Log.d("ReprintActivity", "🔍 Enhanced payment method mapping:");
|
||||||
|
Log.d("ReprintActivity", " channelCode: " + channelCode);
|
||||||
|
Log.d("ReprintActivity", " channelCategory: " + channelCategory);
|
||||||
|
|
||||||
switch (channelCode.toUpperCase()) {
|
// Priority 1: Use channelCode for specific mapping
|
||||||
case "QRIS": return "QRIS";
|
if (channelCode != null && !channelCode.isEmpty()) {
|
||||||
case "DEBIT": return "Kartu Debit";
|
String code = channelCode.toUpperCase().trim();
|
||||||
case "CREDIT": return "Kartu Kredit";
|
|
||||||
|
switch (code) {
|
||||||
|
case "QRIS":
|
||||||
|
Log.d("ReprintActivity", "✅ Mapped to: QRIS");
|
||||||
|
return "QRIS";
|
||||||
|
case "DEBIT":
|
||||||
|
case "DEBIT_CARD":
|
||||||
|
Log.d("ReprintActivity", "✅ Mapped to: Kartu Debit");
|
||||||
|
return "Kartu Debit";
|
||||||
|
case "CREDIT":
|
||||||
|
case "CREDIT_CARD":
|
||||||
|
Log.d("ReprintActivity", "✅ Mapped to: Kartu Kredit");
|
||||||
|
return "Kartu Kredit";
|
||||||
case "BCA":
|
case "BCA":
|
||||||
|
return "BCA";
|
||||||
case "MANDIRI":
|
case "MANDIRI":
|
||||||
|
return "Mandiri";
|
||||||
case "BNI":
|
case "BNI":
|
||||||
case "BRI": return "Kartu " + channelCode.toUpperCase();
|
return "BNI";
|
||||||
case "CASH": return "Tunai";
|
case "BRI":
|
||||||
case "EDC": return "EDC";
|
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":
|
||||||
|
// ✅ ENHANCED: For RETAIL_OUTLET, use intelligent detection
|
||||||
|
String detectedMethod = detectPaymentMethodFromRetailOutlet(channelCategory);
|
||||||
|
Log.d("ReprintActivity", "🔍 RETAIL_OUTLET detected as: " + detectedMethod);
|
||||||
|
return detectedMethod;
|
||||||
default:
|
default:
|
||||||
|
Log.d("ReprintActivity", "🔍 Unknown channelCode: " + code + ", checking channelCategory");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Use channelCategory as fallback
|
||||||
if (channelCategory != null && !channelCategory.isEmpty()) {
|
if (channelCategory != null && !channelCategory.isEmpty()) {
|
||||||
return channelCategory.toUpperCase();
|
String mappedFromCategory = mapChannelCategoryToPaymentMethod(channelCategory);
|
||||||
|
Log.d("ReprintActivity", "✅ Mapped from channelCategory: " + mappedFromCategory);
|
||||||
|
return mappedFromCategory;
|
||||||
}
|
}
|
||||||
return channelCode.toUpperCase();
|
|
||||||
|
// Final fallback
|
||||||
|
Log.w("ReprintActivity", "⚠️ No valid payment method found, defaulting to Unknown");
|
||||||
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String detectPaymentMethodFromRetailOutlet(String channelCategory) {
|
||||||
|
if (channelCategory == null || channelCategory.isEmpty()) {
|
||||||
|
return "QRIS"; // Most RETAIL_OUTLET are QRIS
|
||||||
|
}
|
||||||
|
|
||||||
|
String category = channelCategory.toUpperCase().trim();
|
||||||
|
|
||||||
|
switch (category) {
|
||||||
|
case "RETAIL_OUTLET":
|
||||||
|
return "QRIS"; // RETAIL_OUTLET + RETAIL_OUTLET = 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";
|
||||||
|
default:
|
||||||
|
return "QRIS"; // Default assumption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
return capitalizeFirstLetter(channelCategory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String capitalizeFirstLetter(String text) {
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleaned = text.trim().replace("_", " ");
|
||||||
|
String[] words = cleaned.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String formatDate(String rawDate) {
|
private String formatDate(String rawDate) {
|
||||||
@@ -1165,15 +1388,6 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
if (item.getItemId() == android.R.id.home) {
|
|
||||||
finish();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transaction model class
|
// Transaction model class
|
||||||
static class Transaction {
|
static class Transaction {
|
||||||
int id;
|
int id;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.bdkipoc;
|
package com.example.bdkipoc.cetakulang;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.R;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -26,15 +27,17 @@ import org.json.JSONArray;
|
|||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.TransactionViewHolder> {
|
import com.example.bdkipoc.StyleHelper;
|
||||||
private List<TransactionActivity.Transaction> transactionList;
|
|
||||||
|
public class ReprintAdapterActivity extends RecyclerView.Adapter<ReprintAdapterActivity.TransactionViewHolder> {
|
||||||
|
private List<ReprintActivity.Transaction> transactionList;
|
||||||
private OnPrintClickListener printClickListener;
|
private OnPrintClickListener printClickListener;
|
||||||
|
|
||||||
public interface OnPrintClickListener {
|
public interface OnPrintClickListener {
|
||||||
void onPrintClick(TransactionActivity.Transaction transaction);
|
void onPrintClick(ReprintActivity.Transaction transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TransactionAdapter(List<TransactionActivity.Transaction> transactionList) {
|
public ReprintAdapterActivity(List<ReprintActivity.Transaction> transactionList) {
|
||||||
this.transactionList = transactionList;
|
this.transactionList = transactionList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,23 +48,23 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
/**
|
/**
|
||||||
* Update data without numbering (removed as per request)
|
* Update data without numbering (removed as per request)
|
||||||
*/
|
*/
|
||||||
public void updateData(List<TransactionActivity.Transaction> newData, int startIndex) {
|
public void updateData(List<ReprintActivity.Transaction> newData, int startIndex) {
|
||||||
this.transactionList = newData;
|
this.transactionList = newData;
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "📋 Data updated: " + newData.size() + " items");
|
Log.d("ReprintAdapterActivity", "📋 Data updated: " + newData.size() + " items");
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false);
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_reprint, parent, false);
|
||||||
return new TransactionViewHolder(view);
|
return new TransactionViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
|
||||||
TransactionActivity.Transaction t = transactionList.get(position);
|
ReprintActivity.Transaction t = transactionList.get(position);
|
||||||
|
|
||||||
// ✅ STRIPE TABLE: Set alternating row colors
|
// ✅ STRIPE TABLE: Set alternating row colors
|
||||||
LinearLayout itemContainer = holder.itemView.findViewById(R.id.itemContainer);
|
LinearLayout itemContainer = holder.itemView.findViewById(R.id.itemContainer);
|
||||||
@@ -73,10 +76,10 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
itemContainer.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.background_light));
|
itemContainer.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.background_light));
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "📋 Binding transaction " + position + ":");
|
Log.d("ReprintAdapterActivity", "📋 Binding transaction " + position + ":");
|
||||||
Log.d("TransactionAdapter", " Reference: " + t.referenceId);
|
Log.d("ReprintAdapterActivity", " Reference: " + t.referenceId);
|
||||||
Log.d("TransactionAdapter", " Status: " + t.status);
|
Log.d("ReprintAdapterActivity", " Status: " + t.status);
|
||||||
Log.d("TransactionAdapter", " Amount: " + t.amount);
|
Log.d("ReprintAdapterActivity", " Amount: " + t.amount);
|
||||||
|
|
||||||
// Set reference ID
|
// Set reference ID
|
||||||
holder.referenceId.setText(t.referenceId);
|
holder.referenceId.setText(t.referenceId);
|
||||||
@@ -88,10 +91,10 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
String formattedAmount = formatRupiah(amountValue);
|
String formattedAmount = formatRupiah(amountValue);
|
||||||
holder.amount.setText(formattedAmount);
|
holder.amount.setText(formattedAmount);
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "💰 Amount processed: '" + t.amount + "' -> '" + formattedAmount + "'");
|
Log.d("ReprintAdapterActivity", "💰 Amount processed: '" + t.amount + "' -> '" + formattedAmount + "'");
|
||||||
|
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
Log.e("TransactionAdapter", "❌ Amount format error: " + t.amount, e);
|
Log.e("ReprintAdapterActivity", "❌ Amount format error: " + t.amount, e);
|
||||||
String fallback = t.amount.startsWith("Rp") ? t.amount : "Rp " + t.amount;
|
String fallback = t.amount.startsWith("Rp") ? t.amount : "Rp " + t.amount;
|
||||||
holder.amount.setText(fallback);
|
holder.amount.setText(fallback);
|
||||||
}
|
}
|
||||||
@@ -99,7 +102,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
// ✅ ENHANCED STATUS HANDLING dengan comprehensive checking
|
// ✅ ENHANCED STATUS HANDLING dengan comprehensive checking
|
||||||
String displayStatus = t.status;
|
String displayStatus = t.status;
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "🔍 Checking status for: " + t.referenceId + " (current: " + displayStatus + ")");
|
Log.d("ReprintAdapterActivity", "🔍 Checking status for: " + t.referenceId + " (current: " + displayStatus + ")");
|
||||||
|
|
||||||
// Jika status adalah INIT atau PENDING, lakukan comprehensive check
|
// Jika status adalah INIT atau PENDING, lakukan comprehensive check
|
||||||
if ("INIT".equalsIgnoreCase(t.status) || "PENDING".equalsIgnoreCase(t.status)) {
|
if ("INIT".equalsIgnoreCase(t.status) || "PENDING".equalsIgnoreCase(t.status)) {
|
||||||
@@ -108,7 +111,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
holder.status.setText("CHECKING...");
|
holder.status.setText("CHECKING...");
|
||||||
StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), "CHECKING");
|
StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), "CHECKING");
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "🔄 Starting comprehensive check for: " + t.referenceId);
|
Log.d("ReprintAdapterActivity", "🔄 Starting comprehensive check for: " + t.referenceId);
|
||||||
|
|
||||||
// Check real status dari semua kemungkinan sources
|
// Check real status dari semua kemungkinan sources
|
||||||
checkMidtransStatus(t.referenceId, holder.status);
|
checkMidtransStatus(t.referenceId, holder.status);
|
||||||
@@ -116,13 +119,13 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
// No reference ID to check
|
// No reference ID to check
|
||||||
holder.status.setText(displayStatus.toUpperCase());
|
holder.status.setText(displayStatus.toUpperCase());
|
||||||
StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), displayStatus);
|
StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), displayStatus);
|
||||||
Log.w("TransactionAdapter", "⚠️ No reference ID for status check");
|
Log.w("ReprintAdapterActivity", "⚠️ No reference ID for status check");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use existing status yang sudah confirmed
|
// Use existing status yang sudah confirmed
|
||||||
holder.status.setText(displayStatus.toUpperCase());
|
holder.status.setText(displayStatus.toUpperCase());
|
||||||
StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), displayStatus);
|
StyleHelper.applyStatusTextColor(holder.status, holder.itemView.getContext(), displayStatus);
|
||||||
Log.d("TransactionAdapter", "✅ Using confirmed status: " + displayStatus);
|
Log.d("ReprintAdapterActivity", "✅ Using confirmed status: " + displayStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set payment method
|
// Set payment method
|
||||||
@@ -133,7 +136,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
String formattedDate = formatCreatedAtDate(t.createdAt);
|
String formattedDate = formatCreatedAtDate(t.createdAt);
|
||||||
holder.createdAt.setText(formattedDate);
|
holder.createdAt.setText(formattedDate);
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "📅 Created at: " + t.createdAt + " -> " + formattedDate);
|
Log.d("ReprintAdapterActivity", "📅 Created at: " + t.createdAt + " -> " + formattedDate);
|
||||||
|
|
||||||
// Set click listeners
|
// Set click listeners
|
||||||
holder.itemView.setOnClickListener(v -> {
|
holder.itemView.setOnClickListener(v -> {
|
||||||
@@ -148,7 +151,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "✅ Transaction binding complete for: " + t.referenceId);
|
Log.d("ReprintAdapterActivity", "✅ Transaction binding complete for: " + t.referenceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String cleanAmountString(String amount) {
|
private String cleanAmountString(String amount) {
|
||||||
@@ -156,7 +159,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
return "0";
|
return "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "Cleaning amount: '" + amount + "'");
|
Log.d("ReprintAdapterActivity", "Cleaning amount: '" + amount + "'");
|
||||||
|
|
||||||
// Remove currency symbols and spaces
|
// Remove currency symbols and spaces
|
||||||
String cleaned = amount
|
String cleaned = amount
|
||||||
@@ -199,7 +202,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
// Remove any commas
|
// Remove any commas
|
||||||
cleaned = cleaned.replace(",", "");
|
cleaned = cleaned.replace(",", "");
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "Cleaned result: '" + cleaned + "'");
|
Log.d("ReprintAdapterActivity", "Cleaned result: '" + cleaned + "'");
|
||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +219,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
private void checkMidtransStatus(String referenceId, TextView statusTextView) {
|
private void checkMidtransStatus(String referenceId, TextView statusTextView) {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Log.d("TransactionAdapter", "🔍 Comprehensive status check for reference: " + referenceId);
|
Log.d("ReprintAdapterActivity", "🔍 Comprehensive status check for reference: " + referenceId);
|
||||||
|
|
||||||
// STEP 1: Query webhook logs untuk semua order_id yang terkait
|
// 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";
|
String queryUrl = "https://be-edc.msvc.app/api-logs?limit=200&sortOrder=DESC&sortColumn=created_at";
|
||||||
@@ -244,7 +247,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
String foundAcquirer = null;
|
String foundAcquirer = null;
|
||||||
|
|
||||||
if (results != null && results.length() > 0) {
|
if (results != null && results.length() > 0) {
|
||||||
Log.d("TransactionAdapter", "📊 Processing " + results.length() + " log entries");
|
Log.d("ReprintAdapterActivity", "📊 Processing " + results.length() + " log entries");
|
||||||
|
|
||||||
// STEP 2: Comprehensive search dengan multiple matching strategies
|
// STEP 2: Comprehensive search dengan multiple matching strategies
|
||||||
for (int i = 0; i < results.length(); i++) {
|
for (int i = 0; i < results.length(); i++) {
|
||||||
@@ -270,7 +273,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
String appReferenceId = customData.optString("app_reference_id", "");
|
String appReferenceId = customData.optString("app_reference_id", "");
|
||||||
if (referenceId.equals(originalReference) || referenceId.equals(appReferenceId)) {
|
if (referenceId.equals(originalReference) || referenceId.equals(appReferenceId)) {
|
||||||
isRefreshMatch = true;
|
isRefreshMatch = true;
|
||||||
Log.d("TransactionAdapter", "🔄 Found refresh match: " + logOrderId);
|
Log.d("ReprintAdapterActivity", "🔄 Found refresh match: " + logOrderId);
|
||||||
}
|
}
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
// Ignore custom field parsing errors
|
// Ignore custom field parsing errors
|
||||||
@@ -288,7 +291,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
if (itemName.contains("(Ref: " + referenceId + ")") ||
|
if (itemName.contains("(Ref: " + referenceId + ")") ||
|
||||||
itemName.contains("- " + referenceId)) {
|
itemName.contains("- " + referenceId)) {
|
||||||
isItemMatch = true;
|
isItemMatch = true;
|
||||||
Log.d("TransactionAdapter", "📦 Found item match: " + logOrderId);
|
Log.d("ReprintAdapterActivity", "📦 Found item match: " + logOrderId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,11 +302,11 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
boolean isRelatedTransaction = isDirectMatch || isRefreshMatch || isItemMatch;
|
boolean isRelatedTransaction = isDirectMatch || isRefreshMatch || isItemMatch;
|
||||||
|
|
||||||
if (isRelatedTransaction) {
|
if (isRelatedTransaction) {
|
||||||
Log.d("TransactionAdapter", "🎯 MATCH FOUND!");
|
Log.d("ReprintAdapterActivity", "🎯 MATCH FOUND!");
|
||||||
Log.d("TransactionAdapter", " Order ID: " + logOrderId);
|
Log.d("ReprintAdapterActivity", " Order ID: " + logOrderId);
|
||||||
Log.d("TransactionAdapter", " Status: " + logTransactionStatus);
|
Log.d("ReprintAdapterActivity", " Status: " + logTransactionStatus);
|
||||||
Log.d("TransactionAdapter", " Acquirer: " + logAcquirer);
|
Log.d("ReprintAdapterActivity", " Acquirer: " + logAcquirer);
|
||||||
Log.d("TransactionAdapter", " Match Type: " +
|
Log.d("ReprintAdapterActivity", " Match Type: " +
|
||||||
(isDirectMatch ? "DIRECT " : "") +
|
(isDirectMatch ? "DIRECT " : "") +
|
||||||
(isRefreshMatch ? "REFRESH " : "") +
|
(isRefreshMatch ? "REFRESH " : "") +
|
||||||
(isItemMatch ? "ITEM" : ""));
|
(isItemMatch ? "ITEM" : ""));
|
||||||
@@ -315,29 +318,29 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
finalStatus = "PAID";
|
finalStatus = "PAID";
|
||||||
foundOrderId = logOrderId;
|
foundOrderId = logOrderId;
|
||||||
foundAcquirer = logAcquirer;
|
foundAcquirer = logAcquirer;
|
||||||
Log.d("TransactionAdapter", "✅ PAYMENT CONFIRMED: " + logOrderId + " -> " + logTransactionStatus);
|
Log.d("ReprintAdapterActivity", "✅ PAYMENT CONFIRMED: " + logOrderId + " -> " + logTransactionStatus);
|
||||||
break; // Found paid status, stop searching
|
break; // Found paid status, stop searching
|
||||||
} else if (logTransactionStatus.equals("pending") && finalStatus.equals("INIT")) {
|
} else if (logTransactionStatus.equals("pending") && finalStatus.equals("INIT")) {
|
||||||
finalStatus = "PENDING";
|
finalStatus = "PENDING";
|
||||||
foundOrderId = logOrderId;
|
foundOrderId = logOrderId;
|
||||||
foundAcquirer = logAcquirer;
|
foundAcquirer = logAcquirer;
|
||||||
Log.d("TransactionAdapter", "⏳ PENDING found: " + logOrderId);
|
Log.d("ReprintAdapterActivity", "⏳ PENDING found: " + logOrderId);
|
||||||
} else if (logTransactionStatus.equals("expire") || logTransactionStatus.equals("cancel")) {
|
} else if (logTransactionStatus.equals("expire") || logTransactionStatus.equals("cancel")) {
|
||||||
if (finalStatus.equals("INIT")) { // Only update if no better status found
|
if (finalStatus.equals("INIT")) { // Only update if no better status found
|
||||||
finalStatus = "FAILED";
|
finalStatus = "FAILED";
|
||||||
foundOrderId = logOrderId;
|
foundOrderId = logOrderId;
|
||||||
foundAcquirer = logAcquirer;
|
foundAcquirer = logAcquirer;
|
||||||
Log.d("TransactionAdapter", "❌ FAILED status: " + logOrderId + " -> " + logTransactionStatus);
|
Log.d("ReprintAdapterActivity", "❌ FAILED status: " + logOrderId + " -> " + logTransactionStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "🔍 FINAL RESULT for " + referenceId + ":");
|
Log.d("ReprintAdapterActivity", "🔍 FINAL RESULT for " + referenceId + ":");
|
||||||
Log.d("TransactionAdapter", " Status: " + finalStatus);
|
Log.d("ReprintAdapterActivity", " Status: " + finalStatus);
|
||||||
Log.d("TransactionAdapter", " Order ID: " + (foundOrderId != null ? foundOrderId : "N/A"));
|
Log.d("ReprintAdapterActivity", " Order ID: " + (foundOrderId != null ? foundOrderId : "N/A"));
|
||||||
Log.d("TransactionAdapter", " Acquirer: " + (foundAcquirer != null ? foundAcquirer : "N/A"));
|
Log.d("ReprintAdapterActivity", " Acquirer: " + (foundAcquirer != null ? foundAcquirer : "N/A"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 3: Update UI di main thread
|
// STEP 3: Update UI di main thread
|
||||||
@@ -348,10 +351,10 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
statusTextView.setText(displayStatus);
|
statusTextView.setText(displayStatus);
|
||||||
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), displayStatus);
|
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), displayStatus);
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "🎨 UI UPDATED:");
|
Log.d("ReprintAdapterActivity", "🎨 UI UPDATED:");
|
||||||
Log.d("TransactionAdapter", " Reference: " + referenceId);
|
Log.d("ReprintAdapterActivity", " Reference: " + referenceId);
|
||||||
Log.d("TransactionAdapter", " Display Status: " + displayStatus);
|
Log.d("ReprintAdapterActivity", " Display Status: " + displayStatus);
|
||||||
Log.d("TransactionAdapter", " Detected Acquirer: " + (detectedAcquirer != null ? detectedAcquirer : "Unknown"));
|
Log.d("ReprintAdapterActivity", " Detected Acquirer: " + (detectedAcquirer != null ? detectedAcquirer : "Unknown"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ BONUS: Update backend jika status berubah ke PAID
|
// ✅ BONUS: Update backend jika status berubah ke PAID
|
||||||
@@ -360,7 +363,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Log.w("TransactionAdapter", "⚠️ API call failed with code: " + conn.getResponseCode());
|
Log.w("ReprintAdapterActivity", "⚠️ API call failed with code: " + conn.getResponseCode());
|
||||||
statusTextView.post(() -> {
|
statusTextView.post(() -> {
|
||||||
statusTextView.setText("ERROR");
|
statusTextView.setText("ERROR");
|
||||||
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "ERROR");
|
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "ERROR");
|
||||||
@@ -368,7 +371,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (IOException | JSONException e) {
|
} catch (IOException | JSONException e) {
|
||||||
Log.e("TransactionAdapter", "❌ Comprehensive status check error: " + e.getMessage(), e);
|
Log.e("ReprintAdapterActivity", "❌ Comprehensive status check error: " + e.getMessage(), e);
|
||||||
statusTextView.post(() -> {
|
statusTextView.post(() -> {
|
||||||
statusTextView.setText("INIT");
|
statusTextView.setText("INIT");
|
||||||
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "INIT");
|
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "INIT");
|
||||||
@@ -383,7 +386,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
private void updateBackendTransactionStatus(String referenceId, String status, String orderId, String acquirer) {
|
private void updateBackendTransactionStatus(String referenceId, String status, String orderId, String acquirer) {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Log.d("TransactionAdapter", "🔄 Updating backend status for reference: " + referenceId);
|
Log.d("ReprintAdapterActivity", "🔄 Updating backend status for reference: " + referenceId);
|
||||||
|
|
||||||
JSONObject updatePayload = new JSONObject();
|
JSONObject updatePayload = new JSONObject();
|
||||||
updatePayload.put("status", status);
|
updatePayload.put("status", status);
|
||||||
@@ -414,16 +417,16 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
}
|
}
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
int responseCode = conn.getResponseCode();
|
||||||
Log.d("TransactionAdapter", "📥 Backend update response: " + responseCode);
|
Log.d("ReprintAdapterActivity", "📥 Backend update response: " + responseCode);
|
||||||
|
|
||||||
if (responseCode == 200 || responseCode == 201) {
|
if (responseCode == 200 || responseCode == 201) {
|
||||||
Log.d("TransactionAdapter", "✅ Backend status updated successfully");
|
Log.d("ReprintAdapterActivity", "✅ Backend status updated successfully");
|
||||||
} else {
|
} else {
|
||||||
Log.e("TransactionAdapter", "❌ Backend update failed: " + responseCode);
|
Log.e("ReprintAdapterActivity", "❌ Backend update failed: " + responseCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e("TransactionAdapter", "❌ Backend update error: " + e.getMessage(), e);
|
Log.e("ReprintAdapterActivity", "❌ Backend update error: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
@@ -436,7 +439,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
return "N/A";
|
return "N/A";
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "📅 Input date: '" + rawDate + "'");
|
Log.d("ReprintAdapterActivity", "📅 Input date: '" + rawDate + "'");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Handle different possible input formats from API
|
// Handle different possible input formats from API
|
||||||
@@ -460,7 +463,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "📅 Cleaned date: '" + cleanedDate + "'");
|
Log.d("ReprintAdapterActivity", "📅 Cleaned date: '" + cleanedDate + "'");
|
||||||
|
|
||||||
// Output format: d/M/yyyy H:mm:ss
|
// Output format: d/M/yyyy H:mm:ss
|
||||||
SimpleDateFormat outputFormat = new SimpleDateFormat("d/M/yyyy H:mm:ss", Locale.getDefault());
|
SimpleDateFormat outputFormat = new SimpleDateFormat("d/M/yyyy H:mm:ss", Locale.getDefault());
|
||||||
@@ -468,11 +471,11 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
Date date = inputFormat.parse(cleanedDate);
|
Date date = inputFormat.parse(cleanedDate);
|
||||||
if (date != null) {
|
if (date != null) {
|
||||||
String formatted = outputFormat.format(date);
|
String formatted = outputFormat.format(date);
|
||||||
Log.d("TransactionAdapter", "📅 Date formatted: " + rawDate + " -> " + formatted);
|
Log.d("ReprintAdapterActivity", "📅 Date formatted: " + rawDate + " -> " + formatted);
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e("TransactionAdapter", "❌ Date formatting error for: " + rawDate, e);
|
Log.e("ReprintAdapterActivity", "❌ Date formatting error for: " + rawDate, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Manual parsing
|
// Fallback: Manual parsing
|
||||||
@@ -485,7 +488,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
workingDate = workingDate.substring(0, workingDate.indexOf("."));
|
workingDate = workingDate.substring(0, workingDate.indexOf("."));
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("TransactionAdapter", "📅 Manual parsing attempt: '" + workingDate + "'");
|
Log.d("ReprintAdapterActivity", "📅 Manual parsing attempt: '" + workingDate + "'");
|
||||||
|
|
||||||
// Split into date and time parts
|
// Split into date and time parts
|
||||||
String[] parts = workingDate.split(" ");
|
String[] parts = workingDate.split(" ");
|
||||||
@@ -511,29 +514,34 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
String second = timeComponents[2];
|
String second = timeComponents[2];
|
||||||
|
|
||||||
String result = dayInt + "/" + monthInt + "/" + year + " " + hour + ":" + minute + ":" + second;
|
String result = dayInt + "/" + monthInt + "/" + year + " " + hour + ":" + minute + ":" + second;
|
||||||
Log.d("TransactionAdapter", "📅 Manual format result: " + result);
|
Log.d("ReprintAdapterActivity", "📅 Manual format result: " + result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w("TransactionAdapter", "❌ Manual date formatting failed: " + e.getMessage());
|
Log.w("ReprintAdapterActivity", "❌ Manual date formatting failed: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.w("TransactionAdapter", "📅 Using fallback - returning original date: " + rawDate);
|
Log.w("ReprintAdapterActivity", "📅 Using fallback - returning original date: " + rawDate);
|
||||||
return rawDate;
|
return rawDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPaymentMethodName(String channelCode, String channelCategory) {
|
private String getPaymentMethodName(String channelCode, String channelCategory) {
|
||||||
// Convert channel code to readable payment method name
|
Log.d("ReprintAdapterActivity", "🔍 Mapping payment method - channelCode: " + channelCode + ", channelCategory: " + channelCategory);
|
||||||
if (channelCode == null) return "Unknown";
|
|
||||||
|
|
||||||
switch (channelCode.toUpperCase()) {
|
// Priority 1: Use channelCode for specific mapping
|
||||||
|
if (channelCode != null && !channelCode.isEmpty()) {
|
||||||
|
String code = channelCode.toUpperCase().trim();
|
||||||
|
|
||||||
|
switch (code) {
|
||||||
case "QRIS":
|
case "QRIS":
|
||||||
return "QRIS";
|
return "QRIS";
|
||||||
case "DEBIT":
|
case "DEBIT":
|
||||||
|
case "DEBIT_CARD":
|
||||||
return "Kartu Debit";
|
return "Kartu Debit";
|
||||||
case "CREDIT":
|
case "CREDIT":
|
||||||
|
case "CREDIT_CARD":
|
||||||
return "Kartu Kredit";
|
return "Kartu Kredit";
|
||||||
case "BCA":
|
case "BCA":
|
||||||
return "BCA";
|
return "BCA";
|
||||||
@@ -543,17 +551,103 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
|
|||||||
return "BNI";
|
return "BNI";
|
||||||
case "BRI":
|
case "BRI":
|
||||||
return "BRI";
|
return "BRI";
|
||||||
|
case "PERMATA":
|
||||||
|
return "Permata";
|
||||||
|
case "CIMB":
|
||||||
|
return "CIMB Niaga";
|
||||||
|
case "DANAMON":
|
||||||
|
return "Danamon";
|
||||||
|
case "BSI":
|
||||||
|
return "BSI";
|
||||||
case "CASH":
|
case "CASH":
|
||||||
return "Tunai";
|
return "Tunai";
|
||||||
case "EDC":
|
case "EDC":
|
||||||
return "EDC";
|
return "EDC";
|
||||||
|
case "RETAIL_OUTLET":
|
||||||
|
// ✅ SPECIAL HANDLING: For RETAIL_OUTLET, determine by context
|
||||||
|
return determinePaymentMethodFromCategory(channelCategory);
|
||||||
default:
|
default:
|
||||||
// If channel category is available, use it as fallback
|
Log.d("ReprintAdapterActivity", "🔍 Unknown channelCode: " + code + ", trying channelCategory");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Use channelCategory as fallback
|
||||||
if (channelCategory != null && !channelCategory.isEmpty()) {
|
if (channelCategory != null && !channelCategory.isEmpty()) {
|
||||||
return channelCategory.toUpperCase();
|
return mapChannelCategoryToPaymentMethod(channelCategory);
|
||||||
}
|
}
|
||||||
return channelCode.toUpperCase();
|
|
||||||
|
// 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
|
@Override
|
||||||
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);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
package com.example.bdkipoc;
|
package com.example.bdkipoc.histori;
|
||||||
|
import com.example.bdkipoc.BuildConfig;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
@@ -8,8 +9,10 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import android.graphics.Color;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
@@ -29,6 +32,9 @@ import java.util.ArrayList;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.R;
|
||||||
|
|
||||||
public class HistoryActivity extends AppCompatActivity {
|
public class HistoryActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@@ -39,12 +45,13 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private HistoryAdapter adapter;
|
private HistoryAdapter adapter;
|
||||||
private List<HistoryItem> historyList;
|
private List<HistoryItem> historyList;
|
||||||
private ImageView btnBack;
|
private LinearLayout backNavigation;
|
||||||
|
|
||||||
// Store full data for detail view
|
// Store full data for detail view
|
||||||
private static List<HistoryItem> fullHistoryData = new ArrayList<>();
|
private static List<HistoryItem> fullHistoryData = new ArrayList<>();
|
||||||
|
|
||||||
private String API_URL = "https://be-edc.msvc.app/transactions?page=0&limit=50&sortOrder=DESC&from_date=2025-05-10&to_date=2025-05-21&location_id=0&merchant_id=0&sortColumn=id";
|
private String API_URL;
|
||||||
|
private String SUMMARY_API_URL;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -53,17 +60,31 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
initViews();
|
initViews();
|
||||||
setupRecyclerView();
|
setupRecyclerView();
|
||||||
|
buildApiUrl();
|
||||||
fetchApiData();
|
fetchApiData();
|
||||||
|
fetchSummaryData(); // Add this line to fetch summary data
|
||||||
setupClickListeners();
|
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() {
|
private void initViews() {
|
||||||
tvTotalAmount = findViewById(R.id.tv_total_amount);
|
tvTotalAmount = findViewById(R.id.tv_total_amount);
|
||||||
tvTotalTransactions = findViewById(R.id.tv_total_transactions);
|
tvTotalTransactions = findViewById(R.id.tv_total_transactions);
|
||||||
btnLihatDetailTop = findViewById(R.id.btn_lihat_detail);
|
btnLihatDetailTop = findViewById(R.id.btn_lihat_detail);
|
||||||
btnLihatDetailBottom = findViewById(R.id.btn_lihat_detail_bottom);
|
btnLihatDetailBottom = findViewById(R.id.btn_lihat_detail_bottom);
|
||||||
recyclerView = findViewById(R.id.recycler_view);
|
recyclerView = findViewById(R.id.recycler_view);
|
||||||
btnBack = findViewById(R.id.btn_back);
|
backNavigation = findViewById(R.id.back_navigation);
|
||||||
|
|
||||||
historyList = new ArrayList<>();
|
historyList = new ArrayList<>();
|
||||||
}
|
}
|
||||||
@@ -75,7 +96,7 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupClickListeners() {
|
private void setupClickListeners() {
|
||||||
btnBack.setOnClickListener(new View.OnClickListener() {
|
backNavigation.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
finish();
|
finish();
|
||||||
@@ -86,7 +107,7 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
try {
|
try {
|
||||||
Intent intent = new Intent(HistoryActivity.this, HistoryDetailActivity.class);
|
Intent intent = new Intent(HistoryActivity.this, HistoryListActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -103,14 +124,15 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
new ApiTask().execute(API_URL);
|
new ApiTask().execute(API_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fetchSummaryData() {
|
||||||
|
new SummaryApiTask().execute(SUMMARY_API_URL);
|
||||||
|
}
|
||||||
|
|
||||||
private void processApiData(JSONArray dataArray) {
|
private void processApiData(JSONArray dataArray) {
|
||||||
try {
|
try {
|
||||||
historyList.clear();
|
historyList.clear();
|
||||||
fullHistoryData.clear(); // Clear static data
|
fullHistoryData.clear(); // Clear static data
|
||||||
|
|
||||||
final long[] totalAmountArray = {0};
|
|
||||||
final int[] totalTransactionsArray = {0};
|
|
||||||
|
|
||||||
for (int i = 0; i < dataArray.length(); i++) {
|
for (int i = 0; i < dataArray.length(); i++) {
|
||||||
JSONObject item = dataArray.getJSONObject(i);
|
JSONObject item = dataArray.getJSONObject(i);
|
||||||
|
|
||||||
@@ -134,27 +156,19 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
historyItem.setDate(formatDate(transactionDate));
|
historyItem.setDate(formatDate(transactionDate));
|
||||||
historyItem.setAmount((long) amountValue);
|
historyItem.setAmount((long) amountValue);
|
||||||
historyItem.setChannelName(formatChannelName(channelCode));
|
historyItem.setChannelName(formatChannelName(channelCode));
|
||||||
historyItem.setStatus(status);
|
historyItem.setStatus(status.toUpperCase()); // Ensure uppercase for consistency
|
||||||
historyItem.setReferenceId(referenceId);
|
historyItem.setReferenceId(referenceId);
|
||||||
historyItem.setFullDate(transactionDate);
|
historyItem.setFullDate(transactionDate);
|
||||||
historyItem.setChannelCode(channelCode);
|
historyItem.setChannelCode(channelCode);
|
||||||
|
|
||||||
// Add to full data
|
// Add to both lists (since we're limiting to 10 in API call)
|
||||||
fullHistoryData.add(historyItem);
|
|
||||||
|
|
||||||
// Add first 10 to display list
|
|
||||||
if (i < 10) {
|
|
||||||
historyList.add(historyItem);
|
historyList.add(historyItem);
|
||||||
}
|
fullHistoryData.add(historyItem);
|
||||||
|
|
||||||
totalAmountArray[0] += (long) amountValue;
|
|
||||||
totalTransactionsArray[0]++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
updateSummary(totalAmountArray[0], totalTransactionsArray[0]);
|
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -171,6 +185,55 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private void updateSummary(long totalAmount, int totalTransactions) {
|
||||||
tvTotalAmount.setText("RP " + formatCurrency(totalAmount));
|
tvTotalAmount.setText("RP " + formatCurrency(totalAmount));
|
||||||
tvTotalTransactions.setText(String.valueOf(totalTransactions));
|
tvTotalTransactions.setText(String.valueOf(totalTransactions));
|
||||||
@@ -180,24 +243,35 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
historyList.clear();
|
historyList.clear();
|
||||||
fullHistoryData.clear();
|
fullHistoryData.clear();
|
||||||
|
|
||||||
// Create sample data
|
// 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 = {
|
HistoryItem[] sampleData = {
|
||||||
new HistoryItem("03:44", "11-05-2025", 2018619, "Kredit", "FAILED", "197870"),
|
new HistoryItem("08:30", todayDate, 1500000, "QRIS", "SUCCESS", "TXN001"),
|
||||||
new HistoryItem("03:10", "12-05-2025", 3974866, "QRIS", "SUCCESS", "053059"),
|
new HistoryItem("09:15", todayDate, 750000, "Debit", "SUCCESS", "TXN002"),
|
||||||
new HistoryItem("15:17", "13-05-2025", 2418167, "QRIS", "FAILED", "668320"),
|
new HistoryItem("10:20", todayDate, 2250000, "QRIS", "FAILED", "TXN003"),
|
||||||
new HistoryItem("12:09", "11-05-2025", 3429230, "Debit", "FAILED", "454790"),
|
new HistoryItem("11:45", todayDate, 980000, "Kredit", "SUCCESS", "TXN004"),
|
||||||
new HistoryItem("08:39", "10-05-2025", 4656447, "QRIS", "FAILED", "454248"),
|
new HistoryItem("12:30", todayDate, 1800000, "QRIS", "SUCCESS", "TXN005"),
|
||||||
new HistoryItem("00:35", "12-05-2025", 3507704, "QRIS", "FAILED", "301644"),
|
new HistoryItem("13:15", todayDate, 650000, "Debit", "FAILED", "TXN006"),
|
||||||
new HistoryItem("22:43", "13-05-2025", 4277904, "Debit", "SUCCESS", "388709"),
|
new HistoryItem("14:00", todayDate, 3200000, "QRIS", "SUCCESS", "TXN007"),
|
||||||
new HistoryItem("18:16", "11-05-2025", 4456904, "Debit", "FAILED", "986861")
|
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) {
|
for (HistoryItem item : sampleData) {
|
||||||
historyList.add(item);
|
historyList.add(item);
|
||||||
fullHistoryData.add(item);
|
fullHistoryData.add(item);
|
||||||
|
totalAmount += item.getAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
tvTotalAmount.setText("RP 36.166.829");
|
tvTotalAmount.setText("RP " + formatCurrency(totalAmount));
|
||||||
tvTotalTransactions.setText("10");
|
tvTotalTransactions.setText("10");
|
||||||
|
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
@@ -205,12 +279,16 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
private String formatChannelName(String channelCode) {
|
private String formatChannelName(String channelCode) {
|
||||||
switch (channelCode) {
|
switch (channelCode) {
|
||||||
case "DEBIT":
|
case "DEBIT_CARD":
|
||||||
return "Debit";
|
return "Debit";
|
||||||
|
case "CREDIT_CARD":
|
||||||
|
return "Kredit";
|
||||||
case "QRIS":
|
case "QRIS":
|
||||||
return "QRIS";
|
return "QRIS";
|
||||||
|
case "E_MONEY":
|
||||||
|
return "E-Money";
|
||||||
case "OTHER":
|
case "OTHER":
|
||||||
return "Kredit";
|
return "Lainnya";
|
||||||
default:
|
default:
|
||||||
return channelCode.substring(0, 1).toUpperCase() +
|
return channelCode.substring(0, 1).toUpperCase() +
|
||||||
channelCode.substring(1).toLowerCase();
|
channelCode.substring(1).toLowerCase();
|
||||||
@@ -220,22 +298,31 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
private String formatTime(String isoDate) {
|
private String formatTime(String isoDate) {
|
||||||
try {
|
try {
|
||||||
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
|
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
|
||||||
SimpleDateFormat outputFormat = new SimpleDateFormat("HH:mm", 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);
|
Date date = inputFormat.parse(isoDate);
|
||||||
return outputFormat.format(date);
|
return outputFormat.format(date);
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
return "00:00";
|
return "00.00.00";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String formatDate(String isoDate) {
|
private String formatDate(String isoDate) {
|
||||||
try {
|
try {
|
||||||
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
|
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());
|
SimpleDateFormat outputFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
|
||||||
|
outputFormat.setTimeZone(TimeZone.getDefault());
|
||||||
|
|
||||||
Date date = inputFormat.parse(isoDate);
|
Date date = inputFormat.parse(isoDate);
|
||||||
return outputFormat.format(date);
|
return outputFormat.format(date);
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
return "01-01-2025";
|
SimpleDateFormat fallbackFormat = new SimpleDateFormat("dd-MM-yyyy", Locale.getDefault());
|
||||||
|
return fallbackFormat.format(new Date());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +336,7 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
return new ArrayList<>(fullHistoryData);
|
return new ArrayList<>(fullHistoryData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsyncTask for API call
|
// AsyncTask for main API call (transaction list with limit)
|
||||||
private class ApiTask extends AsyncTask<String, Void, String> {
|
private class ApiTask extends AsyncTask<String, Void, String> {
|
||||||
@Override
|
@Override
|
||||||
protected String doInBackground(String... urls) {
|
protected String doInBackground(String... urls) {
|
||||||
@@ -302,6 +389,59 @@ public class HistoryActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// HistoryItem class - enhanced with more fields
|
||||||
@@ -366,21 +506,30 @@ class HistoryAdapter extends RecyclerView.Adapter<HistoryAdapter.HistoryViewHold
|
|||||||
public void onBindViewHolder(@NonNull HistoryViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull HistoryViewHolder holder, int position) {
|
||||||
HistoryItem item = historyList.get(position);
|
HistoryItem item = historyList.get(position);
|
||||||
|
|
||||||
holder.tvTime.setText(item.getTime() + ", " + item.getDate());
|
// 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.tvAmount.setText("Rp. " + formatCurrency(item.getAmount()));
|
||||||
holder.tvChannel.setText(item.getChannelName());
|
holder.tvChannel.setText(item.getChannelName());
|
||||||
|
|
||||||
// Set status color
|
// Set status color and text based on API response
|
||||||
String status = item.getStatus();
|
String status = item.getStatus();
|
||||||
if ("SUCCESS".equals(status)) {
|
if ("SETTLEMENT".equalsIgnoreCase(status)) {
|
||||||
holder.tvStatus.setText("Berhasil");
|
holder.tvStatus.setText("Success");
|
||||||
holder.tvStatus.setTextColor(0xFF4CAF50); // Green
|
holder.tvStatus.setTextColor(Color.parseColor("#4CAF50")); // Green
|
||||||
} else if ("FAILED".equals(status)) {
|
} else if ("SUCCESS".equalsIgnoreCase(status)) {
|
||||||
holder.tvStatus.setText("Gagal");
|
holder.tvStatus.setText("Success");
|
||||||
holder.tvStatus.setTextColor(0xFFF44336); // Red
|
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 {
|
} else {
|
||||||
holder.tvStatus.setText("Tertunda");
|
holder.tvStatus.setText("Pending");
|
||||||
holder.tvStatus.setTextColor(0xFFFF9800); // Orange
|
holder.tvStatus.setTextColor(Color.parseColor("#3141FF")); // Blue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
package com.example.bdkipoc;
|
package com.example.bdkipoc;
|
||||||
|
import com.example.bdkipoc.qris.view.QrisResultActivity;
|
||||||
|
import com.example.bdkipoc.BuildConfig;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -53,6 +55,7 @@ public class QrisActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
private String transactionId;
|
private String transactionId;
|
||||||
private String transactionUuid;
|
private String transactionUuid;
|
||||||
|
private String transactionDate;
|
||||||
private String referenceId;
|
private String referenceId;
|
||||||
private int amount;
|
private int amount;
|
||||||
private JSONObject midtransResponse;
|
private JSONObject midtransResponse;
|
||||||
@@ -68,10 +71,7 @@ public class QrisActivity extends AppCompatActivity {
|
|||||||
private static final long REFERENCE_COOLDOWN_MS = 60000; // 1 minute cooldown
|
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 long TRANSACTION_COOLDOWN_MS = 5000; // 5 second cooldown
|
||||||
|
|
||||||
private static final String BACKEND_BASE = "https://be-edc.msvc.app";
|
private static final String MIDTRANS_AUTH = BuildConfig.MIDTRANS_SANDBOX_AUTH;
|
||||||
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
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
@@ -119,6 +119,11 @@ public class QrisActivity extends AppCompatActivity {
|
|||||||
initiatePaymentButton.setEnabled(false);
|
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
|
* ✅ FRONTEND DEDUPLICATION: Generate unique reference ID with local tracking
|
||||||
*/
|
*/
|
||||||
@@ -303,10 +308,10 @@ public class QrisActivity extends AppCompatActivity {
|
|||||||
editTextAmount.setText(formatAmount(amountStr));
|
editTextAmount.setText(formatAmount(amountStr));
|
||||||
descriptionText.setText("Tekan Konfirmasi untuk melanjutkan");
|
descriptionText.setText("Tekan Konfirmasi untuk melanjutkan");
|
||||||
|
|
||||||
// Enable button if amount is valid
|
// Enable button if amount is valid (any positive amount)
|
||||||
try {
|
try {
|
||||||
int amt = Integer.parseInt(amountStr);
|
int amt = Integer.parseInt(amountStr);
|
||||||
initiatePaymentButton.setEnabled(amt >= 1000);
|
initiatePaymentButton.setEnabled(amt > 0); // Changed from >= 1000 to > 0
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
initiatePaymentButton.setEnabled(false);
|
initiatePaymentButton.setEnabled(false);
|
||||||
}
|
}
|
||||||
@@ -381,7 +386,8 @@ public class QrisActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
private boolean isValidServerKey(String serverKey) {
|
private boolean isValidServerKey(String serverKey) {
|
||||||
return serverKey != null &&
|
return serverKey != null &&
|
||||||
serverKey.startsWith("SB-Mid-server-") &&
|
(serverKey.startsWith("SB-Mid-server-") || // Sandbox format
|
||||||
|
serverKey.startsWith("Mid-server-")) && // Production format
|
||||||
serverKey.length() > 20;
|
serverKey.length() > 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,17 +452,17 @@ public class QrisActivity extends AppCompatActivity {
|
|||||||
// Parse amount - expecting integer in lowest denomination (Indonesian Rupiah)
|
// Parse amount - expecting integer in lowest denomination (Indonesian Rupiah)
|
||||||
amount = Integer.parseInt(amountText);
|
amount = Integer.parseInt(amountText);
|
||||||
|
|
||||||
// Validate minimum amount
|
// // Validate minimum amount
|
||||||
if (amount < 1000) {
|
// if (amount < 1000) {
|
||||||
errorMessage = "Minimum amount is IDR 1,000";
|
// errorMessage = "Minimum amount is IDR 1,000";
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Validate maximum amount for testing
|
// // Validate maximum amount for testing
|
||||||
if (amount > 10000000) {
|
// if (amount > 10000000) {
|
||||||
errorMessage = "Maximum amount is IDR 10,000,000";
|
// errorMessage = "Maximum amount is IDR 10,000,000";
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
|
|
||||||
Log.d("MidtransCharge", "Parsed amount: " + amount);
|
Log.d("MidtransCharge", "Parsed amount: " + amount);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
@@ -470,6 +476,7 @@ public class QrisActivity extends AppCompatActivity {
|
|||||||
payload.put("status", "INIT");
|
payload.put("status", "INIT");
|
||||||
payload.put("device_id", 1);
|
payload.put("device_id", 1);
|
||||||
payload.put("transaction_uuid", transactionUuid);
|
payload.put("transaction_uuid", transactionUuid);
|
||||||
|
payload.put("transaction_date", getCurrentDateTime());
|
||||||
payload.put("transaction_time_seconds", 0.0);
|
payload.put("transaction_time_seconds", 0.0);
|
||||||
payload.put("device_code", "PB4K252T00021");
|
payload.put("device_code", "PB4K252T00021");
|
||||||
payload.put("merchant_name", "Marcel Panjaitan");
|
payload.put("merchant_name", "Marcel Panjaitan");
|
||||||
@@ -479,7 +486,7 @@ public class QrisActivity extends AppCompatActivity {
|
|||||||
Log.d("MidtransCharge", "Backend transaction payload: " + payload.toString());
|
Log.d("MidtransCharge", "Backend transaction payload: " + payload.toString());
|
||||||
|
|
||||||
// Make the API call
|
// Make the API call
|
||||||
URL url = new URI(BACKEND_BASE + "/transactions").toURL();
|
URL url = new URI(BuildConfig.BACKEND_BASE_URL + "/transactions").toURL();
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
conn.setRequestMethod("POST");
|
conn.setRequestMethod("POST");
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
@@ -629,22 +636,22 @@ public class QrisActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
// Log the request details
|
// Log the request details
|
||||||
Log.d("MidtransCharge", "=== MIDTRANS QRIS REQUEST ===");
|
Log.d("MidtransCharge", "=== MIDTRANS QRIS REQUEST ===");
|
||||||
Log.d("MidtransCharge", "URL: " + MIDTRANS_CHARGE_URL);
|
Log.d("MidtransCharge", "URL: " + BuildConfig.MIDTRANS_CHARGE_URL);
|
||||||
Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
|
Log.d("MidtransCharge", "Authorization: " + MIDTRANS_AUTH);
|
||||||
Log.d("MidtransCharge", "X-Override-Notification: " + WEBHOOK_URL);
|
Log.d("MidtransCharge", "X-Override-Notification: " + BuildConfig.WEBHOOK_URL);
|
||||||
Log.d("MidtransCharge", "Reference ID: " + referenceId);
|
Log.d("MidtransCharge", "Reference ID: " + referenceId);
|
||||||
Log.d("MidtransCharge", "Order ID: " + transactionUuid);
|
Log.d("MidtransCharge", "Order ID: " + transactionUuid);
|
||||||
Log.d("MidtransCharge", "Amount: " + amount);
|
Log.d("MidtransCharge", "Amount: " + amount);
|
||||||
Log.d("MidtransCharge", "================================");
|
Log.d("MidtransCharge", "================================");
|
||||||
|
|
||||||
// Make the API call to Midtrans
|
// Make the API call to Midtrans
|
||||||
URL url = new URI(MIDTRANS_CHARGE_URL).toURL();
|
URL url = new URI(BuildConfig.MIDTRANS_CHARGE_URL).toURL();
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
conn.setRequestMethod("POST");
|
conn.setRequestMethod("POST");
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
|
conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
|
||||||
conn.setRequestProperty("X-Override-Notification", WEBHOOK_URL);
|
conn.setRequestProperty("X-Override-Notification", BuildConfig.WEBHOOK_URL);
|
||||||
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
|
conn.setRequestProperty("User-Agent", "BDKIPOCApp/1.0");
|
||||||
conn.setDoOutput(true);
|
conn.setDoOutput(true);
|
||||||
conn.setConnectTimeout(30000); // 30 seconds
|
conn.setConnectTimeout(30000); // 30 seconds
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.bdkipoc;
|
package com.example.bdkipoc.settlement;
|
||||||
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -8,6 +8,9 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.content.Intent;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
@@ -20,11 +23,16 @@ import java.io.IOException;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import com.example.bdkipoc.BuildConfig;
|
||||||
|
import com.example.bdkipoc.R;
|
||||||
|
|
||||||
public class SettlementActivity extends AppCompatActivity {
|
public class SettlementActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private TextView tvTotalAmount;
|
private TextView tvTotalAmount;
|
||||||
@@ -32,9 +40,8 @@ public class SettlementActivity extends AppCompatActivity {
|
|||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private SettlementAdapter adapter;
|
private SettlementAdapter adapter;
|
||||||
private List<SettlementItem> settlementList;
|
private List<SettlementItem> settlementList;
|
||||||
private ImageView btnBack;
|
private LinearLayout backNavigation;
|
||||||
|
private Button btnContinue;
|
||||||
private String API_URL = "https://be-edc.msvc.app/transactions/performa-chanel-pembayaran?from_date=2025-01-01&to_date=2025-06-04&location_id=0&merchant_id=0";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -48,8 +55,18 @@ public class SettlementActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void fetchApiData() {
|
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
|
// Execute network call in background thread
|
||||||
new ApiTask().execute(API_URL);
|
new ApiTask().execute(apiUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processApiData(JSONArray dataArray) {
|
private void processApiData(JSONArray dataArray) {
|
||||||
@@ -112,7 +129,8 @@ public class SettlementActivity extends AppCompatActivity {
|
|||||||
tvTotalAmount = findViewById(R.id.tv_total_amount);
|
tvTotalAmount = findViewById(R.id.tv_total_amount);
|
||||||
tvTotalTransactions = findViewById(R.id.tv_total_transactions);
|
tvTotalTransactions = findViewById(R.id.tv_total_transactions);
|
||||||
recyclerView = findViewById(R.id.recycler_view);
|
recyclerView = findViewById(R.id.recycler_view);
|
||||||
btnBack = findViewById(R.id.btn_back);
|
backNavigation = findViewById(R.id.back_navigation);
|
||||||
|
btnContinue = findViewById(R.id.btn_continue);
|
||||||
|
|
||||||
settlementList = new ArrayList<>();
|
settlementList = new ArrayList<>();
|
||||||
}
|
}
|
||||||
@@ -124,12 +142,21 @@ public class SettlementActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupClickListeners() {
|
private void setupClickListeners() {
|
||||||
btnBack.setOnClickListener(new View.OnClickListener() {
|
// Updated to use backNavigation instead of btnBack
|
||||||
|
backNavigation.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
finish();
|
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() {
|
private void loadSampleData() {
|
||||||
@@ -216,9 +243,6 @@ public class SettlementActivity extends AppCompatActivity {
|
|||||||
return formatter.format(amount);
|
return formatter.format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated helper class - no longer needed
|
|
||||||
// private static class ChannelData { ... }
|
|
||||||
|
|
||||||
// AsyncTask for API call
|
// AsyncTask for API call
|
||||||
private class ApiTask extends AsyncTask<String, Void, String> {
|
private class ApiTask extends AsyncTask<String, Void, String> {
|
||||||
@Override
|
@Override
|
||||||
@@ -230,6 +254,9 @@ public class SettlementActivity extends AppCompatActivity {
|
|||||||
connection.setConnectTimeout(5000);
|
connection.setConnectTimeout(5000);
|
||||||
connection.setReadTimeout(5000);
|
connection.setReadTimeout(5000);
|
||||||
|
|
||||||
|
// Add authorization header if needed
|
||||||
|
// connection.setRequestProperty("Authorization", BuildConfig.MIDTRANS_SANDBOX_AUTH);
|
||||||
|
|
||||||
int responseCode = connection.getResponseCode();
|
int responseCode = connection.getResponseCode();
|
||||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||||
@@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:duration="300"
|
android:duration="300"
|
||||||
android:fromAlpha="0.0"
|
android:fromAlpha="0.0"
|
||||||
android:toAlpha="1.0" />
|
android:toAlpha="1.0" />
|
||||||
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_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>
|
||||||
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>
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:shape="rectangle">
|
android:shape="rectangle">
|
||||||
<solid android:color="#3498DB" />
|
|
||||||
<corners android:radius="12dp" />
|
<!-- Warna solid (biru cerah seperti #4299E1) -->
|
||||||
<stroke android:width="0dp" />
|
<solid android:color="#4299E1" />
|
||||||
|
|
||||||
|
<!-- Sudut melengkung -->
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
|
||||||
|
<!-- Hilangkan stroke jika tidak dibutuhkan -->
|
||||||
|
<stroke android:width="0dp" android:color="#00000000" />
|
||||||
</shape>
|
</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_e_money.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<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="#000000"
|
|
||||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,16h2v-1h1c0.55,0 1,-0.45 1,-1v-3c0,-0.55 -0.45,-1 -1,-1h-3v-1h4V8h-2V7h-2v1h-1c-0.55,0 -1,0.45 -1,1v3c0,0.55 0.45,1 1,1h3v1H9v2h2v1z"/>
|
|
||||||
</vector>
|
|
||||||
BIN
app/src/main/res/drawable/ic_logo_toko.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
app/src/main/res/drawable/ic_settings.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/drawable/ic_settlement.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<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="#000000"
|
|
||||||
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM12,7c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,19L6,19v-1.4c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1L18,19z"/>
|
|
||||||
</vector>
|
|
||||||
BIN
app/src/main/res/drawable/ic_transfer.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<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="#000000"
|
|
||||||
android:pathData="M16,17.01V10h-2v7.01h-3L15,20l4,-3h-3zM9,3L5,6h3v7.01h2V6h3L9,3z"/>
|
|
||||||
</vector>
|
|
||||||
10
app/src/main/res/drawable/ic_visibility.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/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/ic_visibility_off.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/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/black"
|
||||||
|
android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
|
||||||
|
</vector>
|
||||||
BIN
app/src/main/res/drawable/ic_whatsapp.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
8
app/src/main/res/drawable/oval_blur_decoration.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<!-- Semi-transparent color similar to #CCB2B24D -->
|
||||||
|
<solid android:color="#30FFFFFF" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
13
app/src/main/res/drawable/rounded_bottom_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="#DE0701" />
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:bottomLeftRadius="48dp"
|
||||||
|
android:bottomRightRadius="48dp"
|
||||||
|
android:topLeftRadius="0dp"
|
||||||
|
android:topRightRadius="0dp" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
8
app/src/main/res/drawable/tab_active_bg.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!-- res/drawable/tab_active_bg.xml -->
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#DE0701" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#FFFFFF" />
|
||||||
|
</shape>
|
||||||
8
app/src/main/res/drawable/tab_inactive_bg.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!-- res/drawable/tab_inactive_bg.xml -->
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#FFFFFF" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="#DE0701" />
|
||||||
|
</shape>
|
||||||
5
app/src/main/res/drawable/timer_circle_background.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#F0F0F0"/>
|
||||||
|
<corners android:radius="24dp"/>
|
||||||
|
<stroke android:width="1dp" android:color="#E0E0E0"/>
|
||||||
|
</shape>
|
||||||
@@ -8,4 +8,8 @@
|
|||||||
app:font="@font/inter_medium"
|
app:font="@font/inter_medium"
|
||||||
app:fontWeight="500"
|
app:fontWeight="500"
|
||||||
app:fontStyle="normal"/>
|
app:fontStyle="normal"/>
|
||||||
|
<font
|
||||||
|
app:font="@font/inter_bold"
|
||||||
|
app:fontWeight="700"
|
||||||
|
app:fontStyle="normal"/>
|
||||||
</font-family>
|
</font-family>
|
||||||
BIN
app/src/main/res/font/inter_bold.ttf
Normal file
518
app/src/main/res/layout/activity_bantuan.xml
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="#FFFFFF">
|
||||||
|
|
||||||
|
<!-- Custom AppBar -->
|
||||||
|
<include layout="@layout/component_appbar" />
|
||||||
|
|
||||||
|
<!-- Main Card Container -->
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="480dp"
|
||||||
|
android:layout_marginTop="-80dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardCornerRadius="12dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@android:color/white">
|
||||||
|
|
||||||
|
<!-- Logo Section with Tabs -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="94.149dp"
|
||||||
|
android:layout_height="38.248dp"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:layout_marginLeft="10.9dp"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:src="@drawable/ic_logo_icon"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:contentDescription="Payvora PRO Logo"/>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/tab_umum"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:background="@drawable/tab_active_bg"
|
||||||
|
android:layout_marginEnd="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_umum"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Umum"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/tab_riwayat"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/tab_inactive_bg"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:layout_marginStart="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_riwayat"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Riwayat"
|
||||||
|
android:textColor="#DE0701"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<!-- Content Umum (Static help content) -->
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/content_umum"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:visibility="visible">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Help Items -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Transaksi gagal tapi saldo terpotong"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="QRIS tidak terbaca"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="EDC tidak merespon / hang"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Gagal cetak struk"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Cara Reset EDC"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Cara Hubungkan ke WiFi"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Cara pembayaran kartu (debit/kredit)"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Cara Refund Transaksi QRIS"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Cara melakukan Settlement"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Cara membatalkan transaksi (void)"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#e0e0e0"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Cara melihat riwayat transaksi"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Lihat Solusi"
|
||||||
|
android:textColor="@android:color/holo_blue_light"
|
||||||
|
android:textSize="14sp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<!-- Content Riwayat (Dynamic content from API) -->
|
||||||
|
<ScrollView
|
||||||
|
android:id="@+id/content_riwayat"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Dynamic content will be added here programmatically -->
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<!-- Bottom Buttons -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingVertical="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<!-- Tombol Isi Form -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/btn_form"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@drawable/border_button_red">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Isi Form Bantuan"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#DE0701"
|
||||||
|
android:fontFamily="sans-serif-medium" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Tombol WhatsApp -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/btn_whatsapp"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@drawable/border_button_red"
|
||||||
|
android:paddingHorizontal="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:src="@drawable/ic_whatsapp"
|
||||||
|
android:contentDescription="WhatsApp Icon"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="WhatsApp CS"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#DE0701"
|
||||||
|
android:fontFamily="sans-serif-medium" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
305
app/src/main/res/layout/activity_bantuan_form.xml
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#F5F5F5">
|
||||||
|
|
||||||
|
<!-- Main Content (Original Layout) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/main_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<!-- Custom AppBar -->
|
||||||
|
<include layout="@layout/component_appbar_small" />
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Form Card -->
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardCornerRadius="12dp"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:cardBackgroundColor="@android:color/white">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<!-- Form Title -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Formulir Helpdesk untuk Merchant"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="#DE0701"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<!-- Form Subtitle -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Mohon isi data Anda pada formulir di bawah ini untuk melaporkan masalah yang terjadi."
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:lineSpacingExtra="2dp" />
|
||||||
|
|
||||||
|
<!-- Ticket Code Field -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Ticket Code"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_ticket_code"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:drawable/editbox_background"
|
||||||
|
android:hint="Masukkan kode tiket (contoh: #20240312)"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColorHint="#AAAAAA"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:singleLine="true" />
|
||||||
|
|
||||||
|
<!-- Source Field -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Source"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_source"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:drawable/editbox_background"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:padding="16dp" />
|
||||||
|
|
||||||
|
<!-- Issue Field -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Issue"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_issue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:drawable/editbox_background"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:padding="16dp" />
|
||||||
|
|
||||||
|
<!-- Merchant Field -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Merchant"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_merchant"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:drawable/editbox_background"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:padding="16dp" />
|
||||||
|
|
||||||
|
<!-- Status Field (Read-only) -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Status"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:drawable/editbox_background"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:layout_marginBottom="20dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Pengajuan"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#666666" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Assign Field -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Assign To"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_assign"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:drawable/editbox_background"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:padding="16dp" />
|
||||||
|
|
||||||
|
<!-- Resolved Date Field -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Resolved Date"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/ll_resolved_date"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:drawable/editbox_background"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_resolved_date"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Pilih Tanggal Resolved"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="#AAAAAA" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@android:drawable/ic_menu_my_calendar"
|
||||||
|
android:contentDescription="Calendar Icon" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:elevation="4dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_kirim"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:text="Kirim Sekarang"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:background="@android:color/darker_gray"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:enabled="false"
|
||||||
|
android:stateListAnimator="@null"
|
||||||
|
android:elevation="0dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Success Screen (Full Screen Overlay) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/success_screen"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="#E31937"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<!-- Success Icon -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/success_icon"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:src="@drawable/ic_success_payment"
|
||||||
|
android:layout_marginBottom="32dp"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:contentDescription="Success Icon" />
|
||||||
|
|
||||||
|
<!-- Success Message -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/success_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Formulir Berhasil Dikirim"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:letterSpacing="0.02"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
328
app/src/main/res/layout/activity_create_transaction.xml
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#FFFFFF"
|
||||||
|
tools:context=".transaction.CreateTransactionActivity">
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:scrollbars="none"
|
||||||
|
android:background="#FFFFFF">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<include layout="@layout/component_appbar" />
|
||||||
|
|
||||||
|
<!-- Payment Card - Positioned to overlap with red header -->
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/paymentCard"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="191dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginTop="-85dp"
|
||||||
|
app:cardBackgroundColor="#3498DB"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
app:cardElevation="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp"
|
||||||
|
android:paddingTop="32dp">
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="TOTAL PEMBAYARAN"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:layout_marginBottom="20dp" />
|
||||||
|
|
||||||
|
<!-- Amount Input Section -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="RP"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_gravity="bottom" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_amount_display"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="32sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:text=""
|
||||||
|
android:gravity="start"
|
||||||
|
android:paddingBottom="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- White Underline -->
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Pastikan kembali nominal pembayaran pelanggan Anda"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:textStyle="normal"
|
||||||
|
android:alpha="0.9" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<!-- Numpad -->
|
||||||
|
<GridLayout
|
||||||
|
android:id="@+id/numpad_grid"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:columnCount="3"
|
||||||
|
android:rowCount="4"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp">
|
||||||
|
|
||||||
|
<!-- Row 1: 1, 2, 3 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_1"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_2"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="2" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_3"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="3" />
|
||||||
|
|
||||||
|
<!-- Row 2: 4, 5, 6 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_4"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="4" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_5"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="5" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_6"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="6" />
|
||||||
|
|
||||||
|
<!-- Row 3: 7, 8, 9 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_7"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="7" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_8"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="8" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_9"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="9" />
|
||||||
|
|
||||||
|
<!-- Row 4: 000, 0, Delete -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_00"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="000" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btn_0"
|
||||||
|
style="@style/NumpadButton"
|
||||||
|
android:text="0" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_clear"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_columnWeight="1"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_backspace"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:contentDescription="Delete" />
|
||||||
|
|
||||||
|
</GridLayout>
|
||||||
|
|
||||||
|
<!-- Confirmation Button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btn_confirm"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:text="Konfirmasi"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
app:backgroundTint="@null"
|
||||||
|
android:background="@drawable/button_confirm_background_selector"
|
||||||
|
android:enabled="false"
|
||||||
|
app:cornerRadius="8dp"
|
||||||
|
app:rippleColor="#B3000000" />
|
||||||
|
|
||||||
|
<!-- Hidden Components -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_mode_indicator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_toggle_mode"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:indeterminateTint="#E31937" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<!-- Modal Overlay -->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/modal_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#80000000"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:elevation="100dp">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/modal_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_margin="32dp"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
app:cardElevation="20dp"
|
||||||
|
app:cardBackgroundColor="@android:color/white">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="32dp"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/modal_icon"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:src="@drawable/ic_card_insert"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
app:tint="#E31937" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/modal_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Silakan Tempelkan / Gesekkan / Masukkan Kartu ke Perangkat"
|
||||||
|
style="@style/StatusTextStyle"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:gravity="center"
|
||||||
|
android:lineSpacingExtra="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/success_screen"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="#E31937"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:elevation="100dp">
|
||||||
|
|
||||||
|
<!-- Success Icon -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/success_icon"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:src="@drawable/ic_success_payment"
|
||||||
|
android:layout_marginBottom="32dp"
|
||||||
|
android:scaleType="centerInside"/>
|
||||||
|
|
||||||
|
<!-- Success Message -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/success_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Pembayaran Berhasil"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:gravity="center"
|
||||||
|
android:letterSpacing="0.02"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -3,136 +3,124 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="#F5F5F5"
|
android:background="#FFFFFF"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<!-- Header with red background -->
|
<!-- Custom AppBar -->
|
||||||
<RelativeLayout
|
<include layout="@layout/component_appbar" />
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="200dp"
|
|
||||||
android:background="#E53E3E">
|
|
||||||
|
|
||||||
<!-- Back button -->
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/btn_back"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:padding="4dp"
|
|
||||||
android:src="@android:drawable/ic_menu_revert"
|
|
||||||
app:tint="@android:color/white" />
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_toEndOf="@id/btn_back"
|
|
||||||
android:text="Kembali"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="16sp" />
|
|
||||||
|
|
||||||
<!-- History Card -->
|
<!-- History Card -->
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="120dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="64dp"
|
android:layout_marginTop="-70dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="5dp"
|
||||||
app:cardCornerRadius="12dp"
|
app:cardCornerRadius="16dp"
|
||||||
app:cardElevation="4dp">
|
app:cardElevation="6dp">
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:background="#4299E1"
|
android:background="@drawable/card_background"
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
<LinearLayout
|
<!-- Judul -->
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Transaksi Hari Ini"
|
android:text="Transaksi Hari Ini"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="14sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold" />
|
android:fontFamily="@font/inter"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<!-- Lihat Detail -->
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/btn_lihat_detail"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:text="Lihat Detail"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:textStyle="normal"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="1.0"
|
||||||
|
app:layout_constraintVertical_bias="0.5" />
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Label Total Transaksi -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/label_total"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
android:text="Total Transaksi"
|
android:text="Total Transaksi"
|
||||||
android:textColor="#E0FFFFFF"
|
android:textColor="#E0FFFFFF"
|
||||||
android:textSize="12sp" />
|
android:textSize="10sp"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:textStyle="normal"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<!-- Nilai Total Transaksi -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_total_amount"
|
android:id="@+id/tv_total_amount"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:text="RP 4.500.000"
|
android:text="RP 0"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold" />
|
android:fontFamily="@font/inter"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/label_total"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<!-- Label Jumlah Transaksi -->
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/label_jumlah"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="Jumlah Transaksi"
|
android:text="Jumlah Transaksi"
|
||||||
android:textColor="#E0FFFFFF"
|
android:textColor="#E0FFFFFF"
|
||||||
android:textSize="12sp" />
|
android:fontFamily="@font/inter"
|
||||||
|
android:textStyle="normal"
|
||||||
|
android:textSize="10sp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tv_total_amount"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<!-- Nilai Jumlah Transaksi -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_total_transactions"
|
android:id="@+id/tv_total_transactions"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="30"
|
android:text="0"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="16sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold" />
|
android:fontFamily="@font/inter"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/label_jumlah"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/btn_lihat_detail"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:text="Lihat Detail"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -145,10 +133,11 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="2dp"
|
||||||
android:text="Transaksi Terbaru"
|
android:text="Transaksi Terbaru"
|
||||||
android:textColor="#333333"
|
android:textColor="#000000"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<!-- Transaction List -->
|
<!-- Transaction List -->
|
||||||
@@ -170,6 +159,8 @@
|
|||||||
android:text="Lihat Detail"
|
android:text="Lihat Detail"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:fontFamily="@font/inter"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:cornerRadius="8dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="#F5F5F5"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:background="#E53E3E"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/btn_back"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:src="@android:drawable/ic_menu_revert"
|
|
||||||
app:tint="@android:color/white" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:text="Detail Transaksi"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:padding="16dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||