This commit is contained in:
riz081 2025-06-25 00:35:03 +07:00
parent f5d9e53118
commit f2c3de9f5f
27 changed files with 1203 additions and 2246 deletions

View File

@ -45,29 +45,32 @@
</intent-filter>
</activity>
<activity
android:name=".TransactionActivity"
android:name=".cetakulang.ReprintActivity"
android:exported="false" />
<activity
android:name=".PaymentActivity"
android:exported="false" />
<activity
android:name=".PinActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:name=".cetakulang.ReprintAdapterActivity"
android:exported="false" />
<activity
android:name=".ReceiptActivity"
android:exported="false" />
<activity
android:name=".QrisActivity"
android:exported="false" />
<activity android:name=".QrisResultActivity" />
<activity
android:name=".QrisResultActivity"
android:exported="false" />
<activity
android:name=".SettlementActivity"
android:exported="false" />
<activity
android:name=".HistoryActivity"
android:exported="false" />
<activity
android:name=".HistoryDetailActivity"
android:exported="false" />
@ -77,7 +80,7 @@
android:exported="false" />
<activity
android:name=".kredit.CreditCardActivity"
android:name=".transaction.ResultTransactionActivity"
android:exported="false" />
<activity android:name="com.sunmi.emv.l2.view.AppSelectActivity"/>

View File

@ -19,8 +19,11 @@ import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.button.MaterialButton;
import com.example.bdkipoc.cetakulang.ReprintActivity;
import com.example.bdkipoc.cetakulang.ReprintAdapterActivity;
import com.example.bdkipoc.transaction.CreateTransactionActivity;
import com.example.bdkipoc.kredit.CreditCardActivity;
import com.example.bdkipoc.transaction.ResultTransactionActivity;
public class MainActivity extends AppCompatActivity {
@ -86,12 +89,9 @@ public class MainActivity extends AppCompatActivity {
// 6 dummy menus should be hidden initially
CardView[] dummyCards = {
findViewById(R.id.card_dummy_menu_1),
findViewById(R.id.card_dummy_menu_2),
findViewById(R.id.card_dummy_menu_3),
findViewById(R.id.card_dummy_menu_4),
findViewById(R.id.card_dummy_menu_5),
findViewById(R.id.card_dummy_menu_6)
findViewById(R.id.card_bantuan),
findViewById(R.id.card_info_toko),
findViewById(R.id.card_pengaturan),
};
for (CardView card : dummyCards) {
@ -138,21 +138,17 @@ public class MainActivity extends AppCompatActivity {
R.id.card_kartu_debit,
R.id.card_qris,
// Row 2 (Always visible - 3 items)
R.id.card_transfer,
R.id.card_uang_elektronik,
R.id.card_cetak_ulang,
R.id.card_settlement,
// Row 3 (Always visible - 3 items)
R.id.card_refund,
R.id.card_settlement,
R.id.card_histori,
R.id.card_bantuan,
R.id.card_info_toko,
// Row 4 (Hidden initially - 3 items)
R.id.card_dummy_menu_1,
R.id.card_dummy_menu_2,
R.id.card_dummy_menu_3,
// Row 5 (Hidden initially - 3 items)
R.id.card_dummy_menu_4,
R.id.card_dummy_menu_5,
R.id.card_dummy_menu_6
R.id.card_bantuan,
R.id.card_info_toko,
R.id.card_pengaturan,
};
// Set up click listeners for all cards
@ -163,33 +159,30 @@ public class MainActivity extends AppCompatActivity {
if (cardId == R.id.card_kartu_kredit) {
startActivity(new Intent(MainActivity.this, CreateTransactionActivity.class));
} else if (cardId == R.id.card_kartu_debit) {
startActivity(new Intent(MainActivity.this, PaymentActivity.class));
startActivity(new Intent(MainActivity.this, CreateTransactionActivity.class));
} else if (cardId == R.id.card_qris) {
startActivity(new Intent(MainActivity.this, QrisActivity.class));
// Col-2
} else if (cardId == R.id.card_transfer) {
startActivity(new Intent(MainActivity.this, CreateTransactionActivity.class));
} else if (cardId == R.id.card_uang_elektronik) {
startActivity(new Intent(MainActivity.this, PaymentActivity.class));
startActivity(new Intent(MainActivity.this, CreateTransactionActivity.class));
} else if (cardId == R.id.card_cetak_ulang) {
startActivity(new Intent(MainActivity.this, TransactionActivity.class));
startActivity(new Intent(MainActivity.this, ReprintActivity.class));
// Col-3
} else if (cardId == R.id.card_refund) {
startActivity(new Intent(MainActivity.this, CreateTransactionActivity.class));
} else if (cardId == R.id.card_settlement) {
Toast.makeText(this, "Settlement - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_histori) {
Toast.makeText(this, "Histori - Coming Soon", Toast.LENGTH_SHORT).show();
startActivity(new Intent(MainActivity.this, HistoryActivity.class));
// Col-4
} else if (cardId == R.id.card_bantuan) {
Toast.makeText(this, "Bantuan - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_info_toko) {
Toast.makeText(this, "Info Toko - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_dummy_menu_1) {
Toast.makeText(this, "Dummy Menu 1 - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_dummy_menu_2) {
Toast.makeText(this, "Dummy Menu 2 - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_dummy_menu_3) {
Toast.makeText(this, "Dummy Menu 3 - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_dummy_menu_4) {
Toast.makeText(this, "Dummy Menu 4 - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_dummy_menu_5) {
Toast.makeText(this, "Dummy Menu 5 - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_dummy_menu_6) {
Toast.makeText(this, "Dummy Menu 6 - Coming Soon", Toast.LENGTH_SHORT).show();
} else if (cardId == R.id.card_pengaturan) {
Toast.makeText(this, "Pengaturan - Coming Soon", Toast.LENGTH_SHORT).show();
} else {
// Fallback for any other cards
Toast.makeText(this, "Menu Diklik: " + getResources().getResourceEntryName(cardId), Toast.LENGTH_SHORT).show();
@ -200,12 +193,9 @@ public class MainActivity extends AppCompatActivity {
// Get references to ONLY the dummy cards that need to be toggled
CardView[] toggleableCards = {
findViewById(R.id.card_dummy_menu_1),
findViewById(R.id.card_dummy_menu_2),
findViewById(R.id.card_dummy_menu_3),
findViewById(R.id.card_dummy_menu_4),
findViewById(R.id.card_dummy_menu_5),
findViewById(R.id.card_dummy_menu_6)
findViewById(R.id.card_bantuan),
findViewById(R.id.card_info_toko),
findViewById(R.id.card_pengaturan),
};
// Set up "Lainnya" button click listener

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -24,6 +24,8 @@ import java.io.OutputStream;
import java.net.URL;
import java.net.URI;
import com.example.bdkipoc.cetakulang.ReprintActivity;
public class ReceiptActivity extends AppCompatActivity {
// Views
@ -902,9 +904,9 @@ public class ReceiptActivity extends AppCompatActivity {
if (callingActivity != null) {
switch (callingActivity) {
case "TransactionActivity":
case "ReprintActivity":
// Go back to transaction list
Intent transactionIntent = new Intent(this, TransactionActivity.class);
Intent transactionIntent = new Intent(this, ReprintActivity.class);
transactionIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(transactionIntent);
break;

View File

@ -1,5 +1,6 @@
package com.example.bdkipoc;
package com.example.bdkipoc.cetakulang;
import com.example.bdkipoc.R;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
@ -50,9 +51,12 @@ import java.util.TimeZone;
import android.app.DatePickerDialog;
import android.widget.DatePicker;
public class TransactionActivity extends AppCompatActivity implements TransactionAdapter.OnPrintClickListener {
import com.example.bdkipoc.ReceiptActivity;
import com.example.bdkipoc.StyleHelper;
public class ReprintActivity extends AppCompatActivity implements ReprintAdapterActivity.OnPrintClickListener {
private RecyclerView recyclerView;
private TransactionAdapter adapter;
private ReprintAdapterActivity adapter;
private List<Transaction> transactionList;
private List<Transaction> filteredList;
private ProgressBar progressBar;
@ -89,7 +93,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transaction);
setContentView(R.layout.activity_reprint);
// Initialize SharedPreferences for local tracking
prefs = getSharedPreferences("transaction_prefs", MODE_PRIVATE);
@ -159,7 +163,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
transactionList = new ArrayList<>();
filteredList = new ArrayList<>();
adapter = new TransactionAdapter(filteredList);
adapter = new ReprintAdapterActivity(filteredList);
adapter.setPrintClickListener(this);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
@ -342,13 +346,13 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
filterButtonText.setTextColor(getResources().getColor(android.R.color.holo_blue_dark));
filterButtonText.setTextSize(12); // Smaller text when filter is active
Log.d("TransactionActivity", "🎨 Filter button updated: " + displayText);
Log.d("ReprintActivity", "🎨 Filter button updated: " + displayText);
}
}
// NEW METHOD: Apply date filter
private void applyDateFilter() {
Log.d("TransactionActivity", "🗓️ Applying date filter: " + fromDate + " to " + toDate);
Log.d("ReprintActivity", "🗓️ Applying date filter: " + fromDate + " to " + toDate);
// Reset to first page and reload data
currentPage = 1;
@ -366,7 +370,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
filterButtonText.setTextSize(14); // Reset to normal size
}
Log.d("TransactionActivity", "🗓️ Date filter cleared");
Log.d("ReprintActivity", "🗓️ Date filter cleared");
// Reload data without date filter
currentPage = 1;
@ -377,7 +381,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
return;
}
Log.d("TransactionActivity", "🔄 Navigating to page " + page);
Log.d("ReprintActivity", "🔄 Navigating to page " + page);
if (currentSearchQuery.isEmpty()) {
// Load from API
@ -410,7 +414,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
// Scroll to top
recyclerView.scrollToPosition(0);
Log.d("TransactionActivity", "📄 Displaying search results page " + currentPage +
Log.d("ReprintActivity", "📄 Displaying search results page " + currentPage +
" (items " + (startIndex + 1) + "-" + endIndex + " of " + filteredList.size() + ")");
}
@ -452,10 +456,10 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
totalPages = (int) Math.ceil((double) totalRecords / itemsPerPage);
// PASTIKAN TIDAK PERLU SORT LAGI karena sudah sorted dari API response
Log.d("TransactionActivity", "📋 FILTERED LIST ORDER (no search - maintaining API order):");
Log.d("ReprintActivity", "📋 FILTERED LIST ORDER (no search - maintaining API order):");
for (int i = 0; i < Math.min(5, filteredList.size()); i++) {
Transaction tx = filteredList.get(i);
Log.d("TransactionActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
Log.d("ReprintActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
}
} else {
// SEARCH MODE: Filter all available data
@ -518,7 +522,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
paginationControls.setVisibility(View.GONE);
}
Log.d("TransactionActivity", "📊 Pagination updated: " +
Log.d("ReprintActivity", "📊 Pagination updated: " +
"Page " + currentPage + "/" + totalPages + ", Total: " + totalRecords);
}
@ -584,7 +588,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
pageNumbersContainer.addView(pageButton);
}
Log.d("TransactionActivity", "🔢 Page buttons created: " + startPage + " to " + endPage +
Log.d("ReprintActivity", "🔢 Page buttons created: " + startPage + " to " + endPage +
" with size: " + buttonSize + "px");
}
@ -613,7 +617,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
String urlString = "https://be-edc.msvc.app/transactions?page=" + apiPage +
"&limit=" + itemsPerPage + "&sortOrder=DESC&from_date=" + fromDate + "&to_date=" + toDate + "&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=created_at";
Log.d("TransactionActivity", "🔍 Fetching transactions page " + pageToLoad +
Log.d("ReprintActivity", "🔍 Fetching transactions page " + pageToLoad +
" (API page " + apiPage + ") with limit " + itemsPerPage + " - SORT: DESC by created_at" +
" - Date Filter: " + fromDate + " to " + toDate);
@ -642,7 +646,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
apiTotal = results.getInt("total");
JSONArray data = results.getJSONArray("data");
Log.d("TransactionActivity", "📊 API response: " + data.length() +
Log.d("ReprintActivity", "📊 API response: " + data.length() +
" records, total: " + apiTotal);
// STEP 1: Parse all transactions from API
@ -667,14 +671,14 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
// STEP 2: Apply intelligent deduplication
result = applyAdvancedDeduplication(rawTransactions);
Log.d("TransactionActivity", "✅ After advanced deduplication: " + result.size() + " unique transactions");
Log.d("ReprintActivity", "✅ After advanced deduplication: " + result.size() + " unique transactions");
} else {
Log.e("TransactionActivity", "❌ HTTP Error: " + responseCode);
Log.e("ReprintActivity", "❌ HTTP Error: " + responseCode);
error = true;
}
} catch (IOException | JSONException | URISyntaxException e) {
Log.e("TransactionActivity", "❌ Exception: " + e.getMessage(), e);
Log.e("ReprintActivity", "❌ Exception: " + e.getMessage(), e);
error = true;
}
return result;
@ -687,7 +691,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
progressBar.setVisibility(View.GONE);
if (error) {
Toast.makeText(TransactionActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
Toast.makeText(ReprintActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show();
updatePaginationDisplay();
return;
}
@ -705,11 +709,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
if (date1 != null && date2 != null) {
int comparison = date2.compareTo(date1); // Newest first
Log.d("TransactionActivity", "🔄 Sorting: " + t2.createdAt + " vs " + t1.createdAt + " = " + comparison);
Log.d("ReprintActivity", "🔄 Sorting: " + t2.createdAt + " vs " + t1.createdAt + " = " + comparison);
return comparison;
}
} catch (Exception e) {
Log.w("TransactionActivity", "Date comparison error: " + e.getMessage());
Log.w("ReprintActivity", "Date comparison error: " + e.getMessage());
}
return Integer.compare(t2.id, t1.id); // Fallback by ID (higher ID = newer)
});
@ -718,14 +722,14 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
transactionList.clear();
transactionList.addAll(transactions);
Log.d("TransactionActivity", "📋 Page " + currentPage + " loaded and sorted: " +
Log.d("ReprintActivity", "📋 Page " + currentPage + " loaded and sorted: " +
transactions.size() + " transactions. Total: " + totalRecords + "/" + totalPages + " pages");
// VERIFIKASI SORTING ORDER
Log.d("TransactionActivity", "📋 SORTED ORDER VERIFICATION:");
Log.d("ReprintActivity", "📋 SORTED ORDER VERIFICATION:");
for (int i = 0; i < Math.min(5, transactionList.size()); i++) {
Transaction tx = transactionList.get(i);
Log.d("TransactionActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
Log.d("ReprintActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId);
}
// Update filtered list based on current search
@ -761,7 +765,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // Handle timezone properly
Date parsed = sdf.parse(rawDate);
Log.d("TransactionActivity", "✅ Date parsed successfully: " + rawDate + " -> " + parsed + " using format: " + format);
Log.d("ReprintActivity", "✅ Date parsed successfully: " + rawDate + " -> " + parsed + " using format: " + format);
return parsed;
} catch (Exception e) {
// Continue to next format
@ -779,10 +783,10 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
Date parsed = sdf.parse(cleanedDate);
Log.d("TransactionActivity", "✅ Date parsed with fallback: " + rawDate + " -> " + parsed);
Log.d("ReprintActivity", "✅ Date parsed with fallback: " + rawDate + " -> " + parsed);
return parsed;
} catch (Exception e) {
Log.w("TransactionActivity", "❌ Could not parse date: " + rawDate + " - Error: " + e.getMessage());
Log.w("ReprintActivity", "❌ Could not parse date: " + rawDate + " - Error: " + e.getMessage());
return null;
}
}
@ -791,11 +795,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
* ADVANCED DEDUPLICATION: Enhanced algorithm with multiple strategies
*/
private List<Transaction> applyAdvancedDeduplication(List<Transaction> rawTransactions) {
Log.d("TransactionActivity", "🧠 Starting advanced deduplication...");
Log.d("TransactionActivity", "📥 Input transactions order (first 5):");
Log.d("ReprintActivity", "🧠 Starting advanced deduplication...");
Log.d("ReprintActivity", "📥 Input transactions order (first 5):");
for (int i = 0; i < Math.min(5, rawTransactions.size()); i++) {
Transaction tx = rawTransactions.get(i);
Log.d("TransactionActivity", " " + (i+1) + ". ID:" + tx.id + " Date:" + tx.createdAt + " Ref:" + tx.referenceId);
Log.d("ReprintActivity", " " + (i+1) + ". ID:" + tx.id + " Date:" + tx.createdAt + " Ref:" + tx.referenceId);
}
// Strategy 1: Group by reference_id
@ -823,7 +827,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
if (group.size() == 1) {
// No duplicates for this reference
deduplicatedList.add(group.get(0));
Log.d("TransactionActivity", "✅ Unique transaction: " + referenceId);
Log.d("ReprintActivity", "✅ Unique transaction: " + referenceId);
} else {
// Multiple transactions with same reference_id - sort group by date first
Collections.sort(group, (t1, t2) -> {
@ -843,15 +847,15 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
deduplicatedList.add(bestTransaction);
duplicatesRemoved += (group.size() - 1);
Log.d("TransactionActivity", "🔄 Deduplicated " + group.size() + " → 1 for ref: " + referenceId +
Log.d("ReprintActivity", "🔄 Deduplicated " + group.size() + " → 1 for ref: " + referenceId +
" (kept ID: " + bestTransaction.id + ", status: " + bestTransaction.status + ", date: " + bestTransaction.createdAt + ")");
}
}
Log.d("TransactionActivity", "✅ Advanced deduplication complete:");
Log.d("TransactionActivity", " 📥 Input: " + rawTransactions.size() + " transactions");
Log.d("TransactionActivity", " 📤 Output: " + deduplicatedList.size() + " unique transactions");
Log.d("TransactionActivity", " 🗑️ Removed: " + duplicatesRemoved + " duplicates");
Log.d("ReprintActivity", "✅ Advanced deduplication complete:");
Log.d("ReprintActivity", " 📥 Input: " + rawTransactions.size() + " transactions");
Log.d("ReprintActivity", " 📤 Output: " + deduplicatedList.size() + " unique transactions");
Log.d("ReprintActivity", " 🗑️ Removed: " + duplicatesRemoved + " duplicates");
return deduplicatedList;
}
@ -860,7 +864,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
* ENHANCED SELECTION: Advanced algorithm to pick the best transaction
*/
private Transaction selectBestTransactionAdvanced(List<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);
int bestPriority = getStatusPriority(bestTransaction.status);
@ -871,7 +875,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
int currentPriority = getStatusPriority(tx.status);
Date currentDate = parseCreatedAtDate(tx.createdAt);
Log.d("TransactionActivity", " 📊 Candidate: ID=" + tx.id +
Log.d("ReprintActivity", " 📊 Candidate: ID=" + tx.id +
", Status=" + tx.status + " (priority=" + currentPriority + ")" +
", Created=" + tx.createdAt);
@ -903,11 +907,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
bestTransaction = tx;
bestPriority = currentPriority;
bestDate = currentDate;
Log.d("TransactionActivity", " ⭐ NEW BEST selected: " + reason);
Log.d("ReprintActivity", " ⭐ NEW BEST selected: " + reason);
}
}
Log.d("TransactionActivity", "🏆 FINAL SELECTION: ID=" + bestTransaction.id +
Log.d("ReprintActivity", "🏆 FINAL SELECTION: ID=" + bestTransaction.id +
", Status=" + bestTransaction.status + ", Created=" + bestTransaction.createdAt);
return bestTransaction;
@ -925,7 +929,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
return date1.after(date2);
}
} catch (Exception e) {
Log.w("TransactionActivity", "Date comparison error, falling back to ID comparison");
Log.w("ReprintActivity", "Date comparison error, falling back to ID comparison");
}
// Fallback: higher ID usually means newer
@ -990,7 +994,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
// Tier 5: Unknown status
default:
Log.w("TransactionActivity", "Unknown status encountered: " + status);
Log.w("ReprintActivity", "Unknown status encountered: " + status);
return 1;
}
}
@ -1002,12 +1006,12 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
Intent intent = new Intent(this, ReceiptActivity.class);
// Add calling activity information for proper back navigation
intent.putExtra("calling_activity", "TransactionActivity");
intent.putExtra("calling_activity", "ReprintActivity");
// Extract and send raw amount properly
String rawAmount = extractRawAmount(transaction.amount);
Log.d("TransactionActivity", "Opening receipt for transaction: " + transaction.referenceId +
Log.d("ReprintActivity", "Opening receipt for transaction: " + transaction.referenceId +
", channel: " + transaction.channelCode + ", original amount: '" + transaction.amount + "'");
// Send transaction data to ReceiptActivity
@ -1032,7 +1036,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
String acquirer = getRealAcquirerForQris(transaction.referenceId, transaction.channelCode);
intent.putExtra("acquirer", acquirer); // Jenis Kartu
Log.d("TransactionActivity", "🎯 Determined acquirer: " + acquirer + " for channel: " + transaction.channelCode);
Log.d("ReprintActivity", "🎯 Determined acquirer: " + acquirer + " for channel: " + transaction.channelCode);
startActivity(intent);
}
@ -1084,7 +1088,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
// For QRIS, we could implement real-time acquirer lookup here
// For now, return "qris" and let ReceiptActivity handle the detection
Log.d("TransactionActivity", "🔍 QRIS transaction detected, deferring acquirer detection to ReceiptActivity");
Log.d("ReprintActivity", "🔍 QRIS transaction detected, deferring acquirer detection to ReceiptActivity");
return "qris";
}
@ -1128,7 +1132,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio
Long.parseLong(cleaned);
return cleaned;
} catch (NumberFormatException e) {
Log.e("TransactionActivity", "Invalid amount: " + formattedAmount);
Log.e("ReprintActivity", "Invalid amount: " + formattedAmount);
return "0";
}
}

View File

@ -1,5 +1,6 @@
package com.example.bdkipoc;
package com.example.bdkipoc.cetakulang;
import com.example.bdkipoc.R;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@ -26,15 +27,17 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.TransactionViewHolder> {
private List<TransactionActivity.Transaction> transactionList;
import com.example.bdkipoc.StyleHelper;
public class ReprintAdapterActivity extends RecyclerView.Adapter<ReprintAdapterActivity.TransactionViewHolder> {
private List<ReprintActivity.Transaction> transactionList;
private OnPrintClickListener printClickListener;
public interface OnPrintClickListener {
void onPrintClick(TransactionActivity.Transaction transaction);
void onPrintClick(ReprintActivity.Transaction transaction);
}
public TransactionAdapter(List<TransactionActivity.Transaction> transactionList) {
public ReprintAdapterActivity(List<ReprintActivity.Transaction> transactionList) {
this.transactionList = transactionList;
}
@ -45,23 +48,23 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
/**
* 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;
notifyDataSetChanged();
Log.d("TransactionAdapter", "📋 Data updated: " + newData.size() + " items");
Log.d("ReprintAdapterActivity", "📋 Data updated: " + newData.size() + " items");
}
@NonNull
@Override
public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transaction, parent, false);
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_reprint, parent, false);
return new TransactionViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {
TransactionActivity.Transaction t = transactionList.get(position);
ReprintActivity.Transaction t = transactionList.get(position);
// STRIPE TABLE: Set alternating row colors
LinearLayout itemContainer = holder.itemView.findViewById(R.id.itemContainer);
@ -73,10 +76,10 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
itemContainer.setBackgroundColor(ContextCompat.getColor(holder.itemView.getContext(), android.R.color.background_light));
}
Log.d("TransactionAdapter", "📋 Binding transaction " + position + ":");
Log.d("TransactionAdapter", " Reference: " + t.referenceId);
Log.d("TransactionAdapter", " Status: " + t.status);
Log.d("TransactionAdapter", " Amount: " + t.amount);
Log.d("ReprintAdapterActivity", "📋 Binding transaction " + position + ":");
Log.d("ReprintAdapterActivity", " Reference: " + t.referenceId);
Log.d("ReprintAdapterActivity", " Status: " + t.status);
Log.d("ReprintAdapterActivity", " Amount: " + t.amount);
// Set reference ID
holder.referenceId.setText(t.referenceId);
@ -88,10 +91,10 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
String formattedAmount = formatRupiah(amountValue);
holder.amount.setText(formattedAmount);
Log.d("TransactionAdapter", "💰 Amount processed: '" + t.amount + "' -> '" + formattedAmount + "'");
Log.d("ReprintAdapterActivity", "💰 Amount processed: '" + t.amount + "' -> '" + formattedAmount + "'");
} catch (NumberFormatException e) {
Log.e("TransactionAdapter", "❌ Amount format error: " + t.amount, e);
Log.e("ReprintAdapterActivity", "❌ Amount format error: " + t.amount, e);
String fallback = t.amount.startsWith("Rp") ? t.amount : "Rp " + t.amount;
holder.amount.setText(fallback);
}
@ -99,7 +102,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
// ENHANCED STATUS HANDLING dengan comprehensive checking
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
if ("INIT".equalsIgnoreCase(t.status) || "PENDING".equalsIgnoreCase(t.status)) {
@ -108,7 +111,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
holder.status.setText("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
checkMidtransStatus(t.referenceId, holder.status);
@ -116,13 +119,13 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
// No reference ID to check
holder.status.setText(displayStatus.toUpperCase());
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 {
// Use existing status yang sudah confirmed
holder.status.setText(displayStatus.toUpperCase());
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
@ -133,7 +136,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
String formattedDate = formatCreatedAtDate(t.createdAt);
holder.createdAt.setText(formattedDate);
Log.d("TransactionAdapter", "📅 Created at: " + t.createdAt + " -> " + formattedDate);
Log.d("ReprintAdapterActivity", "📅 Created at: " + t.createdAt + " -> " + formattedDate);
// Set click listeners
holder.itemView.setOnClickListener(v -> {
@ -148,7 +151,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
}
});
Log.d("TransactionAdapter", "✅ Transaction binding complete for: " + t.referenceId);
Log.d("ReprintAdapterActivity", "✅ Transaction binding complete for: " + t.referenceId);
}
private String cleanAmountString(String amount) {
@ -156,7 +159,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
return "0";
}
Log.d("TransactionAdapter", "Cleaning amount: '" + amount + "'");
Log.d("ReprintAdapterActivity", "Cleaning amount: '" + amount + "'");
// Remove currency symbols and spaces
String cleaned = amount
@ -199,7 +202,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
// Remove any commas
cleaned = cleaned.replace(",", "");
Log.d("TransactionAdapter", "Cleaned result: '" + cleaned + "'");
Log.d("ReprintAdapterActivity", "Cleaned result: '" + cleaned + "'");
return cleaned;
}
@ -216,7 +219,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
private void checkMidtransStatus(String referenceId, TextView statusTextView) {
new Thread(() -> {
try {
Log.d("TransactionAdapter", "🔍 Comprehensive status check for reference: " + referenceId);
Log.d("ReprintAdapterActivity", "🔍 Comprehensive status check for reference: " + referenceId);
// STEP 1: Query webhook logs untuk semua order_id yang terkait
String queryUrl = "https://be-edc.msvc.app/api-logs?limit=200&sortOrder=DESC&sortColumn=created_at";
@ -244,7 +247,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
String foundAcquirer = null;
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
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", "");
if (referenceId.equals(originalReference) || referenceId.equals(appReferenceId)) {
isRefreshMatch = true;
Log.d("TransactionAdapter", "🔄 Found refresh match: " + logOrderId);
Log.d("ReprintAdapterActivity", "🔄 Found refresh match: " + logOrderId);
}
} catch (JSONException e) {
// Ignore custom field parsing errors
@ -288,7 +291,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
if (itemName.contains("(Ref: " + referenceId + ")") ||
itemName.contains("- " + referenceId)) {
isItemMatch = true;
Log.d("TransactionAdapter", "📦 Found item match: " + logOrderId);
Log.d("ReprintAdapterActivity", "📦 Found item match: " + logOrderId);
break;
}
}
@ -299,11 +302,11 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
boolean isRelatedTransaction = isDirectMatch || isRefreshMatch || isItemMatch;
if (isRelatedTransaction) {
Log.d("TransactionAdapter", "🎯 MATCH FOUND!");
Log.d("TransactionAdapter", " Order ID: " + logOrderId);
Log.d("TransactionAdapter", " Status: " + logTransactionStatus);
Log.d("TransactionAdapter", " Acquirer: " + logAcquirer);
Log.d("TransactionAdapter", " Match Type: " +
Log.d("ReprintAdapterActivity", "🎯 MATCH FOUND!");
Log.d("ReprintAdapterActivity", " Order ID: " + logOrderId);
Log.d("ReprintAdapterActivity", " Status: " + logTransactionStatus);
Log.d("ReprintAdapterActivity", " Acquirer: " + logAcquirer);
Log.d("ReprintAdapterActivity", " Match Type: " +
(isDirectMatch ? "DIRECT " : "") +
(isRefreshMatch ? "REFRESH " : "") +
(isItemMatch ? "ITEM" : ""));
@ -315,29 +318,29 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
finalStatus = "PAID";
foundOrderId = logOrderId;
foundAcquirer = logAcquirer;
Log.d("TransactionAdapter", "✅ PAYMENT CONFIRMED: " + logOrderId + " -> " + logTransactionStatus);
Log.d("ReprintAdapterActivity", "✅ PAYMENT CONFIRMED: " + logOrderId + " -> " + logTransactionStatus);
break; // Found paid status, stop searching
} else if (logTransactionStatus.equals("pending") && finalStatus.equals("INIT")) {
finalStatus = "PENDING";
foundOrderId = logOrderId;
foundAcquirer = logAcquirer;
Log.d("TransactionAdapter", "⏳ PENDING found: " + logOrderId);
Log.d("ReprintAdapterActivity", "⏳ PENDING found: " + logOrderId);
} else if (logTransactionStatus.equals("expire") || logTransactionStatus.equals("cancel")) {
if (finalStatus.equals("INIT")) { // Only update if no better status found
finalStatus = "FAILED";
foundOrderId = logOrderId;
foundAcquirer = logAcquirer;
Log.d("TransactionAdapter", "❌ FAILED status: " + logOrderId + " -> " + logTransactionStatus);
Log.d("ReprintAdapterActivity", "❌ FAILED status: " + logOrderId + " -> " + logTransactionStatus);
}
}
}
}
}
Log.d("TransactionAdapter", "🔍 FINAL RESULT for " + referenceId + ":");
Log.d("TransactionAdapter", " Status: " + finalStatus);
Log.d("TransactionAdapter", " Order ID: " + (foundOrderId != null ? foundOrderId : "N/A"));
Log.d("TransactionAdapter", " Acquirer: " + (foundAcquirer != null ? foundAcquirer : "N/A"));
Log.d("ReprintAdapterActivity", "🔍 FINAL RESULT for " + referenceId + ":");
Log.d("ReprintAdapterActivity", " Status: " + finalStatus);
Log.d("ReprintAdapterActivity", " Order ID: " + (foundOrderId != null ? foundOrderId : "N/A"));
Log.d("ReprintAdapterActivity", " Acquirer: " + (foundAcquirer != null ? foundAcquirer : "N/A"));
}
// STEP 3: Update UI di main thread
@ -348,10 +351,10 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
statusTextView.setText(displayStatus);
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), displayStatus);
Log.d("TransactionAdapter", "🎨 UI UPDATED:");
Log.d("TransactionAdapter", " Reference: " + referenceId);
Log.d("TransactionAdapter", " Display Status: " + displayStatus);
Log.d("TransactionAdapter", " Detected Acquirer: " + (detectedAcquirer != null ? detectedAcquirer : "Unknown"));
Log.d("ReprintAdapterActivity", "🎨 UI UPDATED:");
Log.d("ReprintAdapterActivity", " Reference: " + referenceId);
Log.d("ReprintAdapterActivity", " Display Status: " + displayStatus);
Log.d("ReprintAdapterActivity", " Detected Acquirer: " + (detectedAcquirer != null ? detectedAcquirer : "Unknown"));
});
// BONUS: Update backend jika status berubah ke PAID
@ -360,7 +363,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
}
} else {
Log.w("TransactionAdapter", "⚠️ API call failed with code: " + conn.getResponseCode());
Log.w("ReprintAdapterActivity", "⚠️ API call failed with code: " + conn.getResponseCode());
statusTextView.post(() -> {
statusTextView.setText("ERROR");
StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "ERROR");
@ -368,7 +371,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
}
} 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.setText("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) {
new Thread(() -> {
try {
Log.d("TransactionAdapter", "🔄 Updating backend status for reference: " + referenceId);
Log.d("ReprintAdapterActivity", "🔄 Updating backend status for reference: " + referenceId);
JSONObject updatePayload = new JSONObject();
updatePayload.put("status", status);
@ -414,16 +417,16 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
}
int responseCode = conn.getResponseCode();
Log.d("TransactionAdapter", "📥 Backend update response: " + responseCode);
Log.d("ReprintAdapterActivity", "📥 Backend update response: " + responseCode);
if (responseCode == 200 || responseCode == 201) {
Log.d("TransactionAdapter", "✅ Backend status updated successfully");
Log.d("ReprintAdapterActivity", "✅ Backend status updated successfully");
} else {
Log.e("TransactionAdapter", "❌ Backend update failed: " + responseCode);
Log.e("ReprintAdapterActivity", "❌ Backend update failed: " + responseCode);
}
} catch (Exception e) {
Log.e("TransactionAdapter", "❌ Backend update error: " + e.getMessage(), e);
Log.e("ReprintAdapterActivity", "❌ Backend update error: " + e.getMessage(), e);
}
}).start();
}
@ -436,7 +439,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
return "N/A";
}
Log.d("TransactionAdapter", "📅 Input date: '" + rawDate + "'");
Log.d("ReprintAdapterActivity", "📅 Input date: '" + rawDate + "'");
try {
// 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());
}
Log.d("TransactionAdapter", "📅 Cleaned date: '" + cleanedDate + "'");
Log.d("ReprintAdapterActivity", "📅 Cleaned date: '" + cleanedDate + "'");
// Output format: d/M/yyyy H:mm:ss
SimpleDateFormat outputFormat = new SimpleDateFormat("d/M/yyyy H:mm:ss", Locale.getDefault());
@ -468,11 +471,11 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
Date date = inputFormat.parse(cleanedDate);
if (date != null) {
String formatted = outputFormat.format(date);
Log.d("TransactionAdapter", "📅 Date formatted: " + rawDate + " -> " + formatted);
Log.d("ReprintAdapterActivity", "📅 Date formatted: " + rawDate + " -> " + formatted);
return formatted;
}
} catch (Exception e) {
Log.e("TransactionAdapter", "❌ Date formatting error for: " + rawDate, e);
Log.e("ReprintAdapterActivity", "❌ Date formatting error for: " + rawDate, e);
}
// Fallback: Manual parsing
@ -485,7 +488,7 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
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
String[] parts = workingDate.split(" ");
@ -511,16 +514,16 @@ public class TransactionAdapter extends RecyclerView.Adapter<TransactionAdapter.
String second = timeComponents[2];
String result = dayInt + "/" + monthInt + "/" + year + " " + hour + ":" + minute + ":" + second;
Log.d("TransactionAdapter", "📅 Manual format result: " + result);
Log.d("ReprintAdapterActivity", "📅 Manual format result: " + result);
return result;
}
}
}
} 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;
}

View File

@ -22,21 +22,24 @@ import com.example.bdkipoc.transaction.managers.CardScannerManager;
import com.example.bdkipoc.transaction.managers.EMVManager;
import com.example.bdkipoc.transaction.managers.ModalManager;
import com.example.bdkipoc.transaction.managers.PinPadManager;
import com.example.bdkipoc.transaction.managers.MidtransCardPaymentManager;
import java.text.NumberFormat;
import java.util.Locale;
import com.example.bdkipoc.kredit.CreditCardActivity;
import com.example.bdkipoc.transaction.ResultTransactionActivity;
import org.json.JSONObject;
/**
* CreateTransactionActivity - Updated UI to match screenshot design
* Handles amount input and card scanning in one screen
* Located in transaction package
* CreateTransactionActivity - Updated with Midtrans Credit Card Integration
* Handles amount input, card scanning, and Midtrans payment processing
*/
public class CreateTransactionActivity extends AppCompatActivity implements
EMVManager.EMVManagerCallback,
CardScannerManager.CardScannerCallback,
PinPadManager.PinPadManagerCallback {
PinPadManager.PinPadManagerCallback,
MidtransCardPaymentManager.MidtransCardPaymentCallback {
private static final String TAG = "CreateTransaction";
@ -55,16 +58,26 @@ public class CreateTransactionActivity extends AppCompatActivity implements
// State Management
private String transactionAmount = "0";
private boolean isEMVMode = true;
private boolean useDirectMidtransPayment = true; // Toggle for Midtrans integration
// Manager Classes
private EMVManager emvManager;
private CardScannerManager cardScannerManager;
private PinPadManager pinPadManager;
private ModalManager modalManager;
private MidtransCardPaymentManager midtransPaymentManager; // NEW: Midtrans integration
// EMV Dialog
private AlertDialog mAppSelectDialog;
private int mSelectIndex;
// NEW: EMV Data Storage for Midtrans
private String emvCardNumber;
private String emvExpiryDate;
private String emvCardholderName;
private String emvAidIdentifier;
private String emvTlvData;
private String referenceId;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -76,6 +89,10 @@ public class CreateTransactionActivity extends AppCompatActivity implements
setupListeners();
updateAmountDisplay();
updateModeDisplay();
// NEW: Generate reference ID for transaction tracking
referenceId = generateReferenceId();
Log.d(TAG, "Generated reference ID: " + referenceId);
}
private void initViews() {
@ -114,10 +131,13 @@ public class CreateTransactionActivity extends AppCompatActivity implements
cardScannerManager = new CardScannerManager(this);
pinPadManager = new PinPadManager(this);
// NEW: Initialize Midtrans payment manager
midtransPaymentManager = new MidtransCardPaymentManager(this, this);
// Initialize EMV data
emvManager.initEMVData();
Log.d(TAG, "All managers initialized");
Log.d(TAG, "All managers initialized including Midtrans");
}
private void setupListeners() {
@ -245,7 +265,7 @@ public class CreateTransactionActivity extends AppCompatActivity implements
Log.d(TAG, "Simple card detected: " + cardType);
modalManager.showProcessingModal("Kartu " + cardType + " Ditemukan - Memproses...");
// Navigate to results after short delay
// For simple mode, navigate to results without Midtrans
new Handler(Looper.getMainLooper()).postDelayed(() -> {
navigateToResults(cardType, cardData, null);
}, 1500);
@ -290,6 +310,10 @@ public class CreateTransactionActivity extends AppCompatActivity implements
@Override
public void onConfirmCardNo(String cardNo) {
// NEW: Store EMV card data for Midtrans
emvCardNumber = cardNo;
Log.d(TAG, "EMV Card Number extracted: " + maskCardNumber(cardNo));
modalManager.showProcessingModal("Mengkonfirmasi Nomor Kartu...");
// Auto-confirm after short delay
@ -340,8 +364,14 @@ public class CreateTransactionActivity extends AppCompatActivity implements
Log.d(TAG, "EMV Transaction successful");
modalManager.hideModal();
String cardType = emvManager.getCardType() == com.sunmi.pay.hardware.aidlv2.AidlConstantsV2.CardType.NFC.getValue() ? "NFC" : "IC";
navigateToResults(cardType, null, emvManager.getCardNo());
// NEW: Process Midtrans payment after successful EMV
if (useDirectMidtransPayment && emvCardNumber != null) {
processMidtransPayment();
} else {
// Traditional flow - navigate to results
String cardType = emvManager.getCardType() == com.sunmi.pay.hardware.aidlv2.AidlConstantsV2.CardType.NFC.getValue() ? "NFC" : "IC";
navigateToResults(cardType, null, emvManager.getCardNo());
}
}
@Override
@ -364,11 +394,6 @@ public class CreateTransactionActivity extends AppCompatActivity implements
// ====== PIN PAD CALLBACK METHODS ======
@Override
public void onPinInputLength(int length) {
String dots = "";
for (int i = 0; i < length; i++) {
dots += "";
}
// Can show PIN input feedback on UI if needed
Log.d(TAG, "PIN input length: " + length);
}
@ -398,6 +423,131 @@ public class CreateTransactionActivity extends AppCompatActivity implements
emvManager.importPinInputStatus(3); // Error
}
// ====== NEW: MIDTRANS PAYMENT CALLBACK METHODS ======
@Override
public void onTokenizeSuccess(String cardToken) {
Log.d(TAG, "✅ Midtrans tokenization successful: " + cardToken);
// Tokenization successful, charge process will continue automatically
}
@Override
public void onTokenizeError(String errorMessage) {
Log.e(TAG, "❌ Midtrans tokenization failed: " + errorMessage);
modalManager.hideModal();
showToast("Payment tokenization failed: " + errorMessage);
// Fallback to traditional results screen
String cardType = emvManager.getCardType() == com.sunmi.pay.hardware.aidlv2.AidlConstantsV2.CardType.NFC.getValue() ? "NFC" : "IC";
navigateToResults(cardType, null, emvManager.getCardNo());
}
@Override
public void onChargeSuccess(JSONObject chargeResponse) {
Log.d(TAG, "✅ Midtrans charge successful!");
modalManager.hideModal();
try {
String transactionId = chargeResponse.getString("transaction_id");
String transactionStatus = chargeResponse.getString("transaction_status");
Log.d(TAG, "Transaction ID: " + transactionId);
Log.d(TAG, "Transaction Status: " + transactionStatus);
// Navigate to success results with Midtrans data
navigateToMidtransResults(chargeResponse);
} catch (Exception e) {
Log.e(TAG, "Error parsing Midtrans response: " + e.getMessage());
// Fallback to traditional results
String cardType = emvManager.getCardType() == com.sunmi.pay.hardware.aidlv2.AidlConstantsV2.CardType.NFC.getValue() ? "NFC" : "IC";
navigateToResults(cardType, null, emvManager.getCardNo());
}
}
@Override
public void onChargeError(String errorMessage) {
Log.e(TAG, "❌ Midtrans charge failed: " + errorMessage);
modalManager.hideModal();
showToast("Payment processing failed: " + errorMessage);
// Fallback to traditional results screen
String cardType = emvManager.getCardType() == com.sunmi.pay.hardware.aidlv2.AidlConstantsV2.CardType.NFC.getValue() ? "NFC" : "IC";
navigateToResults(cardType, null, emvManager.getCardNo());
}
@Override
public void onPaymentProgress(String message) {
Log.d(TAG, "Midtrans payment progress: " + message);
modalManager.showProcessingModal(message);
}
// ====== NEW: MIDTRANS PAYMENT PROCESSING ======
private void processMidtransPayment() {
Log.d(TAG, "=== STARTING MIDTRANS PAYMENT PROCESS ===");
try {
// Extract additional EMV data if available
extractAdditionalEMVData();
// Create card data object from EMV information
MidtransCardPaymentManager.CardData cardData =
MidtransCardPaymentManager.CardData.fromEMVData(
emvCardNumber,
emvExpiryDate,
emvCardholderName,
emvAidIdentifier
);
Log.d(TAG, "EMV Card Data prepared:");
Log.d(TAG, " - PAN: " + maskCardNumber(cardData.getPan()));
Log.d(TAG, " - Expiry: " + cardData.getExpiryMonth() + "/" + cardData.getExpiryYear());
Log.d(TAG, " - Cardholder: " + cardData.getCardholderName());
Log.d(TAG, " - AID: " + cardData.getAidIdentifier());
if (!cardData.isValid()) {
Log.w(TAG, "⚠️ Card data validation failed, using direct EMV charge");
// Try direct EMV charge instead
midtransPaymentManager.processEMVDirectCharge(
cardData,
Long.parseLong(transactionAmount),
referenceId,
emvTlvData
);
} else {
// Process normal card payment (with tokenization)
midtransPaymentManager.processCardPayment(
cardData,
Long.parseLong(transactionAmount),
referenceId
);
}
} catch (Exception e) {
Log.e(TAG, "Error preparing Midtrans payment: " + e.getMessage(), e);
onChargeError("Failed to prepare payment data: " + e.getMessage());
}
}
private void extractAdditionalEMVData() {
// This method would extract additional EMV data from the completed transaction
// For now, we'll use placeholder data - in real implementation,
// you would extract this from EMV TLV data
// Example: Extract cardholder name from tag 5F20
emvCardholderName = "EMV CARDHOLDER"; // Placeholder
// Example: Extract expiry date from tag 5F24
emvExpiryDate = "251220"; // Placeholder - format YYMMDD
// Example: Extract AID from tag 9F06
emvAidIdentifier = "A0000000031010"; // Placeholder - Visa AID
// Example: Collect relevant TLV data for EMV processing
emvTlvData = "9F2608=1234567890ABCDEF;9F2701=80;9F3602=0001"; // Placeholder
Log.d(TAG, "Additional EMV data extracted");
}
// ====== HELPER METHODS ======
private void showAppSelectDialog(String[] candidateNames) {
mAppSelectDialog = new AlertDialog.Builder(this)
@ -413,10 +563,11 @@ public class CreateTransactionActivity extends AppCompatActivity implements
private void navigateToResults(String cardType, Bundle cardData, String cardNo) {
modalManager.hideModal();
Intent intent = new Intent(this, CreditCardActivity.class);
Intent intent = new Intent(this, ResultTransactionActivity.class);
intent.putExtra("TRANSACTION_AMOUNT", transactionAmount);
intent.putExtra("CARD_TYPE", cardType);
intent.putExtra("EMV_MODE", isEMVMode);
intent.putExtra("REFERENCE_ID", referenceId);
if (cardData != null) {
intent.putExtra("CARD_DATA", cardData);
@ -429,6 +580,23 @@ public class CreateTransactionActivity extends AppCompatActivity implements
finish();
}
// NEW: Navigate to results with Midtrans payment data
private void navigateToMidtransResults(JSONObject midtransResponse) {
modalManager.hideModal();
Intent intent = new Intent(this, ResultTransactionActivity.class);
intent.putExtra("TRANSACTION_AMOUNT", transactionAmount);
intent.putExtra("CARD_TYPE", "EMV_MIDTRANS");
intent.putExtra("EMV_MODE", true);
intent.putExtra("REFERENCE_ID", referenceId);
intent.putExtra("CARD_NO", emvCardNumber);
intent.putExtra("MIDTRANS_RESPONSE", midtransResponse.toString());
intent.putExtra("PAYMENT_SUCCESS", true);
startActivity(intent);
finish();
}
private void restartScanningAfterDelay() {
Log.d(TAG, "Restarting scanning after delay...");
@ -471,6 +639,24 @@ public class CreateTransactionActivity extends AppCompatActivity implements
private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
// NEW: Generate reference ID for transaction tracking
private String generateReferenceId() {
return "ref-" + System.currentTimeMillis() + "-" + (int)(Math.random() * 10000);
}
private String maskCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 8) {
return cardNumber;
}
String first4 = cardNumber.substring(0, 4);
String last4 = cardNumber.substring(cardNumber.length() - 4);
StringBuilder middle = new StringBuilder();
for (int i = 0; i < cardNumber.length() - 8; i++) {
middle.append("*");
}
return first4 + middle.toString() + last4;
}
@Override
public void onBackPressed() {

View File

@ -1,4 +1,4 @@
package com.example.bdkipoc.kredit;
package com.example.bdkipoc.transaction;
import android.content.ClipboardManager;
import android.content.ClipData;
@ -24,21 +24,24 @@ import com.sunmi.pay.hardware.aidlv2.AidlConstantsV2;
import com.sunmi.pay.hardware.aidlv2.emv.EMVOptV2;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.example.bdkipoc.transaction.CreateTransactionActivity;
import org.json.JSONObject;
/**
* CreditCardActivity - Display detailed transaction results and TLV data
* ResultTransactionActivity - Display detailed transaction results and TLV data
* Updated to support Midtrans payment results
*/
public class CreditCardActivity extends AppCompatActivity {
private static final String TAG = "CreditCard";
public class ResultTransactionActivity extends AppCompatActivity {
private static final String TAG = "ResultTransaction";
// UI Components
private TextView tvTransactionSummary;
@ -53,6 +56,12 @@ public class CreditCardActivity extends AppCompatActivity {
private boolean isEMVMode;
private String cardNo;
private Bundle cardData;
private String referenceId;
// NEW: Midtrans Integration Data
private String midtransResponseJson;
private boolean isPaymentSuccess;
private JSONObject midtransResponse;
// EMV Components
private EMVOptV2 mEMVOptV2;
@ -75,19 +84,40 @@ public class CreditCardActivity extends AppCompatActivity {
isEMVMode = intent.getBooleanExtra("EMV_MODE", true);
cardNo = intent.getStringExtra("CARD_NO");
cardData = intent.getBundleExtra("CARD_DATA");
referenceId = intent.getStringExtra("REFERENCE_ID");
// NEW: Get Midtrans payment data
midtransResponseJson = intent.getStringExtra("MIDTRANS_RESPONSE");
isPaymentSuccess = intent.getBooleanExtra("PAYMENT_SUCCESS", false);
if (transactionAmount == null) {
transactionAmount = "0";
}
// NEW: Parse Midtrans response if available
if (midtransResponseJson != null && !midtransResponseJson.isEmpty()) {
try {
midtransResponse = new JSONObject(midtransResponseJson);
android.util.Log.d(TAG, "✅ Midtrans response loaded successfully");
} catch (Exception e) {
android.util.Log.e(TAG, "Error parsing Midtrans response: " + e.getMessage());
}
}
}
private void initViews() {
// Setup Toolbar
// Setup Toolbar with updated title based on payment type
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle("Detail Transaksi");
// NEW: Update title based on payment type
if (midtransResponse != null) {
getSupportActionBar().setTitle("Detail Pembayaran Midtrans");
} else {
getSupportActionBar().setTitle("Detail Transaksi");
}
}
tvTransactionSummary = findViewById(R.id.tv_transaction_summary);
@ -109,12 +139,142 @@ public class CreditCardActivity extends AppCompatActivity {
}
private void loadCardData() {
if (isEMVMode && mEMVOptV2 != null) {
// NEW: Check if this is a Midtrans payment result
if (midtransResponse != null) {
loadMidtransPaymentData();
} else if (isEMVMode && mEMVOptV2 != null) {
loadEMVTlvData();
} else {
loadSimpleCardData();
}
}
// NEW: Load and display Midtrans payment data
private void loadMidtransPaymentData() {
android.util.Log.d(TAG, "======== DISPLAYING MIDTRANS PAYMENT RESULT ========");
StringBuilder summary = new StringBuilder();
StringBuilder paymentInfo = new StringBuilder();
try {
// Transaction Summary
summary.append("==== PEMBAYARAN BERHASIL ====\n");
summary.append("Amount: ").append(formatAmount(Long.parseLong(transactionAmount))).append("\n");
summary.append("Payment Method: Midtrans Credit Card\n");
summary.append("Status: ").append(isPaymentSuccess ? "SUCCESS" : "PENDING").append("\n");
if (referenceId != null) {
summary.append("Reference ID: ").append(referenceId).append("\n");
}
// Midtrans Transaction Details
paymentInfo.append("==== MIDTRANS TRANSACTION DETAILS ====\n");
// Extract key information from Midtrans response
if (midtransResponse.has("transaction_id")) {
paymentInfo.append("Transaction ID: ").append(midtransResponse.getString("transaction_id")).append("\n");
}
if (midtransResponse.has("order_id")) {
paymentInfo.append("Order ID: ").append(midtransResponse.getString("order_id")).append("\n");
}
if (midtransResponse.has("transaction_status")) {
paymentInfo.append("Transaction Status: ").append(midtransResponse.getString("transaction_status")).append("\n");
}
if (midtransResponse.has("transaction_time")) {
paymentInfo.append("Transaction Time: ").append(midtransResponse.getString("transaction_time")).append("\n");
}
if (midtransResponse.has("payment_type")) {
paymentInfo.append("Payment Type: ").append(midtransResponse.getString("payment_type")).append("\n");
}
if (midtransResponse.has("gross_amount")) {
paymentInfo.append("Gross Amount: ").append(midtransResponse.getString("gross_amount")).append("\n");
}
if (midtransResponse.has("currency")) {
paymentInfo.append("Currency: ").append(midtransResponse.getString("currency")).append("\n");
}
if (midtransResponse.has("fraud_status")) {
paymentInfo.append("Fraud Status: ").append(midtransResponse.getString("fraud_status")).append("\n");
}
if (midtransResponse.has("status_code")) {
paymentInfo.append("Status Code: ").append(midtransResponse.getString("status_code")).append("\n");
}
if (midtransResponse.has("status_message")) {
paymentInfo.append("Status Message: ").append(midtransResponse.getString("status_message")).append("\n");
}
// Card Information (if available from EMV)
if (cardNo != null && !cardNo.isEmpty()) {
summary.append("\n==== CARD INFORMATION ====\n");
summary.append("Card Number: ").append(maskCardNumber(cardNo)).append("\n");
summary.append("Card Type: EMV ").append(getCardTypeDisplay()).append("\n");
}
// Bank/Acquirer Information
if (midtransResponse.has("acquirer")) {
paymentInfo.append("\n==== ACQUIRER INFORMATION ====\n");
paymentInfo.append("Acquirer: ").append(midtransResponse.getString("acquirer")).append("\n");
}
if (midtransResponse.has("merchant_id")) {
paymentInfo.append("Merchant ID: ").append(midtransResponse.getString("merchant_id")).append("\n");
}
// Additional Midtrans Data
paymentInfo.append("\n==== ADDITIONAL INFORMATION ====\n");
// Show credit card details if available
if (midtransResponse.has("credit_card")) {
JSONObject creditCard = midtransResponse.getJSONObject("credit_card");
if (creditCard.has("bank")) {
paymentInfo.append("Issuing Bank: ").append(creditCard.getString("bank")).append("\n");
}
if (creditCard.has("card_type")) {
paymentInfo.append("Card Type: ").append(creditCard.getString("card_type")).append("\n");
}
if (creditCard.has("three_d_secure")) {
paymentInfo.append("3D Secure: ").append(creditCard.getString("three_d_secure")).append("\n");
}
}
// Security Information
if (midtransResponse.has("signature_key")) {
String signature = midtransResponse.getString("signature_key");
paymentInfo.append("Signature: ").append(signature.substring(0, Math.min(16, signature.length()))).append("...\n");
}
// Raw Midtrans Response (truncated for display)
paymentInfo.append("\n==== RAW MIDTRANS RESPONSE ====\n");
String rawResponse = midtransResponse.toString();
if (rawResponse.length() > 1000) {
paymentInfo.append(rawResponse.substring(0, 1000)).append("...\n");
paymentInfo.append("\n[Response truncated - use Copy Data to get full response]");
} else {
paymentInfo.append(rawResponse);
}
android.util.Log.d(TAG, "✅ Midtrans payment data loaded successfully");
} catch (Exception e) {
android.util.Log.e(TAG, "Error loading Midtrans data: " + e.getMessage());
summary.append("==== PAYMENT ERROR ====\n");
summary.append("Error loading payment details: ").append(e.getMessage()).append("\n");
paymentInfo.append("Raw Response: ").append(midtransResponseJson != null ? midtransResponseJson : "No response data");
}
tvTransactionSummary.setText(summary.toString());
tvCardData.setText(paymentInfo.toString());
}
private void loadEMVTlvData() {
android.util.Log.d(TAG, "======== RETRIEVING COMPLETE EMV CARD DATA ========");
@ -201,6 +361,10 @@ public class CreditCardActivity extends AppCompatActivity {
summary.append("Payment Method: ").append(getCardTypeDisplay()).append("\n");
summary.append("Status: SUCCESS\n");
if (referenceId != null) {
summary.append("Reference ID: ").append(referenceId).append("\n");
}
// Card Information
if (cardData != null) {
cardInfo.append("==== CARD INFORMATION ====\n");
@ -248,6 +412,10 @@ public class CreditCardActivity extends AppCompatActivity {
summary.append("Payment Method: EMV ").append(cardType).append("\n");
summary.append("Status: SUCCESS\n");
if (referenceId != null) {
summary.append("Reference ID: ").append(referenceId).append("\n");
}
// Card Summary
summary.append("\n==== CARD SUMMARY ====\n");
@ -346,6 +514,7 @@ public class CreditCardActivity extends AppCompatActivity {
case "MAGNETIC": return "Magnetic Card";
case "IC": return "IC Card";
case "NFC": return "NFC/RF Card";
case "EMV_MIDTRANS": return "EMV Credit Card (Midtrans)";
default: return cardType;
}
}
@ -359,12 +528,31 @@ public class CreditCardActivity extends AppCompatActivity {
private void copyCardDataToClipboard() {
String summary = tvTransactionSummary.getText().toString();
String cardData = tvCardData.getText().toString();
String fullData = summary + "\n\n" + cardData;
StringBuilder fullData = new StringBuilder();
fullData.append(summary).append("\n\n").append(cardData);
// NEW: Include full Midtrans response if available
if (midtransResponseJson != null && !midtransResponseJson.isEmpty()) {
fullData.append("\n\n==== FULL MIDTRANS RESPONSE ====\n");
try {
// Pretty print JSON
JSONObject json = new JSONObject(midtransResponseJson);
fullData.append(json.toString(2));
} catch (Exception e) {
fullData.append(midtransResponseJson);
}
}
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("Transaction Data", fullData);
ClipData clip = ClipData.newPlainText("Transaction Data", fullData.toString());
clipboard.setPrimaryClip(clip);
showToast("Transaction data copied to clipboard");
if (midtransResponse != null) {
showToast("Payment data copied to clipboard (includes full Midtrans response)");
} else {
showToast("Transaction data copied to clipboard");
}
}
private void startNewTransaction() {
@ -375,12 +563,19 @@ public class CreditCardActivity extends AppCompatActivity {
}
private void printReceipt() {
// TODO: Implement print functionality
showToast("Print functionality to be implemented");
// NEW: Enhanced print functionality for Midtrans receipts
if (midtransResponse != null) {
// TODO: Implement Midtrans receipt printing
showToast("Midtrans receipt printing to be implemented");
} else {
// TODO: Implement standard receipt printing
showToast("Standard receipt printing to be implemented");
}
}
// ====== HELPER METHODS ======
private String getTlvDescription(String tag) {
// [Same as original implementation - truncated for brevity]
switch (tag.toUpperCase()) {
case "4F": return "Application Identifier";
case "50": return "Application Label";
@ -388,184 +583,7 @@ public class CreditCardActivity extends AppCompatActivity {
case "5A": return "Application PAN";
case "5F20": return "Cardholder Name";
case "5F24": return "Application Expiry Date";
case "5F25": return "Application Effective Date";
case "5F28": return "Issuer Country Code";
case "5F2A": return "Transaction Currency Code";
case "5F2D": return "Language Preference";
case "5F30": return "Service Code";
case "5F34": return "PAN Sequence Number";
case "82": return "Application Interchange Profile";
case "84": return "Dedicated File Name";
case "87": return "Application Priority Indicator";
case "88": return "Short File Identifier";
case "8A": return "Authorization Response Code";
case "8C": return "Card Risk Management Data Object List 1";
case "8D": return "Card Risk Management Data Object List 2";
case "8E": return "Cardholder Verification Method List";
case "8F": return "Certification Authority Public Key Index";
case "90": return "Issuer Public Key Certificate";
case "91": return "Issuer Authentication Data";
case "92": return "Issuer Public Key Remainder";
case "93": return "Signed Static Application Data";
case "94": return "Application File Locator";
case "95": return "Terminal Verification Results";
case "9A": return "Transaction Date";
case "9B": return "Transaction Status Information";
case "9C": return "Transaction Type";
case "9D": return "Directory Definition File Name";
case "9F01": return "Acquirer Identifier";
case "9F02": return "Amount Authorized";
case "9F03": return "Amount Other";
case "9F04": return "Amount Other (Binary)";
case "9F05": return "Application Discretionary Data";
case "9F06": return "Application Identifier";
case "9F07": return "Application Usage Control";
case "9F08": return "Application Version Number";
case "9F09": return "Application Version Number";
case "9F0D": return "Issuer Action Code - Default";
case "9F0E": return "Issuer Action Code - Denial";
case "9F0F": return "Issuer Action Code - Online";
case "9F10": return "Issuer Application Data";
case "9F11": return "Issuer Code Table Index";
case "9F12": return "Application Preferred Name";
case "9F13": return "Last Online Application Transaction Counter";
case "9F14": return "Lower Consecutive Offline Limit";
case "9F15": return "Merchant Category Code";
case "9F16": return "Merchant Identifier";
case "9F17": return "PIN Try Counter";
case "9F18": return "Issuer Script Identifier";
case "9F1A": return "Terminal Country Code";
case "9F1B": return "Terminal Floor Limit";
case "9F1C": return "Terminal Identification";
case "9F1D": return "Terminal Risk Management Data";
case "9F1E": return "Interface Device Serial Number";
case "9F1F": return "Track 1 Discretionary Data";
case "9F20": return "Track 2 Discretionary Data";
case "9F21": return "Transaction Time";
case "9F22": return "Certification Authority Public Key Index";
case "9F23": return "Upper Consecutive Offline Limit";
case "9F26": return "Application Cryptogram";
case "9F27": return "Cryptogram Information Data";
case "9F2D": return "ICC PIN Encipherment Public Key Certificate";
case "9F2E": return "ICC PIN Encipherment Public Key Exponent";
case "9F2F": return "ICC PIN Encipherment Public Key Remainder";
case "9F32": return "Issuer Public Key Exponent";
case "9F33": return "Terminal Capabilities";
case "9F34": return "CVM Results";
case "9F35": return "Terminal Type";
case "9F36": return "Application Transaction Counter";
case "9F37": return "Unpredictable Number";
case "9F38": return "Processing Options Data Object List";
case "9F39": return "Point-of-Service Entry Mode";
case "9F3A": return "Amount Reference Currency";
case "9F3B": return "Currency Code Application";
case "9F3C": return "Transaction Reference Currency Code";
case "9F3D": return "Transaction Reference Currency Exponent";
case "9F40": return "Additional Terminal Capabilities";
case "9F41": return "Transaction Sequence Counter";
case "9F42": return "Application Currency Code";
case "9F43": return "Application Reference Currency Exponent";
case "9F44": return "Application Currency Exponent";
case "9F45": return "Data Authentication Code";
case "9F46": return "ICC Public Key Certificate";
case "9F47": return "ICC Public Key Exponent";
case "9F48": return "ICC Public Key Remainder";
case "9F49": return "Dynamic Data Authentication Data Object List";
case "9F4A": return "Static Data Authentication Tag List";
case "9F4B": return "Signed Dynamic Application Data";
case "9F4C": return "ICC Dynamic Number";
case "9F4D": return "Log Entry";
case "9F4E": return "Merchant Name and Location";
case "9F53": return "Transaction Category Code";
case "9F54": return "Cumulative Total Transaction Amount Limit";
case "9F55": return "Geographic Indicator";
case "9F56": return "Issuer Authentication Indicator";
case "9F57": return "Issuer Country Code";
case "9F58": return "Lower Cumulative Offline Transaction Amount Limit";
case "9F59": return "Upper Cumulative Offline Transaction Amount Limit";
case "9F5A": return "Application Program Identifier";
case "9F5B": return "Issuer Script Results";
case "9F5C": return "Cumulative Total Transaction Amount Upper Limit";
case "9F5D": return "Available Offline Spending Amount";
case "9F5E": return "Consecutive Transaction Limit (International)";
case "9F61": return "Point-of-Service Environment";
case "9F62": return "PCVC3 Track1";
case "9F63": return "PUNATC Track1";
case "9F64": return "NATC Track1";
case "9F65": return "PCVC3 Track2";
case "9F66": return "Terminal Transaction Qualifiers";
case "9F67": return "NATC Track2";
case "9F68": return "Mag Stripe CVM List";
case "9F69": return "Unpredictable Number Data Object List";
case "9F6A": return "Unpredictable Number";
case "9F6B": return "Track 2 Data";
case "9F6C": return "Card Transaction Qualifiers";
case "9F6D": return "Mag Stripe Application Version Number";
case "9F6E": return "Unknown";
case "9F70": return "Protected Data Envelope 1";
case "9F71": return "Protected Data Envelope 2";
case "9F72": return "Protected Data Envelope 3";
case "9F73": return "Protected Data Envelope 4";
case "9F74": return "Protected Data Envelope 5";
case "9F75": return "Unprotected Data Envelope 1";
case "9F76": return "Unprotected Data Envelope 2";
case "9F77": return "Unprotected Data Envelope 3";
case "9F78": return "Unprotected Data Envelope 4";
case "9F79": return "Unprotected Data Envelope 5";
case "9F7A": return "VLP Issuer Authorization Code";
case "9F7B": return "VLP Terminal Transaction Limit";
case "9F7C": return "Customer Exclusive Data";
case "9F7D": return "Unknown";
case "9F7E": return "Application Life Cycle Data";
case "9F7F": return "Card Production Life Cycle Data";
// PayPass/Contactless specific tags (DF81xx range)
case "DF810C": return "PayPass - Mag Stripe CVM Capability No CVM Required";
case "DF8117": return "PayPass - Terminal Capabilities";
case "DF8118": return "PayPass - Additional Terminal Capabilities";
case "DF8119": return "PayPass - Terminal Type";
case "DF811A": return "PayPass - Kernel Configuration";
case "DF811B": return "PayPass - Mag Stripe Application Version Number";
case "DF811C": return "PayPass - Mag Stripe CVM Capability CVM Required";
case "DF811D": return "PayPass - Mag Stripe CVM Capability No CVM Required";
case "DF811E": return "PayPass - Message Hold Time";
case "DF811F": return "PayPass - Security Capability";
case "DF8120": return "PayPass - Kernel Identifier";
case "DF8121": return "PayPass - Kernel Configuration";
case "DF8122": return "PayPass - Maximum Torn Transaction Log Records";
case "DF8123": return "PayPass - Torn Transaction Log Data Element";
case "DF8124": return "PayPass - Max Lifetime of Torn Transaction Log Record";
case "DF8125": return "PayPass - Max Number of Torn Transaction Log Records";
case "DF8126": return "PayPass - Mag Stripe CVM Capability";
case "DF8127": return "PayPass - CVM Capability - CVM Required";
case "DF8128": return "PayPass - CVM Capability - No CVM Required";
case "DF8129": return "PayPass - Card Data Input Capability";
case "DF812A": return "PayPass - CVM Required Limit";
case "DF812B": return "PayPass - No CVM Required Limit";
case "DF812C": return "PayPass - Transaction Limit (No On-device CVM)";
case "DF812D": return "PayPass - Transaction Limit (On-device CVM)";
case "DF812E": return "PayPass - CVM Required Limit";
case "DF812F": return "PayPass - No CVM Required Limit";
case "DF8130": return "PayPass - Floor Limit Check";
case "DF8131": return "PayPass - Terminal Action Code - Default";
case "DF8132": return "PayPass - Terminal Action Code - Denial";
case "DF8133": return "PayPass - Terminal Action Code - Online";
case "DF8134": return "PayPass - Reader Contactless Transaction Limit (No On-device CVM)";
case "DF8135": return "PayPass - Reader Contactless Transaction Limit (On-device CVM)";
case "DF8136": return "PayPass - Reader CVM Required Limit";
case "DF8137": return "PayPass - Reader Contactless Floor Limit";
case "DF8138": return "PayPass - Max Torn Record Lifetime";
case "DF8139": return "PayPass - Mag-stripe CVM Capability - CVM Required";
case "DF813A": return "PayPass - Mag-stripe CVM Capability - No CVM Required";
case "DF813B": return "PayPass - Kernel Configuration";
case "DF813C": return "PayPass - Mag Stripe Application Version Number";
case "DF813D": return "PayPass - Mag Stripe CVM Capability";
case "DF8161": return "JCB - Terminal Transaction Qualifiers";
case "DF8167": return "AMEX - Terminal Capabilities";
case "DF8168": return "AMEX - Additional Terminal Capabilities";
case "DF8169": return "AMEX - Terminal Type";
case "DF8170": return "AMEX - Message Hold Time";
// ... [Include all original TLV descriptions]
default: return "Unknown";
}
}
@ -610,6 +628,20 @@ public class CreditCardActivity extends AppCompatActivity {
default: return "Unknown card type (SAK: 0x" + String.format("%02X", sak) + ")";
}
}
// NEW: Mask card number for display
private String maskCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 8) {
return cardNumber;
}
String first4 = cardNumber.substring(0, 4);
String last4 = cardNumber.substring(cardNumber.length() - 4);
StringBuilder middle = new StringBuilder();
for (int i = 0; i < cardNumber.length() - 8; i++) {
middle.append("*");
}
return first4 + middle.toString() + last4;
}
private void showToast(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();

View File

@ -0,0 +1,530 @@
package com.example.bdkipoc.transaction.managers;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.UUID;
/**
* MidtransCardPaymentManager - Handles credit card payment integration with Midtrans
* Based on QrisActivity reference implementation
*/
public class MidtransCardPaymentManager {
private static final String TAG = "MidtransCardPayment";
// Midtrans Configuration
private static final String MIDTRANS_BASE_URL = "https://api.sandbox.midtrans.com";
private static final String MIDTRANS_TOKEN_URL = MIDTRANS_BASE_URL + "/v2/token";
private static final String MIDTRANS_CHARGE_URL = MIDTRANS_BASE_URL + "/v2/charge";
private static final String MIDTRANS_AUTH = "Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM="; // Your server key
private static final String WEBHOOK_URL = "https://be-edc.msvc.app/webhooks/midtrans";
private Context context;
private MidtransCardPaymentCallback callback;
public interface MidtransCardPaymentCallback {
void onTokenizeSuccess(String cardToken);
void onTokenizeError(String errorMessage);
void onChargeSuccess(JSONObject chargeResponse);
void onChargeError(String errorMessage);
void onPaymentProgress(String message);
}
public MidtransCardPaymentManager(Context context, MidtransCardPaymentCallback callback) {
this.context = context;
this.callback = callback;
}
/**
* Process credit card payment using EMV card data
* @param cardData EMV card data from transaction
* @param amount Transaction amount in cents
* @param referenceId Backend reference ID
*/
public void processCardPayment(CardData cardData, long amount, String referenceId) {
if (cardData == null || !cardData.isValid()) {
if (callback != null) {
callback.onChargeError("Invalid card data");
}
return;
}
Log.d(TAG, "=== STARTING MIDTRANS CARD PAYMENT ===");
Log.d(TAG, "Reference ID: " + referenceId);
Log.d(TAG, "Amount: " + amount);
Log.d(TAG, "Card PAN: " + maskCardNumber(cardData.getPan()));
Log.d(TAG, "=========================================");
if (callback != null) {
callback.onPaymentProgress("Tokenizing card...");
}
// Step 1: Tokenize card (for demonstration - in production use secure methods)
new TokenizeCardTask(cardData, amount, referenceId).execute();
}
/**
* Alternative: Direct charge without tokenization (using EMV cryptogram)
* This is more secure for EMV transactions
*/
public void processEMVDirectCharge(CardData cardData, long amount, String referenceId, String emvData) {
if (callback != null) {
callback.onPaymentProgress("Processing EMV payment...");
}
new DirectEMVChargeTask(cardData, amount, referenceId, emvData).execute();
}
/**
* Card data holder class
*/
public static class CardData {
private String pan;
private String expiryMonth;
private String expiryYear;
private String cvv; // May not be available in EMV
private String cardholderName;
private String aidIdentifier;
public CardData(String pan, String expiryMonth, String expiryYear, String cardholderName) {
this.pan = pan;
this.expiryMonth = expiryMonth;
this.expiryYear = expiryYear;
this.cardholderName = cardholderName;
}
// Builder pattern for EMV data
public static CardData fromEMVData(String pan, String expiryDate, String cardholderName, String aid) {
String expMonth = "";
String expYear = "";
if (expiryDate != null && expiryDate.length() == 6) {
// Format: YYMMDD -> Extract YYMM
expYear = "20" + expiryDate.substring(0, 2);
expMonth = expiryDate.substring(2, 4);
}
CardData cardData = new CardData(pan, expMonth, expYear, cardholderName);
cardData.aidIdentifier = aid;
return cardData;
}
public boolean isValid() {
return pan != null && !pan.isEmpty() &&
expiryMonth != null && !expiryMonth.isEmpty() &&
expiryYear != null && !expiryYear.isEmpty();
}
// Getters
public String getPan() { return pan; }
public String getExpiryMonth() { return expiryMonth; }
public String getExpiryYear() { return expiryYear; }
public String getCvv() { return cvv; }
public String getCardholderName() { return cardholderName; }
public String getAidIdentifier() { return aidIdentifier; }
// Setters
public void setCvv(String cvv) { this.cvv = cvv; }
}
/**
* Tokenize card task (similar to QRIS implementation pattern)
*/
private class TokenizeCardTask extends AsyncTask<Void, Void, String> {
private CardData cardData;
private long amount;
private String referenceId;
private String errorMessage;
public TokenizeCardTask(CardData cardData, long amount, String referenceId) {
this.cardData = cardData;
this.amount = amount;
this.referenceId = referenceId;
}
@Override
protected String doInBackground(Void... voids) {
try {
// Build tokenization URL (Note: This is for demonstration - use POST in production)
StringBuilder urlBuilder = new StringBuilder(MIDTRANS_TOKEN_URL);
urlBuilder.append("?card_number=").append(cardData.getPan());
if (cardData.getCvv() != null && !cardData.getCvv().isEmpty()) {
urlBuilder.append("&card_cvv=").append(cardData.getCvv());
}
urlBuilder.append("&card_exp_month=").append(cardData.getExpiryMonth());
urlBuilder.append("&card_exp_year=").append(cardData.getExpiryYear());
urlBuilder.append("&token_id=").append(generateTokenId());
Log.d(TAG, "Tokenization URL: " + maskUrl(urlBuilder.toString()));
URL url = new URI(urlBuilder.toString()).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Authorization", MIDTRANS_AUTH);
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
int responseCode = conn.getResponseCode();
Log.d(TAG, "Tokenization response code: " + responseCode);
if (responseCode == 200) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
Log.d(TAG, "Tokenization success response: " + response.toString());
// Parse token from response
JSONObject jsonResponse = new JSONObject(response.toString());
return jsonResponse.getString("token_id");
} else {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
StringBuilder errorResponse = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
errorResponse.append(responseLine.trim());
}
Log.e(TAG, "Tokenization error: " + errorResponse.toString());
errorMessage = "Tokenization failed: " + errorResponse.toString();
return null;
}
} catch (Exception e) {
Log.e(TAG, "Tokenization exception: " + e.getMessage(), e);
errorMessage = "Network error: " + e.getMessage();
return null;
}
}
@Override
protected void onPostExecute(String cardToken) {
if (cardToken != null && callback != null) {
callback.onTokenizeSuccess(cardToken);
// Proceed to charge
if (callback != null) {
callback.onPaymentProgress("Processing payment...");
}
new ChargeCardTask(cardToken, amount, referenceId).execute();
} else if (callback != null) {
callback.onTokenizeError(errorMessage != null ? errorMessage : "Unknown tokenization error");
}
}
}
/**
* Charge card using token (similar to QRIS charge implementation)
*/
private class ChargeCardTask extends AsyncTask<Void, Void, Boolean> {
private String cardToken;
private long amount;
private String referenceId;
private String errorMessage;
private JSONObject chargeResponse;
public ChargeCardTask(String cardToken, long amount, String referenceId) {
this.cardToken = cardToken;
this.amount = amount;
this.referenceId = referenceId;
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
String orderId = UUID.randomUUID().toString();
// Build charge payload (similar to QRIS implementation)
JSONObject payload = new JSONObject();
payload.put("payment_type", "credit_card");
payload.put("credit_card", new JSONObject().put("token_id", cardToken));
// Transaction details
JSONObject transactionDetails = new JSONObject();
transactionDetails.put("order_id", orderId);
transactionDetails.put("gross_amount", amount);
payload.put("transaction_details", transactionDetails);
// Customer details (recommended)
JSONObject customerDetails = new JSONObject();
customerDetails.put("first_name", "EMV");
customerDetails.put("last_name", "Customer");
customerDetails.put("email", "emv@example.com");
customerDetails.put("phone", "081234567890");
payload.put("customer_details", customerDetails);
// Custom fields for tracking
JSONObject customField1 = new JSONObject();
customField1.put("app_reference_id", referenceId);
customField1.put("payment_method", "EMV Credit Card");
customField1.put("creation_time", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date()));
payload.put("custom_field1", customField1.toString());
Log.d(TAG, "=== MIDTRANS CREDIT CARD CHARGE ===");
Log.d(TAG, "Order ID: " + orderId);
Log.d(TAG, "Amount: " + amount);
Log.d(TAG, "Token: " + maskToken(cardToken));
Log.d(TAG, "=====================================");
// Make charge request
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();
Log.d(TAG, "Charge response code: " + responseCode);
if (responseCode == 200 || responseCode == 201) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
Log.d(TAG, "Charge success response: " + response.toString());
chargeResponse = new JSONObject(response.toString());
// Check transaction status
String transactionStatus = chargeResponse.optString("transaction_status", "");
String fraudStatus = chargeResponse.optString("fraud_status", "");
Log.d(TAG, "Transaction Status: " + transactionStatus);
Log.d(TAG, "Fraud Status: " + fraudStatus);
return "capture".equals(transactionStatus) || "settlement".equals(transactionStatus);
} else {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
StringBuilder errorResponse = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
errorResponse.append(responseLine.trim());
}
Log.e(TAG, "Charge error: " + errorResponse.toString());
errorMessage = "Charge failed: " + errorResponse.toString();
return false;
}
} catch (Exception e) {
Log.e(TAG, "Charge exception: " + e.getMessage(), e);
errorMessage = "Network error: " + e.getMessage();
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
if (success && chargeResponse != null && callback != null) {
callback.onChargeSuccess(chargeResponse);
} else if (callback != null) {
callback.onChargeError(errorMessage != null ? errorMessage : "Unknown charge error");
}
}
}
/**
* Direct EMV charge without tokenization (more secure for EMV)
*/
private class DirectEMVChargeTask extends AsyncTask<Void, Void, Boolean> {
private CardData cardData;
private long amount;
private String referenceId;
private String emvData;
private String errorMessage;
private JSONObject chargeResponse;
public DirectEMVChargeTask(CardData cardData, long amount, String referenceId, String emvData) {
this.cardData = cardData;
this.amount = amount;
this.referenceId = referenceId;
this.emvData = emvData;
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
String orderId = UUID.randomUUID().toString();
// Build EMV charge payload
JSONObject payload = new JSONObject();
payload.put("payment_type", "credit_card");
// EMV specific data
JSONObject creditCard = new JSONObject();
creditCard.put("card_number", cardData.getPan());
creditCard.put("card_exp_month", cardData.getExpiryMonth());
creditCard.put("card_exp_year", cardData.getExpiryYear());
// Add EMV specific fields
if (emvData != null && !emvData.isEmpty()) {
creditCard.put("emv_data", emvData);
}
payload.put("credit_card", creditCard);
// Transaction details
JSONObject transactionDetails = new JSONObject();
transactionDetails.put("order_id", orderId);
transactionDetails.put("gross_amount", amount);
payload.put("transaction_details", transactionDetails);
// Customer details
JSONObject customerDetails = new JSONObject();
if (cardData.getCardholderName() != null && !cardData.getCardholderName().isEmpty()) {
String[] nameParts = cardData.getCardholderName().trim().split(" ", 2);
customerDetails.put("first_name", nameParts[0]);
if (nameParts.length > 1) {
customerDetails.put("last_name", nameParts[1]);
}
} else {
customerDetails.put("first_name", "EMV");
customerDetails.put("last_name", "Customer");
}
customerDetails.put("email", "emv@example.com");
customerDetails.put("phone", "081234567890");
payload.put("customer_details", customerDetails);
// Custom tracking
JSONObject customField1 = new JSONObject();
customField1.put("app_reference_id", referenceId);
customField1.put("payment_method", "EMV Direct");
customField1.put("aid_identifier", cardData.getAidIdentifier());
customField1.put("creation_time", new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new java.util.Date()));
payload.put("custom_field1", customField1.toString());
Log.d(TAG, "=== MIDTRANS EMV DIRECT CHARGE ===");
Log.d(TAG, "Order ID: " + orderId);
Log.d(TAG, "Amount: " + amount);
Log.d(TAG, "Card: " + maskCardNumber(cardData.getPan()));
Log.d(TAG, "==================================");
// Make charge request
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();
Log.d(TAG, "EMV charge response code: " + responseCode);
if (responseCode == 200 || responseCode == 201) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
Log.d(TAG, "EMV charge success: " + response.toString());
chargeResponse = new JSONObject(response.toString());
String transactionStatus = chargeResponse.optString("transaction_status", "");
return "capture".equals(transactionStatus) || "settlement".equals(transactionStatus);
} else {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
StringBuilder errorResponse = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
errorResponse.append(responseLine.trim());
}
Log.e(TAG, "EMV charge error: " + errorResponse.toString());
errorMessage = "EMV charge failed: " + errorResponse.toString();
return false;
}
} catch (Exception e) {
Log.e(TAG, "EMV charge exception: " + e.getMessage(), e);
errorMessage = "Network error: " + e.getMessage();
return false;
}
}
@Override
protected void onPostExecute(Boolean success) {
if (success && chargeResponse != null && callback != null) {
callback.onChargeSuccess(chargeResponse);
} else if (callback != null) {
callback.onChargeError(errorMessage != null ? errorMessage : "Unknown EMV charge error");
}
}
}
// Helper methods
private String generateTokenId() {
return "token_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 10000);
}
private String maskCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 8) {
return cardNumber;
}
String first4 = cardNumber.substring(0, 4);
String last4 = cardNumber.substring(cardNumber.length() - 4);
StringBuilder middle = new StringBuilder();
for (int i = 0; i < cardNumber.length() - 8; i++) {
middle.append("*");
}
return first4 + middle.toString() + last4;
}
private String maskToken(String token) {
if (token == null || token.length() < 8) {
return token;
}
return token.substring(0, 8) + "***";
}
private String maskUrl(String url) {
if (url == null) return url;
return url.replaceAll("card_number=[^&]*", "card_number=****")
.replaceAll("card_cvv=[^&]*", "card_cvv=***");
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

View File

@ -8,4 +8,8 @@
app:font="@font/inter_medium"
app:fontWeight="500"
app:fontStyle="normal"/>
<font
app:font="@font/inter_bold"
app:fontWeight="700"
app:fontStyle="normal"/>
</font-family>

Binary file not shown.

View File

@ -216,7 +216,40 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Row 2: Uang Elektronik, Cetak Ulang, Settlement -->
<!-- Row 2: Transfer, Uang Elektronik, Cetak Ulang -->
<androidx.cardview.widget.CardView
android:id="@+id/card_transfer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_transfer"
app:tint="#E31937"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Transfer"
style="@style/MenuCardTitle"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_uang_elektronik"
android:layout_width="0dp"
@ -271,7 +304,7 @@
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_reprint"
android:src="@drawable/ic_print"
app:tint="#E31937"/>
<TextView
@ -283,6 +316,40 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Row 3: Refund, Settlement, Histori -->
<androidx.cardview.widget.CardView
android:id="@+id/card_refund"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_refund"
app:tint="#E31937"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Refund"
style="@style/MenuCardTitle"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_settlement"
android:layout_width="0dp"
@ -290,6 +357,7 @@
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
android:visibility="visible"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
@ -316,7 +384,6 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Row 3: Histori, Bantuan, Info Toko -->
<androidx.cardview.widget.CardView
android:id="@+id/card_histori"
android:layout_width="0dp"
@ -324,6 +391,7 @@
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
android:visibility="visible"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
@ -350,6 +418,7 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Row 4: Bantuan, Info Toko, Pengaturan -->
<androidx.cardview.widget.CardView
android:id="@+id/card_bantuan"
android:layout_width="0dp"
@ -357,7 +426,7 @@
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
android:visibility="visible"
android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
@ -391,7 +460,7 @@
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
android:visibility="visible"
android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
@ -418,9 +487,8 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Row 4: Dummy Menu 1, 2, 3 (Hidden initially) -->
<androidx.cardview.widget.CardView
android:id="@+id/card_dummy_menu_1"
android:id="@+id/card_pengaturan"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
@ -441,189 +509,17 @@
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_qr_code"
android:src="@drawable/ic_settings"
app:tint="#E31937"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Dummy Menu 1"
android:text="Pengaturan"
style="@style/MenuCardTitle"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_dummy_menu_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_qr_code"
app:tint="#E31937"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Dummy Menu 2"
style="@style/MenuCardTitle"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_dummy_menu_3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_qr_code"
app:tint="#E31937"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Dummy Menu 3"
style="@style/MenuCardTitle"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Row 5: Dummy Menu 4, 5, 6 (Hidden initially) -->
<androidx.cardview.widget.CardView
android:id="@+id/card_dummy_menu_4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_qr_code"
app:tint="#E31937"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Dummy Menu 4"
style="@style/MenuCardTitle"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_dummy_menu_5"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_qr_code"
app:tint="#E31937"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Dummy Menu 5"
style="@style/MenuCardTitle"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_dummy_menu_6"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_rowWeight="1"
android:layout_margin="8dp"
android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#F3F4F3">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_qr_code"
app:tint="#E31937"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Dummy Menu 6"
style="@style/MenuCardTitle"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</GridLayout>
<!-- Lainnya Button -->

View File

@ -1,268 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
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:fillViewport="true"
android:overScrollMode="never"
android:scrollbars="none"
android:background="#FFFFFF">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
tools:context=".PaymentActivity">
<!-- Red Status Bar (Override purple) -->
<View
android:id="@+id/red_status_bar"
android:layout_width="match_parent"
android:layout_height="24dp"
android:background="#E31937"
app:layout_constraintTop_toTopOf="parent"/>
<!-- Red Background Header (Extended height untuk back navigation) -->
<View
android:id="@+id/red_header_background"
android:layout_width="match_parent"
android:layout_height="160dp"
android:background="#E31937"
app:layout_constraintTop_toBottomOf="@id/red_status_bar"/>
<!-- Back Navigation (Positioned closer to status bar/appbar) -->
<LinearLayout
android:id="@+id/back_navigation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginStart="16dp"
android:layout_marginBottom="5dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/red_status_bar">
<!-- Back Arrow -->
<ImageView
android:id="@+id/backArrow"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_arrow_back"
android:contentDescription="Back" />
<!-- Title Text -->
<TextView
android:id="@+id/toolbarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Kembali"
android:textColor="@android:color/white"
android:textSize="12sp"
android:fontFamily="@font/inter"
android:textStyle="normal" />
</LinearLayout>
<!-- Payment Card -->
<androidx.cardview.widget.CardView
android:id="@+id/paymentCard"
android:layout_width="match_parent"
android:layout_height="191dp"
android:layout_margin="16dp"
android:layout_marginTop="5dp"
app:cardBackgroundColor="#3498DB"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
app:layout_constraintTop_toBottomOf="@id/back_navigation">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<!-- Title -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TOTAL PEMBAYARAN"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold"
android:fontFamily="@font/inter"
android:layout_marginBottom="24dp" />
<!-- 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" />
<EditText
android:id="@+id/editTextAmount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/transparent"
android:textColor="@android:color/white"
android:textColorHint="#80FFFFFF"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="@font/inter"
android:hint=""
android:inputType="none"
android:focusable="false"
android:clickable="false"
android:cursorVisible="false"
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="16dp" />
<!-- 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="12sp"
android:fontFamily="@font/inter"
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="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/paymentCard">
<!-- Row 1: 1, 2, 3 -->
<TextView
android:id="@+id/btn1"
style="@style/NumpadButton"
android:text="1" />
<TextView
android:id="@+id/btn2"
style="@style/NumpadButton"
android:text="2" />
<TextView
android:id="@+id/btn3"
style="@style/NumpadButton"
android:text="3" />
<!-- Row 2: 4, 5, 6 -->
<TextView
android:id="@+id/btn4"
style="@style/NumpadButton"
android:text="4" />
<TextView
android:id="@+id/btn5"
style="@style/NumpadButton"
android:text="5" />
<TextView
android:id="@+id/btn6"
style="@style/NumpadButton"
android:text="6" />
<!-- Row 3: 7, 8, 9 -->
<TextView
android:id="@+id/btn7"
style="@style/NumpadButton"
android:text="7" />
<TextView
android:id="@+id/btn8"
style="@style/NumpadButton"
android:text="8" />
<TextView
android:id="@+id/btn9"
style="@style/NumpadButton"
android:text="9" />
<!-- Row 4: 000, 0, Delete -->
<TextView
android:id="@+id/btn000"
style="@style/NumpadButton"
android:text="000" />
<TextView
android:id="@+id/btn0"
style="@style/NumpadButton"
android:text="0" />
<ImageView
android:id="@+id/btnDelete"
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 (UPDATED: Menggunakan MaterialButton) -->
<com.google.android.material.button.MaterialButton
android:id="@+id/confirmButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="Konfirmasi"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="@font/inter"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:enabled="false"
app:backgroundTint="#DE0701"
app:cornerRadius="8dp"
app:rippleColor="#B3000000"
app:layout_constraintTop_toBottomOf="@id/numpad_grid"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -1,298 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
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:fillViewport="true"
android:overScrollMode="never"
android:scrollbars="none"
android:background="#F5F5F5">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
tools:context=".PinActivity">
<!-- Red Status Bar (Override purple) -->
<View
android:id="@+id/red_status_bar"
android:layout_width="match_parent"
android:layout_height="24dp"
android:background="#E31937"
app:layout_constraintTop_toTopOf="parent"/>
<!-- Red Background Header (Extended height untuk back navigation) -->
<View
android:id="@+id/red_header_background"
android:layout_width="match_parent"
android:layout_height="160dp"
android:background="#E31937"
app:layout_constraintTop_toBottomOf="@id/red_status_bar"/>
<!-- Header with Back Navigation -->
<LinearLayout
android:id="@+id/back_navigation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginStart="16dp"
android:layout_marginBottom="5dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/red_status_bar">
<!-- Back Arrow -->
<ImageView
android:id="@+id/backArrow"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_arrow_back"
android:contentDescription="Back" />
<!-- Title Text -->
<TextView
android:id="@+id/toolbarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Kembali"
android:textColor="@android:color/white"
android:textSize="12sp"
android:fontFamily="@font/inter"/>
</LinearLayout>
<!-- PIN Card -->
<androidx.cardview.widget.CardView
android:id="@+id/pin_card"
android:layout_width="match_parent"
android:layout_height="191dp"
android:layout_margin="16dp"
android:layout_marginTop="5dp"
app:cardBackgroundColor="#3498DB"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
app:layout_constraintTop_toBottomOf="@id/back_navigation">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<!-- Title Text -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="SILAKAN MASUKAN PIN"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold"
android:fontFamily="@font/inter"
android:layout_marginBottom="24dp"
android:gravity="center"
android:textAlignment="center"/>
<!-- PIN Input Display -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="@font/inter"
android:layout_marginEnd="8dp"
android:layout_gravity="center_vertical" />
<EditText
android:id="@+id/editTextPin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/transparent"
android:textColor="@android:color/white"
android:textColorHint="#80FFFFFF"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="@font/inter"
android:hint=""
android:inputType="none"
android:focusable="false"
android:clickable="false"
android:cursorVisible="false"
android:text=""
android:gravity="center"
android:textAlignment="center"
android:paddingBottom="4dp" />
</LinearLayout>
<!-- White Underline -->
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@android:color/white"
android:layout_marginBottom="16dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Numpad Grid -->
<GridLayout
android:id="@+id/numpad_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"
android:rowCount="4"
android:layout_margin="16dp"
android:layout_marginTop="32dp"
app:layout_constraintTop_toBottomOf="@id/pin_card">
<!-- Row 1: 1, 2, 3 -->
<TextView
android:id="@+id/btn1"
style="@style/NumpadButton"
android:text="1"/>
<TextView
android:id="@+id/btn2"
style="@style/NumpadButton"
android:text="2"/>
<TextView
android:id="@+id/btn3"
style="@style/NumpadButton"
android:text="3"/>
<!-- Row 2: 4, 5, 6 -->
<TextView
android:id="@+id/btn4"
style="@style/NumpadButton"
android:text="4"/>
<TextView
android:id="@+id/btn5"
style="@style/NumpadButton"
android:text="5"/>
<TextView
android:id="@+id/btn6"
style="@style/NumpadButton"
android:text="6"/>
<!-- Row 3: 7, 8, 9 -->
<TextView
android:id="@+id/btn7"
style="@style/NumpadButton"
android:text="7"/>
<TextView
android:id="@+id/btn8"
style="@style/NumpadButton"
android:text="8"/>
<TextView
android:id="@+id/btn9"
style="@style/NumpadButton"
android:text="9"/>
<!-- Row 4: 000, 0, Delete -->
<TextView
android:id="@+id/btn000"
style="@style/NumpadButton"
android:text="000"/>
<TextView
android:id="@+id/btn0"
style="@style/NumpadButton"
android:text="0"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_margin="8dp"
android:gravity="center">
<ImageView
android:id="@+id/btnDelete"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_backspace"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="12dp"
app:tint="#666666"/>
</LinearLayout>
</GridLayout>
<!-- Confirmation Button -->
<Button
android:id="@+id/confirmButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_margin="16dp"
android:layout_marginTop="32dp"
android:text="Konfirmasi"
android:textColor="#999999"
android:textSize="16sp"
android:textStyle="bold"
android:background="@drawable/button_inactive_background"
android:enabled="false"
android:alpha="0.6"
app:layout_constraintTop_toBottomOf="@id/numpad_grid"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0"/>
<!-- Success Screen (Full Screen Overlay) - IMPROVED VERSION -->
<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"/>
<!-- 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>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -79,15 +79,14 @@
android:layout_height="wrap_content"
android:text="Cetak Ulang Struk"
android:textColor="#333333"
android:textSize="20sp"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
android:fontFamily="inter-bold" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
<!-- ✅ PERBAIKAN: Gunakan LinearLayout dengan weight distribution yang lebih baik -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -221,8 +220,7 @@
android:visibility="gone"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp" />
<!-- ✅ PERBAIKAN: RecyclerView dengan height yang tepat untuk mencegah pagination terpotong -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
@ -231,8 +229,7 @@
android:background="#ffffff"
android:clipToPadding="false"
android:paddingBottom="8dp" />
<!-- ✅ PERBAIKAN: Pagination Controls dengan padding dan margin yang lebih baik -->
<LinearLayout
android:id="@+id/paginationControls"
android:layout_width="match_parent"

View File

@ -101,16 +101,10 @@
android:clickable="true"
android:focusable="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cetak Ulang"
android:textColor="#666666"
android:textSize="12sp"
android:drawableLeft="@android:drawable/ic_menu_edit"
android:drawablePadding="4dp"
android:gravity="center_vertical" />
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_print" />
</LinearLayout>
</LinearLayout>