flutter_smart_otp 1.0.0 copy "flutter_smart_otp: ^1.0.0" to clipboard
flutter_smart_otp: ^1.0.0 copied to clipboard

A highly customizable, production-ready OTP / PIN input field for Flutter with auto-focus, paste support, validation, RTL/LTR support, and rich styling options.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_otp/flutter_smart_otp.dart';

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

/// Root widget of the example application.
class SmartOtpExampleApp extends StatelessWidget {
  /// Creates the example app.
  const SmartOtpExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    final baseScheme = ColorScheme.fromSeed(seedColor: const Color(0xFF2962FF));
    return MaterialApp(
      title: 'flutter_smart_otp demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: baseScheme,
        scaffoldBackgroundColor: const Color(0xFFF5F7FB),
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.transparent,
          elevation: 0,
          foregroundColor: Colors.black87,
        ),
      ),
      home: const OtpShowcasePage(),
    );
  }
}

/// A page that showcases every capability of [SmartOtpField] inside
/// individually explained cards.
class OtpShowcasePage extends StatefulWidget {
  /// Creates the showcase page.
  const OtpShowcasePage({super.key});

  @override
  State<OtpShowcasePage> createState() => _OtpShowcasePageState();
}

class _OtpShowcasePageState extends State<OtpShowcasePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('flutter_smart_otp'),
        centerTitle: false,
      ),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
        children: const [
          _SectionHeader(
            title: 'Basic verification',
            subtitle:
                'Default 4-digit field with auto-focus and completion callback.',
          ),
          _BasicDemoCard(),
          SizedBox(height: 24),
          _SectionHeader(
            title: 'Custom styling',
            subtitle:
                'Custom colors, radius, spacing, box size, and text style.',
          ),
          _StyledDemoCard(),
          SizedBox(height: 24),
          _SectionHeader(
            title: 'Obscured PIN entry',
            subtitle:
                'Six-digit PIN with obscured characters, like a banking app.',
          ),
          _ObscuredDemoCard(),
          SizedBox(height: 24),
          _SectionHeader(
            title: 'Validation & server errors',
            subtitle:
                'Combines a local validator with a simulated server rejection.',
          ),
          _ValidationDemoCard(),
          SizedBox(height: 24),
          _SectionHeader(
            title: 'Resend timer with imperative control',
            subtitle:
                'Uses a GlobalKey to clear the field and manage external controllers.',
          ),
          _ResendTimerDemoCard(),
          SizedBox(height: 24),
          _SectionHeader(
            title: 'Disabled & read-only states',
            subtitle: 'Toggle between enabled, disabled, and read-only.',
          ),
          _StatesDemoCard(),
          SizedBox(height: 24),
          _SectionHeader(
            title: 'Right-to-left layout',
            subtitle: 'Same widget, mirrored for RTL locales such as Arabic.',
          ),
          _RtlDemoCard(),
        ],
      ),
    );
  }
}

class _SectionHeader extends StatelessWidget {
  const _SectionHeader({required this.title, required this.subtitle});

  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title,
              style: theme.textTheme.titleMedium
                  ?.copyWith(fontWeight: FontWeight.w700)),
          const SizedBox(height: 4),
          Text(
            subtitle,
            style: theme.textTheme.bodySmall?.copyWith(color: Colors.black54),
          ),
        ],
      ),
    );
  }
}

class _DemoCard extends StatelessWidget {
  const _DemoCard({required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(20),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.04),
            blurRadius: 16,
            offset: const Offset(0, 8),
          ),
        ],
      ),
      child: child,
    );
  }
}

class _BasicDemoCard extends StatefulWidget {
  const _BasicDemoCard();

  @override
  State<_BasicDemoCard> createState() => _BasicDemoCardState();
}

class _BasicDemoCardState extends State<_BasicDemoCard> {
  String _status = 'Enter the 4-digit code sent to your phone.';

  @override
  Widget build(BuildContext context) {
    return _DemoCard(
      child: Column(
        children: [
          SmartOtpField(
            length: 4,
            autofocus: false,
            keyboardType: TextInputType.number,
            inputFormatters: [FilteringTextInputFormatter.digitsOnly],
            onChanged: (value) =>
                setState(() => _status = 'Current value: $value'),
            onCompleted: (value) =>
                setState(() => _status = 'Completed with $value!'),
          ),
          const SizedBox(height: 16),
          Text(_status, style: Theme.of(context).textTheme.bodySmall),
        ],
      ),
    );
  }
}

class _StyledDemoCard extends StatelessWidget {
  const _StyledDemoCard();

  @override
  Widget build(BuildContext context) {
    return _DemoCard(
      child: SmartOtpField(
        length: 5,
        boxWidth: 52,
        boxHeight: 60,
        spacing: 10,
        borderRadius: 16,
        borderWidth: 2,
        borderColor: const Color(0xFFE0E0E0),
        focusedBorderColor: const Color(0xFF7C4DFF),
        errorBorderColor: const Color(0xFFFF5252),
        fillColor: const Color(0xFFF3F1FF),
        focusedFillColor: const Color(0xFFEDE7FF),
        textStyle: const TextStyle(
          fontSize: 22,
          fontWeight: FontWeight.w700,
          color: Color(0xFF311B92),
        ),
        cursorColor: const Color(0xFF7C4DFF),
        keyboardType: TextInputType.number,
        inputFormatters: [FilteringTextInputFormatter.digitsOnly],
      ),
    );
  }
}

class _ObscuredDemoCard extends StatelessWidget {
  const _ObscuredDemoCard();

  @override
  Widget build(BuildContext context) {
    return _DemoCard(
      child: SmartOtpField(
        length: 6,
        obscureText: true,
        obscuringCharacter: '●',
        boxWidth: 42,
        boxHeight: 52,
        spacing: 8,
        borderRadius: 10,
        keyboardType: TextInputType.number,
        inputFormatters: [FilteringTextInputFormatter.digitsOnly],
        textStyle: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
      ),
    );
  }
}

class _ValidationDemoCard extends StatefulWidget {
  const _ValidationDemoCard();

  @override
  State<_ValidationDemoCard> createState() => _ValidationDemoCardState();
}

class _ValidationDemoCardState extends State<_ValidationDemoCard> {
  final GlobalKey<SmartOtpFieldState> _fieldKey =
      GlobalKey<SmartOtpFieldState>();
  bool _isVerifying = false;

  String? _validate(String? value) {
    if (value == null || value.length < 4) {
      return 'Please enter all 4 digits.';
    }
    return null;
  }

  Future<void> _verify() async {
    final isLocallyValid = _fieldKey.currentState!.validate();
    if (!isLocallyValid) return;

    setState(() => _isVerifying = true);
    await Future<void>.delayed(const Duration(seconds: 1));
    setState(() => _isVerifying = false);

    final code = _fieldKey.currentState!.value;
    if (code != '1234') {
      _fieldKey.currentState!
          .setError('Incorrect code. Try 1234 for this demo.');
    } else {
      _fieldKey.currentState!.setError(null);
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Verified successfully!')),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return _DemoCard(
      child: Column(
        children: [
          SmartOtpField(
            key: _fieldKey,
            length: 4,
            validator: _validate,
            autovalidateMode: AutovalidateMode.onUserInteraction,
            keyboardType: TextInputType.number,
            inputFormatters: [FilteringTextInputFormatter.digitsOnly],
          ),
          const SizedBox(height: 16),
          FilledButton(
            onPressed: _isVerifying ? null : _verify,
            child: _isVerifying
                ? const SizedBox(
                    width: 18,
                    height: 18,
                    child: CircularProgressIndicator(
                        strokeWidth: 2, color: Colors.white),
                  )
                : const Text('Verify code'),
          ),
        ],
      ),
    );
  }
}

class _ResendTimerDemoCard extends StatefulWidget {
  const _ResendTimerDemoCard();

  @override
  State<_ResendTimerDemoCard> createState() => _ResendTimerDemoCardState();
}

class _ResendTimerDemoCardState extends State<_ResendTimerDemoCard> {
  static const int _totalSeconds = 30;

  late final List<TextEditingController> _controllers;
  late final List<FocusNode> _focusNodes;
  final GlobalKey<SmartOtpFieldState> _fieldKey =
      GlobalKey<SmartOtpFieldState>();

  Timer? _timer;
  int _secondsRemaining = _totalSeconds;

  @override
  void initState() {
    super.initState();
    _controllers =
        List<TextEditingController>.generate(4, (_) => TextEditingController());
    _focusNodes = List<FocusNode>.generate(4, (_) => FocusNode());
    _startTimer();
  }

  void _startTimer() {
    _timer?.cancel();
    setState(() => _secondsRemaining = _totalSeconds);
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (_secondsRemaining <= 1) {
        timer.cancel();
        setState(() => _secondsRemaining = 0);
      } else {
        setState(() => _secondsRemaining -= 1);
      }
    });
  }

  void _resend() {
    _fieldKey.currentState!.clear();
    _startTimer();
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('A new code has been sent.')),
    );
  }

  @override
  void dispose() {
    _timer?.cancel();
    for (final controller in _controllers) {
      controller.dispose();
    }
    for (final node in _focusNodes) {
      node.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final canResend = _secondsRemaining == 0;
    return _DemoCard(
      child: Column(
        children: [
          SmartOtpField(
            key: _fieldKey,
            length: 4,
            controllers: _controllers,
            focusNodes: _focusNodes,
            keyboardType: TextInputType.number,
            inputFormatters: [FilteringTextInputFormatter.digitsOnly],
          ),
          const SizedBox(height: 16),
          TextButton(
            onPressed: canResend ? _resend : null,
            child: Text(
              canResend
                  ? 'Resend code'
                  : 'Resend available in ${_secondsRemaining}s',
            ),
          ),
        ],
      ),
    );
  }
}

class _StatesDemoCard extends StatefulWidget {
  const _StatesDemoCard();

  @override
  State<_StatesDemoCard> createState() => _StatesDemoCardState();
}

enum _FieldMode { enabled, disabled, readOnly }

class _StatesDemoCardState extends State<_StatesDemoCard> {
  _FieldMode _mode = _FieldMode.enabled;

  @override
  Widget build(BuildContext context) {
    return _DemoCard(
      child: Column(
        children: [
          SmartOtpField(
            length: 4,
            enabled: _mode != _FieldMode.disabled,
            readOnly: _mode == _FieldMode.readOnly,
            keyboardType: TextInputType.number,
            inputFormatters: [FilteringTextInputFormatter.digitsOnly],
          ),
          const SizedBox(height: 16),
          SegmentedButton<_FieldMode>(
            segments: const [
              ButtonSegment(value: _FieldMode.enabled, label: Text('Enabled')),
              ButtonSegment(
                  value: _FieldMode.disabled, label: Text('Disabled')),
              ButtonSegment(
                  value: _FieldMode.readOnly, label: Text('Read-only')),
            ],
            selected: {_mode},
            onSelectionChanged: (selection) =>
                setState(() => _mode = selection.first),
          ),
        ],
      ),
    );
  }
}

class _RtlDemoCard extends StatelessWidget {
  const _RtlDemoCard();

  @override
  Widget build(BuildContext context) {
    return _DemoCard(
      child: SmartOtpField(
        length: 4,
        textDirection: TextDirection.rtl,
        keyboardType: TextInputType.number,
        inputFormatters: [FilteringTextInputFormatter.digitsOnly],
        semanticLabel: 'حقل رمز التحقق المكون من أربعة أرقام',
      ),
    );
  }
}
2
likes
160
points
37
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A highly customizable, production-ready OTP / PIN input field for Flutter with auto-focus, paste support, validation, RTL/LTR support, and rich styling options.

Repository (GitHub)
View/report issues

Topics

#otp #pin #input #form #widget

License

MIT (license)

Dependencies

characters, flutter

More

Packages that depend on flutter_smart_otp