This commit is contained in:
riz081 2025-05-28 14:33:26 +07:00
parent 810964b4be
commit f1228db89a
4 changed files with 711 additions and 10 deletions

View File

@ -34,6 +34,11 @@
android:name=".PaymentActivity"
android:exported="false" />
<activity android:name=".QrisResultActivity" />
<activity
android:name=".PinActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:exported="false" />
</application>
</manifest>

View File

@ -121,10 +121,14 @@ public class PaymentActivity extends AppCompatActivity {
Handler modalHandler = new Handler(Looper.getMainLooper());
paymentModal.setOnShowListener(dialog -> {
modalHandler.postDelayed(() -> {
if (paymentModal.isShowing()) {
dismissModal();
// Simulate successful card processing
processCardPayment();
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);
});
@ -324,6 +328,9 @@ public class PaymentActivity extends AppCompatActivity {
}
private void showModalWithAnimation() {
// Add debug log
showToast("Showing card modal...");
paymentModal.show();
// Add slide-up animation
@ -371,23 +378,47 @@ public class PaymentActivity extends AppCompatActivity {
// 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 with card payment
showToast("Memproses pembayaran dengan kartu...");
// Show processing message
showToast("PIN berhasil diverifikasi! Memproses pembayaran...");
// Process payment
// 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...");
@ -398,12 +429,12 @@ public class PaymentActivity extends AppCompatActivity {
// Show success message
showToast("Pembayaran berhasil! Jumlah: Rp " + formatCurrency(String.valueOf(amount)));
// Reset state and go back
// Reset state and go back (this is final step after PIN verification)
resetPaymentState();
navigateBack();
}, 2000);
}
private void resetPaymentState() {
currentAmount = new StringBuilder();
updateAmountDisplay();
@ -459,6 +490,20 @@ public class PaymentActivity extends AppCompatActivity {
}
}
@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();

View File

@ -0,0 +1,408 @@
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 backText;
// 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();
}
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);
backText = findViewById(R.id.back_text);
// 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 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();
});
// Back text (also clickable for back navigation)
backText.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)) {
showToast("PIN berhasil diverifikasi!");
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() {
// Return result to 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);
// Navigate back or to next screen based on source activity
if ("PaymentActivity".equals(sourceActivity)) {
// Could navigate to payment success screen
showToast("Pembayaran berhasil diproses!");
}
finish();
}
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() {
navigateBack();
}
@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

@ -0,0 +1,243 @@
<?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="#F5F5F5"
tools:context=".PinActivity">
<!-- Status Bar Area -->
<View
android:id="@+id/status_bar_background"
android:layout_width="match_parent"
android:layout_height="24dp"
android:background="#E31937"
app:layout_constraintTop_toTopOf="parent"/>
<!-- Header with Back Navigation -->
<LinearLayout
android:id="@+id/header_container"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="#E31937"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintTop_toBottomOf="@id/status_bar_background">
<LinearLayout
android:id="@+id/back_navigation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingEnd="8dp"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/backArrow"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_arrow_back"
android:tint="@android:color/white"
android:layout_marginEnd="8dp"/>
<TextView
android:id="@+id/back_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Kembali"
android:textColor="@android:color/white"
android:textSize="16sp"/>
</LinearLayout>
</LinearLayout>
<!-- PIN Card -->
<androidx.cardview.widget.CardView
android:id="@+id/pin_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
app:cardBackgroundColor="#4A90E2"
app:layout_constraintTop_toBottomOf="@id/header_container">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SILAKAN MASUKAN PIN"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="24dp"/>
<!-- PIN Input Display -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginBottom="16dp">
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1"
android:background="@android:color/white"
android:layout_marginEnd="8dp"/>
<EditText
android:id="@+id/editTextPin"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="3"
android:background="@android:color/transparent"
android:textColor="@android:color/white"
android:textColorHint="#CCFFFFFF"
android:textSize="24sp"
android:textStyle="bold"
android:gravity="center"
android:inputType="numberPassword"
android:maxLength="6"
android:focusable="false"
android:clickable="false"
android:cursorVisible="false"
android:letterSpacing="0.5"/>
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_weight="1"
android:background="@android:color/white"
android:layout_marginStart="8dp"/>
</LinearLayout>
</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"
android: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"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>