RN-universal InputOTP pilot
ScaffoldedOne-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
Value: (empty)
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/.nativeblocker 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
TextInputbehind visual slotViews. Same architecture as theinput-otpnpm 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
| Aspect | shadcn (input-otp) | RN | Notes |
|---|---|---|---|
| Architecture | single hidden input + slots | single hidden TextInput + slots | Same approach |
| Compound API | InputOTP / Group / Slot / Separator | RnInputOTP / Group / Slot / Separator | Drop-in surface match |
| Paste | OS paste -> fills slots | OS paste -> fills slots | Free, single-input architecture |
| iOS SMS autofill | textContentType=oneTimeCode | textContentType=oneTimeCode | Default ON; opt out via prop |
| Android autofill | autoComplete=one-time-code | autoComplete=one-time-code | Default ON; opt out via prop |
| Keyboard | inputMode=numeric (web) | keyboardType=number-pad | Default numeric; pass `default` for alphanumeric |
| Caret | fake animated caret in active slot | thicker accent ring on active slot | No fake caret in v1 |
| Filter | pattern prop | transform fn | Same effect, different signature |
| onComplete | yes | yes | Fires once when length === maxLength |
| Disabled | whole input | whole input | Per-slot disable not supported |