Skip to content

PIN Pad

The PIN entry pad is used for customer authentication in the customer-app. It features a 3x4 grid layout with haptic feedback and visual progress dots.

Masukkan PIN Anda
1
2
3
4
5
6
7
8
9
0
Lupa PIN?

Anatomy

+-------------------------------------------+
|                                            |
|  Masukkan PIN Anda                         |
|                                            |
|     ( )  ( )  ( )  ( )  ( )  ( )          |
|                                            |
|     +-----+  +-----+  +-----+             |
|     |  1  |  |  2  |  |  3  |             |
|     +-----+  +-----+  +-----+             |
|     +-----+  +-----+  +-----+             |
|     |  4  |  |  5  |  |  6  |             |
|     +-----+  +-----+  +-----+             |
|     +-----+  +-----+  +-----+             |
|     |  7  |  |  8  |  |  9  |             |
|     +-----+  +-----+  +-----+             |
|     +-----+  +-----+  +-----+             |
|     | Del |  |  0  |  |  OK |             |
|     +-----+  +-----+  +-----+             |
|                                            |
|  Lupa PIN?                                 |
+-------------------------------------------+

Visual Specifications

ElementStyle
Title18px, font-semibold, text-1, centered
Progress dots12px circles, 16px gap between
Empty dot2px border border-strong, transparent fill
Filled dotFilled #D4910A (amber), scale-up animation on fill
Number buttons64x64px, rounded-2xl, bg-inset, text-1
Number text24px, font-semibold
Button pressbg-hover, scale 0.95, 100ms transition
Delete buttonLucide Delete icon, 24px, text-2
Submit buttonLucide Check icon, 24px, text-brand
"Lupa PIN?" link14px, text-brand, centered below pad

Amber Theme

The PIN pad uses an amber accent instead of the default green to distinguish authentication from primary actions:

TokenValue
Dot filled#D4910A
Error shake#C4463A
Success fill#2D6B4A

States

StateBehavior
EntryDots fill left-to-right as digits are entered
ErrorAll dots turn red, shake animation (translateX +/- 8px, 3 cycles, 300ms), then reset
SuccessAll dots turn green, brief pause (300ms), then navigate
LockedAfter 5 failed attempts, show lockout message with countdown timer

React Component

tsx
interface PinPadProps {
  length?: number
  onComplete: (pin: string) => Promise<boolean>
  onForgotPin?: () => void
}

export function PinPad({ length = 6, onComplete, onForgotPin }: PinPadProps) {
  const [pin, setPin] = useState('')
  const [state, setState] = useState<'idle' | 'error' | 'success'>('idle')
  const [attempts, setAttempts] = useState(0)

  const handleDigit = async (digit: string) => {
    if (state !== 'idle' || pin.length >= length) return

    // Haptic feedback
    if ('vibrate' in navigator) navigator.vibrate(10)

    const next = pin + digit
    setPin(next)

    if (next.length === length) {
      const valid = await onComplete(next)
      if (valid) {
        setState('success')
      } else {
        setState('error')
        setAttempts(a => a + 1)
        setTimeout(() => {
          setState('idle')
          setPin('')
        }, 600)
      }
    }
  }

  const handleDelete = () => {
    if (state !== 'idle') return
    if ('vibrate' in navigator) navigator.vibrate(10)
    setPin(p => p.slice(0, -1))
  }

  const buttons = [
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '9'],
    ['del', '0', 'ok'],
  ]

  return (
    <div className="flex flex-col items-center px-8">
      <h2 className="text-lg font-semibold text-1 mb-6">Masukkan PIN Anda</h2>

      {/* Dots */}
      <div
        className={cn('flex gap-4 mb-8', state === 'error' && 'animate-shake')}
      >
        {Array.from({ length }).map((_, i) => (
          <div
            key={i}
            className={cn(
              'w-3 h-3 rounded-full transition-all duration-150',
              i < pin.length
                ? state === 'error' ? 'bg-danger scale-110'
                : state === 'success' ? 'bg-success scale-110'
                : 'bg-amber-500 scale-110'
                : 'border-2 border-border-strong'
            )}
          />
        ))}
      </div>

      {/* Button Grid */}
      <div className="grid grid-cols-3 gap-3">
        {buttons.flat().map(key => (
          <button
            key={key}
            onClick={() => {
              if (key === 'del') handleDelete()
              else if (key === 'ok') { /* no-op, auto-submit on length */ }
              else handleDigit(key)
            }}
            className={cn(
              'w-16 h-16 rounded-2xl flex items-center justify-center transition-all duration-100',
              'active:scale-95 active:bg-hover',
              key === 'del' || key === 'ok' ? 'bg-transparent' : 'bg-inset',
            )}
          >
            {key === 'del' ? (
              <Delete className="w-6 h-6 text-2" />
            ) : key === 'ok' ? (
              <Check className="w-6 h-6 text-brand" />
            ) : (
              <span className="text-2xl font-semibold text-1">{key}</span>
            )}
          </button>
        ))}
      </div>

      {/* Forgot PIN */}
      {onForgotPin && (
        <button onClick={onForgotPin} className="mt-6 text-sm text-brand">
          Lupa PIN?
        </button>
      )}

      {/* Lockout */}
      {attempts >= 5 && (
        <p className="mt-4 text-sm text-danger text-center">
          Terlalu banyak percobaan. Coba lagi dalam 5 menit.
        </p>
      )}
    </div>
  )
}

Haptic Feedback

ActionVibrationiOS Equivalent
Digit press10ms lightUIImpactFeedbackGenerator(.light)
Delete press10ms lightUIImpactFeedbackGenerator(.light)
PIN complete (success)50ms mediumUINotificationFeedbackGenerator(.success)
PIN error100ms, 50ms pause, 100msUINotificationFeedbackGenerator(.error)

CSS Shake Animation

css
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-8px); }
  40%, 80% { transform: translateX(8px); }
}

.animate-shake {
  animation: shake 0.3s ease-in-out;
}

Accessibility

Provide a "Show PIN" toggle for users who need visual confirmation. When active, show digits in the dots instead of filled circles.

Don't

  • Don't store the PIN in plain text --- always hash before transmitting
  • Don't log PIN entry attempts with the actual digits
  • Don't disable haptic feedback --- it confirms key registration for the user

RetailOS - Sistem ERP Retail Modern untuk Indonesia