The Flutter Kit logoThe Flutter Kit
Template

Flutter Pin Input and OTP Screens: 6 Production Patterns (2026)

Six production-ready Flutter pin input and OTP screen patterns. SMS autofill, biometric unlock, app-lock gate, and paywall PIN with full copy-paste code.

Ahmed GaganAhmed Gagan
12 min read

Pin input screens are deceptively hard in Flutter. Between SMS OTP autofill, biometric fallback, paste handling, accessibility, and platform-specific keyboards, the typical implementation has 6 subtle bugs that take a week to find. This guide walks through 6 production patterns that cover the common use cases: phone verification, email OTP, app lock, paywall gate, parental lock, and in-app sensitive action. Each with full code that works today.

Short version: use the pinput package for phone and email OTP because it handles SMS autofill correctly on both iOS and Android. Use local_auth plus a SharedPrefs- backed PIN for app lock. Combine both with a clean state machine and the result is bug-free.

When pin input actually matters

Use caseLengthInput sourceAutofill?
Phone OTP (SMS verification)4 or 6 digitsSMS messageYes, native
Email OTP (login code)6 digitsEmailiOS autofill from Mail app
App lock (biometric fallback)4 or 6 digitsUser inputNo
Paywall PIN (parental gate)4 digitsUser inputNo
In-app sensitive action4 or 6 digitsUser inputNo
Two-factor auth6 digitsAuthenticator appiOS autofill from Messages

Pattern 1: Phone OTP with SMS autofill (pinput)

The most common pin input scenario. The user enters a phone number, receives an SMS with a 6-digit code, and taps the code into the input. With pinput plus proper SMS autofill, the code appears automatically.

# pubspec.yaml
dependencies:
  pinput: ^5.0.0
  sms_autofill: ^2.4.0   # Android-only, for SMS retriever
  firebase_auth: ^5.4.0  # if using Firebase phone auth
import 'package:pinput/pinput.dart';
import 'package:sms_autofill/sms_autofill.dart';

class PhoneOtpScreen extends StatefulWidget {
  @override
  State<PhoneOtpScreen> createState() => _PhoneOtpScreenState();
}

class _PhoneOtpScreenState extends State<PhoneOtpScreen> with CodeAutoFill {
  final _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    listenForCode(); // Android SMS retriever
  }

  @override
  void codeUpdated() {
    setState(() => _controller.text = code ?? '');
  }

  @override
  void dispose() {
    cancel();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final defaultPinTheme = PinTheme(
      width: 56,
      height: 64,
      textStyle: theme.textTheme.headlineSmall,
      decoration: BoxDecoration(
        border: Border.all(color: theme.dividerColor),
        borderRadius: BorderRadius.circular(12),
      ),
    );

    return Scaffold(
      appBar: AppBar(title: const Text('Enter verification code')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
          Text('We sent a 6-digit code to your phone.', style: theme.textTheme.bodyLarge),
          const SizedBox(height: 24),
          Pinput(
            length: 6,
            controller: _controller,
            defaultPinTheme: defaultPinTheme,
            focusedPinTheme: defaultPinTheme.copyWith(
              decoration: defaultPinTheme.decoration!.copyWith(
                border: Border.all(color: theme.colorScheme.primary, width: 2),
              ),
            ),
            onCompleted: (code) => _verify(code),
            androidSmsAutofillMethod: AndroidSmsAutofillMethod.smsRetrieverApi,
          ),
        ]),
      ),
    );
  }

  Future<void> _verify(String code) async {
    // Call FirebaseAuth.signInWithCredential or your backend here
  }
}

Three things to get right:

  • iOS uses a shared cross-app autofill. When iOS detects a code in Messages, the keyboard suggestion bar offers it automatically. You do not need any code for this; iOS reads the field's textContentType which pinput sets correctly by default.
  • Android uses the SMS Retriever API. Register the app's SMS hash and include it in the message body. sms_autofill plus the SMS Retriever pattern is the privacy-respecting way (no SMS-read permission needed).
  • 6-digit codes are the 2026 standard. 4-digit codes are too guessable; 8+ feel excessive. Firebase and Twilio both default to 6.

Pattern 2: Email OTP (login code via email)

Passwordless email auth has replaced password-based login in many 2026 indie apps. Same UI as phone OTP, just different backend. The only autofill is iOS which reads codes from the Mail app.

Pinput(
  length: 6,
  keyboardType: TextInputType.number,
  onCompleted: (code) async {
    final user = await api.verifyEmailCode(email, code);
    if (!context.mounted) return;
    context.go('/home');
  },
),

Pattern 3: App lock with biometric fallback

Users expect to unlock the app with Face ID, Touch ID, or fingerprint. If biometric fails three times or the user prefers not to use it, fall back to a PIN. The PIN is stored hashed in secure storage.

# pubspec.yaml
dependencies:
  local_auth: ^2.2.0
  flutter_secure_storage: ^10.0.0
  crypto: ^3.0.6
class AppLockService {
  final _auth = LocalAuthentication();
  final _storage = const FlutterSecureStorage();

  Future<bool> unlockWithBiometric() async {
    final canUse = await _auth.canCheckBiometrics;
    if (!canUse) return false;
    return _auth.authenticate(
      localizedReason: 'Unlock the app',
      options: const AuthenticationOptions(biometricOnly: true),
    );
  }

  Future<bool> unlockWithPin(String pin) async {
    final storedHash = await _storage.read(key: 'app_lock_pin_hash');
    if (storedHash == null) return false;
    final inputHash = sha256.convert(utf8.encode(pin)).toString();
    return inputHash == storedHash;
  }

  Future<void> setPin(String pin) async {
    final hash = sha256.convert(utf8.encode(pin)).toString();
    await _storage.write(key: 'app_lock_pin_hash', value: hash);
  }
}

The app lock screen shows a biometric prompt on mount. If the user cancels or fails, the pin input appears. After three failed PIN attempts, add a 30-second cooldown. Never store the raw PIN; hash it with SHA-256 and store the hash in flutter_secure_storage.

Pattern 4: Paywall PIN (parental gate)

Kids apps sometimes need a parental gate before the user can access the paywall or settings. Apple and Google both require this for apps targeted at children.

The pattern is a simple 4-digit PIN the parent sets once during onboarding. Show it when the user tries to subscribe, change settings, or access mature content. Do not gate every paywall view; only gate the purchase action.

Future<bool> showParentalGate(BuildContext context) async {
  return await showDialog<bool>(
    context: context,
    barrierDismissible: false,
    builder: (_) => const ParentalGateDialog(),
  ) ?? false;
}

Pattern 5: In-app sensitive action (re-authenticate before destructive)

Common in banking, crypto, and pro-tier apps. Before the user deletes their account, transfers funds, or changes their email, prompt for the PIN again even if the app is already unlocked. Treat it like a "sudo" prompt on desktop.

Pattern 6: Two-factor authentication (6-digit time-based code)

For apps that integrate with Google Authenticator or similar. The UI is the same 6-digit pin input. The backend verifies the TOTP code. iOS autofills from the Messages app if the 2FA code was sent via SMS, otherwise the user types it.

Design variants: how the pin input actually looks

The pinput package supports all the common designs. Pick one that matches your brand.

StyleFeelBest for
Boxes (outlined rectangles)Material 3 default, neutralMost apps
Underline onlyiOS-style, minimalNative iOS feel
Filled dots (hidden)Secure, passcode feelApp lock, sensitive actions
CirclesFriendly, kid-appParental gates
Compact pillDense, mobile-firstSmall-screen apps

Accessibility

  • Every pin box needs a Semantics(label: 'Digit 1 of 6') so screen readers announce position.
  • Support paste: users may copy a code from email. pinput handles this automatically; native TextField needs a paste detector.
  • Keyboard should default to number pad (TextInputType.number).
  • On iOS, set textContentType: oneTimeCode to enable SMS autofill.
  • Test with VoiceOver on iOS and TalkBack on Android.

Common failure modes

  • SMS autofill not working on Android. The app's SMS hash is missing from the SMS body. Generate it with signingReport and include it in the template.
  • iOS autofill suggestion not appearing. The field'stextContentType is not set to oneTimeCode. pinputhandles this; native TextField needs it manual.
  • Paste includes the full SMS body, not just the code. Parse digits out of the pasted text before populating the field.
  • Biometric fails silently. Check isDeviceSupported beforecanCheckBiometrics; the former catches devices without biometric hardware.
  • PIN is easily brute-forced. Add exponential backoff after 3 failed attempts. Lock out for 60 seconds, then 5 minutes, then require biometric or email verification.

What The Flutter Kit ships

The Flutter Kit includes the 6 patterns above as ready-to-use screens: phone OTP, email OTP, app lock with biometric fallback, paywall PIN, sensitive-action re-auth, and 2FA. The pinput package is pre-configured with the brand theme, SMS autofill is wired for both iOS and Android, and the app lock flow uses local_authplus flutter_secure_storage out of the box.

$69 one-time, unlimited commercial projects. See every integration on the features page or jump to checkout.

Final recommendation

Use pinput for anything SMS or email OTP. Use local_auth plus SharedPrefs-hashed PIN for app lock. Don't try to build these from scratch withTextField widgets; the SMS-autofill subtleties alone will burn three days. Copy the six patterns above into your app and you're done in an afternoon.

Share this article

Ready to ship your Flutter app faster?

The Flutter Kit gives you a production-ready Flutter codebase with onboarding, paywalls, auth, AI integrations, and more. Stop building boilerplate. Start building your product.

Get The Flutter Kit