Skip to main content

pinput 8.0.0-beta.1 copy "pinput: ^8.0.0-beta.1" to clipboard
pinput: ^8.0.0-beta.1 copied to clipboard

Pin code input (OTP) text field, iOS SMS autofill, Android SMS autofill One Time Code, Password, Passcode, Captcha, Security, Coupon, Wowcher, 2FA, Two step verification

Flutter pin code input

Need anything Flutter related? Reach out on LinkedIn

Pub package Github starts style: effective dart pub package

Flutter Pinput is a package that provides an easy-to-use and customizable Pin code input field. It offers several features such as animated decoration switching, app-controlled validation feedback, SMS autofill, custom cursor, and more. It also provides beautiful examples that you can choose from.

Features: #

  • Animated Decoration Switching
  • App-controlled validation feedback (Form integration via FormField)
  • SMS Autofill on iOS
  • SMS Autofill on Android (set the code on the controller, see Android)
  • Standard Cursor
  • Custom Cursor
  • Cursor Animation
  • Ready For Custom Keyboard
  • Standard Paste option
  • Obscuring Character
  • Obscuring Widget
  • Haptic Feedback
  • Close Keyboard After Completion
  • Beautiful Examples

Support #

PRs Welcome

Discord Channel

Examples app on Github has multiple templates to choose from

Don't forget to give it a star ⭐

Demo #

Live Demo Rounded With Shadows Rounded With Cursor
Rounded Filled With Bottom Cursor Filled

Getting Started #

The pin has 6 states default focused, filled, empty, disabled, error, you can customize each state by specifying theme parameter. Pin smoothly animates from one state to another automatically. PinTheme Class

Property Default/Type
width 56.0
height 60.0
textStyle TextStyle()
margin EdgeInsetsGeometry
padding EdgeInsetsGeometry
constraints BoxConstraints

You can use standard Pinput like so

Widget buildPinPut() {
  return Pinput(
    onCompleted: (pin) => print(pin),
  );
}

If you want to customize it, create a base PinTheme first.

final defaultPinTheme = PinTheme(
  width: 56,
  height: 56,
  textStyle: TextStyle(fontSize: 20, color: Color.fromRGBO(30, 60, 87, 1), fontWeight: FontWeight.w600),
  decoration: BoxDecoration(
    border: Border.all(color: Color.fromRGBO(234, 239, 243, 1)),
    borderRadius: BorderRadius.circular(20),
  ),
);

If you want all pins to be the same in every state, pass PinThemes.all(defaultPinTheme). Without a theme the built-in look marks the focused, disabled and error states. If not, create the focused, filled, empty, disabled and error themes from defaultPinTheme

final focusedPinTheme = defaultPinTheme.copyDecorationWith(
  border: Border.all(color: Color.fromRGBO(114, 178, 238, 1)),
  borderRadius: BorderRadius.circular(8),
);

final filledPinTheme = defaultPinTheme.copyDecorationWith(
  color: Color.fromRGBO(234, 239, 243, 1),
);

Put everything together

final defaultPinTheme = PinTheme(
  width: 56,
  height: 56,
  textStyle: TextStyle(fontSize: 20, color: Color.fromRGBO(30, 60, 87, 1), fontWeight: FontWeight.w600),
  decoration: BoxDecoration(
    border: Border.all(color: Color.fromRGBO(234, 239, 243, 1)),
    borderRadius: BorderRadius.circular(20),
  ),
);

final focusedPinTheme = defaultPinTheme.copyDecorationWith(
  border: Border.all(color: Color.fromRGBO(114, 178, 238, 1)),
  borderRadius: BorderRadius.circular(8),
);

final filledPinTheme = defaultPinTheme.copyDecorationWith(
  color: Color.fromRGBO(234, 239, 243, 1),
);

return Pinput(
  theme: PinThemes(
    fallback: defaultPinTheme,
    focused: focusedPinTheme,
    filled: filledPinTheme,
  ),
  validationState: const PinValidationState.idle(),
  cursor: const PinCursor.blinking(),
  onCompleted: (pin) => print(pin),
);

PinThemes mirrors WidgetStateProperty: use PinThemes(fallback:, focused:, …), PinThemes.all, or PinThemes.resolveWith((item, validation) => ...) to pick a theme per pin. item has the pin's index, value, type (PinItemStateType: focused, filled, empty, disabled, error), isFilled, isFocused and showCursor. isFocused stays accurate while type is error, and showCursor tells a full PIN's focused last pin from the cursor. In the named form, a state you don't set uses fallback. Set valid or validating to style the pins while the app supplies PinValidationState.valid() or .validating(); otherwise those states don't change the look. Implement PinThemes to share a theme as your own type, or wrap part of your app in DefaultPinThemes(themes: ..., child: ...) to style every Pinput below it that has no theme. A state theme is used exactly as written and is not merged with fallback, so derive it with copyWith. PinTheme.decoration accepts any Decoration (BoxDecoration, ShapeDecoration, …); copyDecorationWith and copyBorderWith require a BoxDecoration. PinTheme.placeholder replaces preFilledWidget. PinTheme.defaults is the built-in theme to derive from.

SMS Autofill #

iOS #

Works out of the box, by tapping the code on top of the keyboard

Android #

If you are using firebase_auth you have to set controller's value in verificationCompleted callback, here is an example code:

    Pinput(
      controller: pinController,
    );

And set pinController's value in verificationCompleted callback:

    await FirebaseAuth.instance.verifyPhoneNumber(
      verificationCompleted: (PhoneAuthCredential credential) {
        pinController.setText(credential.smsCode);
      },
      verificationFailed: (FirebaseAuthException e) {},
      codeSent: (String verificationId, int? resendToken) {},
      codeAutoRetrievalTimeout: (String verificationId) {},
    );

If you aren't using firebase_auth, you have two options, SMS Retriever API and SMS User Consent API,

SmartAuth is a wrapper package for Flutter for these APIs, so go ahead and add it as a dependency.

Pinput doesn't read SMS itself. Your app listens for the code and sets it on the controller, so it also owns starting and stopping the listener:

class _OtpPageState extends State<OtpPage> {
  final pinController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _listenForSmsCode();
  }

  Future<void> _listenForSmsCode() async {
    final res = await SmartAuth.instance.getSmsWithRetrieverApi();
    final code = res.data?.code;
    if (mounted && code != null) pinController.setText(code);
  }

  @override
  void dispose() {
    SmartAuth.instance.removeSmsRetrieverApiListener();
    pinController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Pinput(controller: pinController);
}

To keep listening for more messages, call _listenForSmsCode again after each result.

SMS Retriever API

To use Retriever API you need the App signature - guide

Note that The App Signature might be different for debug and release mode

Once you get the app signature, you should include it in the SMS message in you backend like so:

SMS example:

Your ExampleApp code is: 123456
kg+TZ3A5qzS

Example Code

Sms code will be automatically applied, without user interaction.

You don't need the App signature, the user will be prompted to confirm reading the message Example Code

Request Hint

See Example app for more templates #

Tips #

  • Controller

/// Create Controller  
final pinController = TextEditingController();

/// Set text programmatically  
pinController.setText('1222');

/// Append typed character, useful if you are using custom keyboard  
pinController.appendPin('1', 4);

/// Delete last character  
pinController.deleteLastPin();

/// Text longer than Pinput.length is cut to that length, like typed text.
/// Don't call setText, appendPin, deleteLastPin in build method, this is just illustration.  

return Pinput(
  controller: pinController,  
);  
  • Focus

/// Create FocusNode  
final pinputFocusNode = FocusNode();

/// Focus pinput  
pinputFocusNode.requestFocus();

/// UnFocus pinput  
pinputFocusNode.unfocus();

/// Don't call requestFocus, unfocus in build method, this is just illustration.  

return Pinput(
  focusNode: pinputFocusNode,
);  
  • Cursor, masking, and animation

Pinput(
  cursor: PinCursor.blinking(child: MyCursor()), // static() or hidden() are also available
  obscuring: const PinObscuring.character('*'), // null displays original text
  animation: const PinAnimation.slide(
    beginOffset: Offset(0, 0.5),
    duration: Duration(milliseconds: 250),
  ),
)

PinObscuring.character() uses a bullet by default. To keep a typed pin visible for a moment before masking, pass a duration: PinObscuring.character('*', Duration(seconds: 1)) or PinObscuring.widget(MyDot(), revealDuration: …). PinObscuring.widget(MyDot()) supplies custom visible masking and uses a bullet for native obscuring. A character must be one UTF-16 code unit, matching Flutter's native requirement. Original text remains available in the controller and callbacks. Masking widgets may be stateful: each slot creates its own State. Do not reuse a masking widget containing a shared GlobalKey across slots. PinAnimation.custom(transitionBuilder: …) supplies your own content transition, and PinCursor.blinking(blinkDuration: …) sets the blink speed. Animations and blinking stop when the platform asks to reduce motion. PinLayout(textDirection: TextDirection.ltr) keeps a numeric code left to right in an RTL app. With itemBuilder, obscuring only enables native masking; the builder must mask visible content itself.

PinAnimation supports none(), scale() (default), fade(), slide(), and rotation(). Slide defaults to Offset(0.8, 0). Every constructor accepts duration and curve, which control the transition, decoration, and error layout. Cursor blinking is independent of pin-content transitions.

Set enableInteractiveSelection: false to disable interactive selection. Set contextMenuBuilder: null to hide the modern context menu while keeping selection enabled. Legacy custom selectionControls that build their own toolbar follow Flutter's legacy toolbar behavior; use controls with TextSelectionHandleControls to customize handles with the modern menu API.

  • Validation

Pinput renders app-owned validation feedback through validationState:

The validating() factory uses the PinValidationValidating subtype for pattern matching. Presentation configurations (PinAnimation and PinObscuring) support value equality; custom masking widgets use normal widget equality.

State Behavior
PinValidationState.idle() Default; no current result or feedback.
PinValidationState.validating() Pending; normal appearance, no error.
PinValidationState.valid() Passed; normal appearance, no error.
PinValidationState.invalid([message]) Error pin theme and optional error message, even while focused.
Pinput(
  validationState: state.pinValidation,
  onChanged: cubit.pinChanged,
  onCompleted: cubit.verifyPin,
)

Run synchronous or asynchronous validation in your app, using onChanged, onCompleted, onSubmitted, or a submit button. onCompleted reports a full PIN with the cursor at the end, not successful validation. Clear old results when the PIN changes and ignore async responses for outdated inputs or disposed views. Pinput does not clear supplied feedback, run checks, or modify text or focus. Pending and valid states do not add loading/success visuals or disable editing; use inputMode: PinInputMode.readOnly or PinInputMode.disabled explicitly when needed.

For a runnable async example, open Async example in the default example app, or the Async validation tab in the demo gallery. Enter 2222 for success. The example shows progress, errors, and discarding outdated responses when edited. See AsyncValidationExample.

errorTextStyle styles the default error text. errorBuilder(context, message, pin) replaces the default error widget and ignores errorTextStyle. Both are available in Pinput, including with itemBuilder. The invalid state's message is optional: invalid() applies error styling without default text or error padding. An errorBuilder still runs with a null message, so it can supply its own localized text or other content.

Flutter Form integration #

Pinput does not register itself with Form. Wrap it in FormField<String>:

FormField<String>(
  initialValue: '',
  validator: (pin) => pin == '2224' ? null : 'Pin is incorrect',
  builder: (field) => Pinput(
    onChanged: field.didChange,
    validationState: field.errorText == null
        ? const PinValidationState.idle()
        : PinValidationState.invalid(field.errorText!),
  ),
)

Configure autovalidateMode on the wrapper for automatic Form validation. FormState.validate() remains synchronous: it cannot await server verification. An external async result must already be available if the wrapper uses it.

When using a text controller, synchronize it with the wrapper using a listener (including programmatic changes). For example, in the owning State:

final pinController = TextEditingController();
final pinFieldKey = GlobalKey<FormFieldState<String>>();

@override
void initState() {
  super.initState();
  pinController.addListener(() {
    final field = pinFieldKey.currentState;
    if (field != null && field.value != pinController.text) {
      field.didChange(pinController.text);
    }
  });
}

// Inside the Form:
FormField<String>(
  key: pinFieldKey,
  initialValue: '',
  onReset: pinController.clear,
  validator: (pin) => pin == '2224' ? null : 'Pin is incorrect',
  builder: (field) => Pinput(
    controller: pinController,
    validationState: field.errorText == null
        ? const PinValidationState.idle()
        : PinValidationState.invalid(field.errorText!),
  ),
)

Dispose pinController in the owning State. For non-empty initial values, restore that same text in onReset rather than clearing it.

FAQ #

autofill isn't working on iOS?

  • Make sure you are using real device, not simulator
  • Temporary replace Pinput with TextField, and check if autofill works. If, not it's probably a problem with SMS you are getting, autofill doesn't work with most of the languages
  • If you are using non stable version of Flutter that might be cause because something might be broken inside the Framework

are you using firebase_auth?

You should set controller's value in verificationCompleted callback, here is an example code:

    Pinput(
      controller: pinController,
    );
    
    await FirebaseAuth.instance.verifyPhoneNumber(
      verificationCompleted: (PhoneAuthCredential credential) {
        pinController.setText(credential.smsCode);
      },
      verificationFailed: (FirebaseAuthException e) {},
      codeSent: (String verificationId, int? resendToken) {},
      codeAutoRetrievalTimeout: (String verificationId) {},
    );
3.51k
likes
160
points
618k
downloads
screenshot

Documentation

API reference

Publisher

verified publishertornike.dev

Weekly Downloads

Pin code input (OTP) text field, iOS SMS autofill, Android SMS autofill One Time Code, Password, Passcode, Captcha, Security, Coupon, Wowcher, 2FA, Two step verification

Repository (GitHub)
View/report issues

Topics

#otp #pin-input #pin-code #passcode #sms-autofill

Funding

Consider supporting this project:

ko-fi.com

License

MIT (license)

Dependencies

flutter

More

Packages that depend on pinput