Phase 9d · Tier B

RN-universal InputOTP pilot

Scaffolded

One-time-passcode entry. Compound API mirrors shadcn (InputOTP / Group / Slot / Separator). Pure RN — single TextInput captures keystrokes; visual slots overlay positionally. Free OS paste, iOS oneTimeCode autofill, Android autocomplete hint, numeric keyboard.

Side-by-side: 6-digit code

shadcn (input-otp)

Value: (empty)

RN

Value: (empty)

Alphanumeric (transform + keyboardType)

For non-numeric codes, pass keyboardType="default" and a transform to filter input. The example below keeps letters and digits, uppercases everything.

Value: (empty)

Invalid state

Pass invalid on the root to flip slot borders to the danger token. Set aria-invalid on the hidden input automatically.

Library decision

Per Phase 9 Q2, every Tier B primitive must record its library choice. RnInputOTP ships with no new runtime dependency.

  • input-otp (npm package) — the library the shadcn primitive wraps. Web-only (DOM-coupled focus + selection refs). Same .web/.native blocker as cmdk / downshift / sonner.
  • react-native-confirmation-code-field@9 (MIT, ~220 k/wk) — evaluated and rejected. Mature multi-/single-input modes, but the API does not match shadcn's compound surface; a wrapper would be most of the work. Building from scratch is cleaner and keeps the package free of new peer deps.
  • react-native-otp-textinput (MIT, < 30 k/wk) — evaluated and rejected. Smaller surface; same wrapping cost.
  • This implementation — single hidden TextInput behind visual slot Views. Same architecture as the input-otp npm library. Free OS paste, iOS oneTimeCode autofill, Android autocomplete hint, numeric keyboard.

Why otp-field is not deleted in this PR

The 9a inventory tagged otp-field for removal alongside the input-otp port. After surveying the consumer graph, 6 TpEMIS template pages import OtpField: TpEmisLoginPage, TpEmisSignupPinPage, TpEmisSignupBankApiPage, TpEmisSignupBankManualPage, TpEmisSignupOnlinePaymentPage, TpEmisSignupFreePage. Removing the file breaks all six.

This PR therefore defers deletion: it marks otp-field.tsx with a@deprecated JSDoc and updates the 9a inventory row. A follow-up PR will migrate each template to compose RnInputOTP + the existing countdown / resend / channel-switch utilities directly, then delete otp-field.tsx.

Semantics

Aspectshadcn (input-otp)RNNotes
Architecturesingle hidden input + slotssingle hidden TextInput + slotsSame approach
Compound APIInputOTP / Group / Slot / SeparatorRnInputOTP / Group / Slot / SeparatorDrop-in surface match
PasteOS paste -> fills slotsOS paste -> fills slotsFree, single-input architecture
iOS SMS autofilltextContentType=oneTimeCodetextContentType=oneTimeCodeDefault ON; opt out via prop
Android autofillautoComplete=one-time-codeautoComplete=one-time-codeDefault ON; opt out via prop
KeyboardinputMode=numeric (web)keyboardType=number-padDefault numeric; pass `default` for alphanumeric
Caretfake animated caret in active slotthicker accent ring on active slotNo fake caret in v1
Filterpattern proptransform fnSame effect, different signature
onCompleteyesyesFires once when length === maxLength
Disabledwhole inputwhole inputPer-slot disable not supported