premium_otp_input 0.1.0 copy "premium_otp_input: ^0.1.0" to clipboard
premium_otp_input: ^0.1.0 copied to clipboard

A highly customizable, beautiful, and interactive OTP and PIN entry widget for Flutter featuring rich animations, verification loaders, and secure masking.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:premium_otp_input/premium_otp_input.dart';

void main() {
  runApp(const MyApp());
}

/// The main application widget for the example.
class MyApp extends StatelessWidget {
  /// Creates the [MyApp] widget.
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Premium OTP Input Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.light(useMaterial3: true).copyWith(
        scaffoldBackgroundColor: const Color(0xFFF8FAFC),
        colorScheme: const ColorScheme.light(
          primary: Color(0xFF3B82F6),
          surface: Colors.white,
        ),
      ),
      home: const OtpDemoScreen(),
    );
  }
}

/// A screen showcasing the different interactive features of the [PremiumOtpInput] widget.
class OtpDemoScreen extends StatefulWidget {
  /// Creates the [OtpDemoScreen] widget.
  const OtpDemoScreen({super.key});

  @override
  State<OtpDemoScreen> createState() => _OtpDemoScreenState();
}

enum DemoStyle { standard, liquid, motion }

class _OtpDemoScreenState extends State<OtpDemoScreen> {
  final TextEditingController _otpController = TextEditingController();
  final FocusNode _otpFocusNode = FocusNode();

  final TextEditingController _liquidOtpController = TextEditingController();
  final FocusNode _liquidOtpFocusNode = FocusNode();

  final TextEditingController _motionOtpController = TextEditingController();
  final FocusNode _motionOtpFocusNode = FocusNode();

  DemoStyle _currentStyle = DemoStyle.motion;
  bool _isVerifying = false;
  bool _isSuccess = false;
  bool _isError = false;

  OtpEntryAnimationStyle _entryAnimationStyle = OtpEntryAnimationStyle.scale;
  OtpSuccessAnimationStyle _successAnimationStyle =
      OtpSuccessAnimationStyle.bounce;
  bool _obscureText = false;
  String _obscuringCharacter = '●';

  void _handleOtpCompleted(String value) async {
    setState(() {
      _isVerifying = true;
      _isError = false;
    });

    // Simulate verification delay
    await Future.delayed(const Duration(milliseconds: 1500));

    if (!mounted) return;

    if (value == "123456") {
      setState(() {
        _isVerifying = false;
        _isSuccess = true;
      });
    } else {
      setState(() {
        _isVerifying = false;
        _isError = true;
      });
      // Automatically clear the error state after 2 seconds
      await Future.delayed(const Duration(seconds: 2));
      if (!mounted) return;
      setState(() {
        _isError = false;
        _otpController.clear();
        _otpFocusNode.requestFocus();
      });
    }
  }

  void _handleLiquidOtpCompleted(String value) async {
    setState(() {
      _isVerifying = true;
      _isError = false;
    });

    // Wait roughly 200–300ms (we use 250ms) as specified in the timeline:
    // "After Last Digit: The app waits roughly 200–300ms. This small delay makes it feel like 'System is checking your code.'"
    await Future.delayed(const Duration(milliseconds: 250));

    if (!mounted) return;

    if (value == "1234") {
      setState(() {
        _isVerifying = false;
        _isSuccess = true;
      });
    } else {
      setState(() {
        _isVerifying = false;
        _isError = true;
      });
      // Automatically clear the error state after 2 seconds
      await Future.delayed(const Duration(seconds: 2));
      if (!mounted) return;
      setState(() {
        _isError = false;
        _liquidOtpController.clear();
        _liquidOtpFocusNode.requestFocus();
      });
    }
  }

  void _handleMotionOtpCompleted(String value) async {
    setState(() {
      _isVerifying = true;
      _isError = false;
    });

    // Simulated network request
    await Future.delayed(const Duration(milliseconds: 1500));

    if (!mounted) return;

    if (value == "1234") {
      setState(() {
        _isVerifying = false;
        _isSuccess = true;
      });
    } else {
      setState(() {
        _isVerifying = false;
        _isError = true;
      });
      // Automatically clear the error state after 2 seconds
      await Future.delayed(const Duration(seconds: 2));
      if (!mounted) return;
      setState(() {
        _isError = false;
        _motionOtpController.clear();
        _motionOtpFocusNode.requestFocus();
      });
    }
  }

  void _reset() {
    setState(() {
      _isVerifying = false;
      _isSuccess = false;
      _isError = false;
      _otpController.clear();
      _liquidOtpController.clear();
      _motionOtpController.clear();
      if (_currentStyle == DemoStyle.liquid) {
        _liquidOtpFocusNode.requestFocus();
      } else if (_currentStyle == DemoStyle.motion) {
        _motionOtpFocusNode.requestFocus();
      } else {
        _otpFocusNode.requestFocus();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'Premium OTP Input',
          style: TextStyle(
            color: Color(0xFF0F172A),
            fontWeight: FontWeight.bold,
          ),
        ),
        backgroundColor: Colors.transparent,
        elevation: 0,
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // Style Selector (Toggle between Standard and Liquid Animation Styles)
              Center(
                child: Container(
                  padding: const EdgeInsets.all(4),
                  decoration: BoxDecoration(
                    color: const Color(0xFFE2E8F0),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      GestureDetector(
                        onTap: () {
                          setState(() {
                            _currentStyle = DemoStyle.standard;
                            _reset();
                          });
                        },
                        child: Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 16,
                            vertical: 8,
                          ),
                          decoration: BoxDecoration(
                            color: _currentStyle == DemoStyle.standard
                                ? const Color(0xFF4F6EF7)
                                : Colors.transparent,
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Text(
                            'Standard',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              color: _currentStyle == DemoStyle.standard
                                  ? Colors.white
                                  : const Color(0xFF475569),
                            ),
                          ),
                        ),
                      ),
                      GestureDetector(
                        onTap: () {
                          setState(() {
                            _currentStyle = DemoStyle.liquid;
                            _reset();
                          });
                        },
                        child: Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 16,
                            vertical: 8,
                          ),
                          decoration: BoxDecoration(
                            color: _currentStyle == DemoStyle.liquid
                                ? const Color(0xFF4F6EF7)
                                : Colors.transparent,
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Text(
                            'Liquid',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              color: _currentStyle == DemoStyle.liquid
                                  ? Colors.white
                                  : const Color(0xFF475569),
                            ),
                          ),
                        ),
                      ),
                      GestureDetector(
                        onTap: () {
                          setState(() {
                            _currentStyle = DemoStyle.motion;
                            _reset();
                          });
                        },
                        child: Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 16,
                            vertical: 8,
                          ),
                          decoration: BoxDecoration(
                            color: _currentStyle == DemoStyle.motion
                                ? const Color(0xFF4F6EF7)
                                : Colors.transparent,
                            borderRadius: BorderRadius.circular(8),
                          ),
                          child: Text(
                            'Motion',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              color: _currentStyle == DemoStyle.motion
                                  ? Colors.white
                                  : const Color(0xFF475569),
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 24),

              // 1. Info / Status banner
              Card(
                color: Colors.white,
                elevation: 2,
                shadowColor: Colors.black12,
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: Column(
                    children: [
                      Text(
                        _currentStyle == DemoStyle.motion
                            ? 'Premium Motion Animation'
                            : _currentStyle == DemoStyle.liquid
                            ? 'Liquid Motion Verification'
                            : 'Interactive Demo App',
                        style: TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                          color: _currentStyle == DemoStyle.standard
                              ? const Color(0xFFF97316)
                              : const Color(0xFF4F6EF7),
                        ),
                      ),
                      const SizedBox(height: 8),
                      Text(
                        _isSuccess
                            ? 'Verification Successful!'
                            : _isError
                            ? 'Verification Failed (Try again)'
                            : _isVerifying
                            ? 'Verifying code...'
                            : _currentStyle == DemoStyle.standard
                            ? 'Enter "123456" for success, any other code for error.'
                            : 'Enter "1234" for success, any other code for error.',
                        textAlign: TextAlign.center,
                        style: TextStyle(
                          color: _isSuccess
                              ? const Color(0xFF22C55E)
                              : _isError
                              ? const Color(0xFFEF5350)
                              : const Color(0xFF475569),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 32),

              // 2. The OTP Input component
              if (_currentStyle == DemoStyle.motion)
                MotionOtpVerificationView(
                  length: 4,
                  controller: _motionOtpController,
                  focusNode: _motionOtpFocusNode,
                  isSuccess: _isSuccess,
                  isError: _isError,
                  isVerifying: _isVerifying,
                  onCompleted: _handleMotionOtpCompleted,
                  onResend: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                        content: Text('OTP Resent successfully!'),
                        backgroundColor: Color(0xFF4F6EF7),
                      ),
                    );
                  },
                )
              else if (_currentStyle == DemoStyle.liquid)
                LiquidOtpVerificationView(
                  length: 4,
                  controller: _liquidOtpController,
                  focusNode: _liquidOtpFocusNode,
                  isSuccess: _isSuccess,
                  isError: _isError,
                  isVerifying: _isVerifying,
                  onCompleted: _handleLiquidOtpCompleted,
                  cardBackgroundColor: Colors.white,
                  boxBackgroundColor: const Color(0xFFF3F4F6),
                  inactiveBorderColor: const Color(0xFFE5E7EB),
                  headingTextColor: const Color(0xFF111827),
                  subtitleTextColor: const Color(0xFF6D5CE0),
                  bodyTextColor: const Color(0xFF374151),
                  hintTextColor: const Color(0xFF9CA3AF),
                  primaryColor: const Color(0xFF3B82F6),
                  successColor: const Color(0xFF22C55E),
                  errorColor: const Color(0xFFEF5350),
                  onResend: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                        content: Text('OTP Resent successfully!'),
                        backgroundColor: Color(0xFF4F6EF7),
                      ),
                    );
                  },
                )
              else
                PremiumOtpInput(
                  length: 6,
                  controller: _otpController,
                  focusNode: _otpFocusNode,
                  isSuccess: _isSuccess,
                  isError: _isError,
                  isVerifying: _isVerifying,
                  onCompleted: _handleOtpCompleted,
                  obscureText: _obscureText,
                  obscuringCharacter: _obscuringCharacter,
                  entryAnimationStyle: _entryAnimationStyle,
                  successAnimationStyle: _successAnimationStyle,
                  animateActiveBorder: true,
                  boxHeight: 64.0,
                  spacing: 12.0,
                  borderRadius: 16.0,
                  activeBorderColor: const Color(0xFF3B82F6),
                  defaultBorderColor: const Color(0xFFE2E8F0),
                  successColor: const Color(0xFF22C55E),
                  errorColor: const Color(0xFFEF5350),
                ),
              const SizedBox(height: 32),

              // 3. Actions / Controls
              if (_isSuccess || _isError || _isVerifying)
                Center(
                  child: TextButton.icon(
                    onPressed: _reset,
                    icon: const Icon(Icons.refresh),
                    label: const Text('Reset Input'),
                    style: TextButton.styleFrom(
                      foregroundColor: const Color(0xFF3B82F6),
                    ),
                  ),
                ),
              const SizedBox(height: 24),

              // 4. Configuration Controls Card (Only show for standard widget customization)
              if (_currentStyle == DemoStyle.standard)
                Card(
                  color: Colors.white,
                  elevation: 2,
                  shadowColor: Colors.black12,
                  child: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        const Text(
                          'Customize Settings',
                          style: TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.bold,
                            color: Color(0xFF0F172A),
                          ),
                        ),
                        const Divider(height: 24, color: Color(0xFFE2E8F0)),

                        // Entry Animation Style
                        const Text(
                          'Digit Entry Animation',
                          style: TextStyle(
                            color: Color(0xFF475569),
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                        const SizedBox(height: 8),
                        Wrap(
                          spacing: 8.0,
                          children: OtpEntryAnimationStyle.values.map((style) {
                            return ChoiceChip(
                              label: Text(style.name),
                              selected: _entryAnimationStyle == style,
                              onSelected: (selected) {
                                if (selected) {
                                  setState(() => _entryAnimationStyle = style);
                                }
                              },
                            );
                          }).toList(),
                        ),
                        const SizedBox(height: 20),

                        // Success Animation Style
                        const Text(
                          'Success State Transition',
                          style: TextStyle(
                            color: Color(0xFF475569),
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                        const SizedBox(height: 8),
                        Wrap(
                          spacing: 8.0,
                          children: OtpSuccessAnimationStyle.values.map((
                            style,
                          ) {
                            return ChoiceChip(
                              label: Text(style.name),
                              selected: _successAnimationStyle == style,
                              onSelected: (selected) {
                                if (selected) {
                                  setState(
                                    () => _successAnimationStyle = style,
                                  );
                                }
                              },
                            );
                          }).toList(),
                        ),
                        const SizedBox(height: 20),

                        // Obscure text & character selection
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            const Text(
                              'Obscure / Hide Text',
                              style: TextStyle(
                                color: Color(0xFF475569),
                                fontWeight: FontWeight.w600,
                              ),
                            ),
                            Switch(
                              value: _obscureText,
                              activeThumbColor: const Color(0xFF3B82F6),
                              onChanged: (val) {
                                setState(() => _obscureText = val);
                              },
                            ),
                          ],
                        ),
                        if (_obscureText) ...[
                          const SizedBox(height: 12),
                          const Text(
                            'Obscuring Character',
                            style: TextStyle(
                              color: Color(0xFF475569),
                              fontWeight: FontWeight.w600,
                            ),
                          ),
                          const SizedBox(height: 8),
                          Wrap(
                            spacing: 12.0,
                            children: ['●', '*', '★', '♥'].map((char) {
                              return ChoiceChip(
                                label: Text(char),
                                selected: _obscuringCharacter == char,
                                onSelected: (selected) {
                                  if (selected) {
                                    setState(() => _obscuringCharacter = char);
                                  }
                                },
                              );
                            }).toList(),
                          ),
                        ],
                      ],
                    ),
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }
}
10
likes
0
points
305
downloads

Publisher

unverified uploader

Weekly Downloads

A highly customizable, beautiful, and interactive OTP and PIN entry widget for Flutter featuring rich animations, verification loaders, and secure masking.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, google_fonts

More

Packages that depend on premium_otp_input