Vault 'Change PIN' uses window.prompt() — broken on Capacitor Android/iOS build #189

Open
opened 2026-09-21 13:03:04 +00:00 by Mark · 0 comments
Owner

Symptom

In Settings -> Vault -> Change PIN, clicking the Change PIN button invokes window.prompt("Enter new 4-digit PIN:") to collect the new PIN.

On the Vite dev server (Chromium), this works as a synchronous blocking dialog. On the Capacitor Android/iOS build (which is the production target -- see package.json: @capacitor/android, @capacitor/ios), window.prompt is not implemented in the WebView. The click handler throws immediately, the user can never change their PIN from Settings, and the existing PIN remains the only credential. There is no error path -- the throw is silent.

Repro (Android WebView)

  1. Build the Capacitor app and install on device.
  2. Settings -> Vault -> Change PIN.
  3. Click the button.
  4. Observe: nothing happens. No dialog, no error toast.

Repro (dev -- confirms handler is wired to prompt)

Driving the dev server via CDP and clicking the button will hang waiting for prompt input because the CDP driver has no protocol for window.prompt. Inspecting Settings.tsx:1093 shows the source.

Source pointer

src/pages/Settings.tsx:1088-1107

<Button
  size="sm"
  onClick={() => {
    const newPin = prompt("Enter new 4-digit PIN:");
    if (newPin && /^\d{4}$/.test(newPin)) {
      changeVaultPin(newPin);
    } else if (newPin) {
      toast.error("PIN must be exactly 4 digits");
    }
  }}
  ...
>

Comparison

The Vault enrollment flow on #/vault (src/pages/Vault.tsx) correctly uses real React inputs inside a multi-step wizard (choose factor -> enter PIN -> confirm PIN -> enroll). The Settings "Change PIN" should follow the same pattern: a Radix Dialog with <input type=password inputMode=numeric maxLength=4>.

Severity

Functional. Affects all production builds (Capacitor Android + iOS). Dev server is unaffected.

Suggested fix

Replace prompt(...) with a controlled dialog state and a Radix <Dialog> containing:

  • <input type="password" inputMode="numeric" maxLength={4} value={pin} onChange={...} />
  • Confirm + Cancel buttons
  • Submit calls changeVaultPin(pin) if /^\d{4}$/.test(pin), else toast.error("PIN must be exactly 4 digits")

The changeVaultPin action in src/lib/store.ts:1351 is correctly implemented -- only the UI binding is broken.

QA context

Verified during the 2026-09-20 privacy vault + feature tracking shakedown. All other vault/privacy switches (Intimacy Logging, Vault Enabled, Relational Cadence Tracking, On-Device-Only, Investment Tracking Paused, Auto-lock Delay, Panic Decoy, Shake Threshold) persist correctly across hard navigation. Vault PIN enrollment, unlock, wrong-PIN rejection, lock-now, and panic gesture redirect-to-decoy all work as expected.

## Symptom In `Settings -> Vault -> Change PIN`, clicking the **Change PIN** button invokes `window.prompt("Enter new 4-digit PIN:")` to collect the new PIN. On the Vite dev server (Chromium), this works as a synchronous blocking dialog. On the Capacitor Android/iOS build (which is the production target -- see `package.json: @capacitor/android`, `@capacitor/ios`), `window.prompt` is **not implemented** in the WebView. The click handler throws immediately, the user can never change their PIN from Settings, and the existing PIN remains the only credential. There is no error path -- the throw is silent. ## Repro (Android WebView) 1. Build the Capacitor app and install on device. 2. `Settings -> Vault -> Change PIN`. 3. Click the button. 4. Observe: nothing happens. No dialog, no error toast. ## Repro (dev -- confirms handler is wired to `prompt`) Driving the dev server via CDP and clicking the button will hang waiting for prompt input because the CDP driver has no protocol for `window.prompt`. Inspecting `Settings.tsx:1093` shows the source. ## Source pointer `src/pages/Settings.tsx:1088-1107` ```tsx <Button size="sm" onClick={() => { const newPin = prompt("Enter new 4-digit PIN:"); if (newPin && /^\d{4}$/.test(newPin)) { changeVaultPin(newPin); } else if (newPin) { toast.error("PIN must be exactly 4 digits"); } }} ... > ``` ## Comparison The Vault enrollment flow on `#/vault` (`src/pages/Vault.tsx`) correctly uses real React inputs inside a multi-step wizard (choose factor -> enter PIN -> confirm PIN -> enroll). The Settings "Change PIN" should follow the same pattern: a Radix Dialog with `<input type=password inputMode=numeric maxLength=4>`. ## Severity Functional. Affects all production builds (Capacitor Android + iOS). Dev server is unaffected. ## Suggested fix Replace `prompt(...)` with a controlled dialog state and a Radix `<Dialog>` containing: - `<input type="password" inputMode="numeric" maxLength={4} value={pin} onChange={...} />` - Confirm + Cancel buttons - Submit calls `changeVaultPin(pin)` if `/^\d{4}$/.test(pin)`, else `toast.error("PIN must be exactly 4 digits")` The `changeVaultPin` action in `src/lib/store.ts:1351` is correctly implemented -- only the UI binding is broken. ## QA context Verified during the 2026-09-20 privacy vault + feature tracking shakedown. All other vault/privacy switches (Intimacy Logging, Vault Enabled, Relational Cadence Tracking, On-Device-Only, Investment Tracking Paused, Auto-lock Delay, Panic Decoy, Shake Threshold) persist correctly across hard navigation. Vault PIN enrollment, unlock, wrong-PIN rejection, lock-now, and panic gesture redirect-to-decoy all work as expected.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Mark/kimi_agent_kin_app_sync#189
No description provided.