premium_otp_input 0.4.0
premium_otp_input: ^0.4.0 copied to clipboard
A highly customizable, beautiful, and interactive OTP and PIN entry widget for Flutter featuring Sigil, Nexus, Lightning, Liquid, and Motion animations.
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.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 StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isGlobalDark = true;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Premium OTP Input Showcase',
debugShowCheckedModeBanner: false,
themeMode: _isGlobalDark ? ThemeMode.dark : ThemeMode.light,
theme: ThemeData.light(useMaterial3: true).copyWith(
scaffoldBackgroundColor: const Color(0xFFF8FAFC),
colorScheme: const ColorScheme.light(
primary: Color(0xFF4F6EF7),
surface: Colors.white,
),
),
darkTheme: ThemeData.dark(useMaterial3: true).copyWith(
scaffoldBackgroundColor: const Color(0xFF0B0F19),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF6366F1),
surface: Color(0xFF1E293B),
),
),
home: OtpDemoScreen(
isGlobalDark: _isGlobalDark,
onToggleGlobalTheme: () {
setState(() {
_isGlobalDark = !_isGlobalDark;
});
},
),
);
}
}
enum DemoStyle { nexus, sigil, liquid, lightning, motion, standard }
class OtpDemoScreen extends StatefulWidget {
final bool isGlobalDark;
final VoidCallback onToggleGlobalTheme;
const OtpDemoScreen({
super.key,
required this.isGlobalDark,
required this.onToggleGlobalTheme,
});
@override
State<OtpDemoScreen> createState() => _OtpDemoScreenState();
}
class _OtpDemoScreenState extends State<OtpDemoScreen> {
// Controllers and Focus Nodes
final TextEditingController _sigilController = TextEditingController();
final FocusNode _sigilFocusNode = FocusNode();
final TextEditingController _liquidController = TextEditingController();
final FocusNode _liquidFocusNode = FocusNode();
final TextEditingController _lightningController = TextEditingController();
final FocusNode _lightningFocusNode = FocusNode();
final TextEditingController _motionController = TextEditingController();
final FocusNode _motionFocusNode = FocusNode();
final TextEditingController _nexusController = TextEditingController();
final FocusNode _nexusFocusNode = FocusNode();
final TextEditingController _standardController = TextEditingController();
final FocusNode _standardFocusNode = FocusNode();
DemoStyle _currentStyle = DemoStyle.nexus;
// Widget States
bool _isVerifying = false;
bool _isSuccess = false;
bool _isError = false;
// Per-widget dark overrides (null = sync with global theme)
bool? _widgetDarkOverride;
// Standard Customization Options
OtpEntryAnimationStyle _entryAnimationStyle = OtpEntryAnimationStyle.scale;
OtpSuccessAnimationStyle _successAnimationStyle =
OtpSuccessAnimationStyle.bounce;
bool _obscureText = false;
String _obscuringCharacter = 'β';
double _boxHeight = 64.0;
double _borderRadius = 16.0;
bool get _isEffectiveDark => _widgetDarkOverride ?? widget.isGlobalDark;
@override
void dispose() {
_sigilController.dispose();
_sigilFocusNode.dispose();
_liquidController.dispose();
_liquidFocusNode.dispose();
_lightningController.dispose();
_lightningFocusNode.dispose();
_motionController.dispose();
_motionFocusNode.dispose();
_nexusController.dispose();
_nexusFocusNode.dispose();
_standardController.dispose();
_standardFocusNode.dispose();
super.dispose();
}
void _clearAllInput() {
setState(() {
_isVerifying = false;
_isSuccess = false;
_isError = false;
_sigilController.clear();
_liquidController.clear();
_lightningController.clear();
_motionController.clear();
_nexusController.clear();
_standardController.clear();
});
_focusActiveNode();
}
void _focusActiveNode() {
switch (_currentStyle) {
case DemoStyle.sigil:
_sigilFocusNode.requestFocus();
case DemoStyle.liquid:
_liquidFocusNode.requestFocus();
case DemoStyle.lightning:
_lightningFocusNode.requestFocus();
case DemoStyle.motion:
_motionFocusNode.requestFocus();
case DemoStyle.nexus:
_nexusFocusNode.requestFocus();
case DemoStyle.standard:
_standardFocusNode.requestFocus();
}
}
void _simulateCompletion(String code, String targetSuccessCode) async {
setState(() {
_isVerifying = true;
_isError = false;
});
await Future.delayed(const Duration(milliseconds: 1400));
if (!mounted) return;
if (code == targetSuccessCode) {
setState(() {
_isVerifying = false;
_isSuccess = true;
});
} else {
setState(() {
_isVerifying = false;
_isError = true;
});
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
setState(() {
_isError = false;
});
_clearAllInput();
}
}
@override
Widget build(BuildContext context) {
final isDark = widget.isGlobalDark;
final bg = isDark ? const Color(0xFF0B0F19) : const Color(0xFFF1F5F9);
final cardBg = isDark ? const Color(0xFF131C2E) : Colors.white;
final textPrimary = isDark ? Colors.white : const Color(0xFF0F172A);
final textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF64748B);
return Scaffold(
backgroundColor: bg,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: false,
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFFA855F7)],
),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.shield_outlined, color: Colors.white, size: 20),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Premium OTP Input',
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.bold,
color: textPrimary,
),
),
Text(
'v1.2.0 β’ Ultra Creative Flutter Suite',
style: GoogleFonts.outfit(
fontSize: 11,
fontWeight: FontWeight.w500,
color: textSecondary,
),
),
],
),
],
),
actions: [
IconButton(
tooltip: isDark ? 'Switch to Light Mode' : 'Switch to Dark Mode',
icon: Icon(
isDark ? Icons.wb_sunny_rounded : Icons.nightlight_round,
color: isDark ? const Color(0xFFF59E0B) : const Color(0xFF6366F1),
),
onPressed: widget.onToggleGlobalTheme,
),
const SizedBox(width: 12),
],
),
body: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 1. Creative Header & Hero Badge
_buildHeroBanner(isDark, cardBg, textPrimary, textSecondary),
const SizedBox(height: 20),
// 2. Style Selector Tabs (Sigil, Liquid, Lightning, Motion, Nexus, Standard)
_buildStyleSelector(isDark),
const SizedBox(height: 24),
// 4. Main Active OTP Input Container Card
_buildActiveOtpShowcaseCard(isDark, cardBg),
const SizedBox(height: 24),
// 5. Customization Options (Only for Standard mode or global overrides)
if (_currentStyle == DemoStyle.standard) ...[
_buildStandardCustomizer(isDark, cardBg, textPrimary, textSecondary),
const SizedBox(height: 24),
],
const SizedBox(height: 32),
],
),
),
),
),
);
}
Widget _buildHeroBanner(
bool isDark,
Color cardBg,
Color textPrimary,
Color textSecondary,
) {
final styleInfo = _getStyleInfo(_currentStyle);
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cardBg,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.08)
: const Color(0xFFE2E8F0),
),
boxShadow: [
BoxShadow(
color: styleInfo.accentColor.withValues(alpha: 0.12),
blurRadius: 24,
spreadRadius: -4,
offset: const Offset(0, 8),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: styleInfo.accentColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(18),
),
child: Icon(styleInfo.icon, color: styleInfo.accentColor, size: 28),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
styleInfo.name,
style: GoogleFonts.outfit(
fontSize: 18,
fontWeight: FontWeight.bold,
color: textPrimary,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: styleInfo.accentColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
styleInfo.tag,
style: GoogleFonts.outfit(
fontSize: 10,
fontWeight: FontWeight.bold,
color: styleInfo.accentColor,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: const Color(0xFF22C55E).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF22C55E).withValues(alpha: 0.4),
),
),
child: Text(
'π Code: ${styleInfo.correctCode}',
style: GoogleFonts.outfit(
fontSize: 11,
fontWeight: FontWeight.bold,
color: const Color(0xFF22C55E),
),
),
),
],
),
const SizedBox(height: 4),
Text(
styleInfo.description,
style: GoogleFonts.outfit(
fontSize: 13,
color: textSecondary,
),
),
],
),
),
],
),
);
}
Widget _buildStyleSelector(bool isDark) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row(
children: DemoStyle.values.map((style) {
final isSelected = _currentStyle == style;
final info = _getStyleInfo(style);
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
child: InkWell(
onTap: () {
setState(() {
_currentStyle = style;
_widgetDarkOverride = null;
_clearAllInput();
});
},
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: isSelected
? info.accentColor
: (isDark
? const Color(0xFF1E293B)
: Colors.white),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected
? info.accentColor
: (isDark
? Colors.white.withValues(alpha: 0.08)
: const Color(0xFFE2E8F0)),
),
boxShadow: isSelected
? [
BoxShadow(
color: info.accentColor.withValues(alpha: 0.35),
blurRadius: 12,
offset: const Offset(0, 4),
)
]
: null,
),
child: Row(
children: [
Icon(
info.icon,
size: 16,
color: isSelected
? Colors.white
: (isDark ? const Color(0xFF94A3B8) : const Color(0xFF64748B)),
),
const SizedBox(width: 8),
Text(
info.shortName,
style: GoogleFonts.outfit(
fontSize: 13,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.w600,
color: isSelected
? Colors.white
: (isDark ? Colors.white : const Color(0xFF334155)),
),
),
],
),
),
),
),
);
}).toList(),
),
);
}
Widget _buildActiveOtpShowcaseCard(bool isDark, Color cardBg) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
child: switch (_currentStyle) {
DemoStyle.sigil => SigilOtpVerificationView(
key: ValueKey('sigil_$_isEffectiveDark'),
length: 4,
isDark: _isEffectiveDark,
controller: _sigilController,
focusNode: _sigilFocusNode,
isSuccess: _isSuccess,
isError: _isError,
isVerifying: _isVerifying,
onCompleted: (val) => _simulateCompletion(val, '7890'),
onResend: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('β¨ Resent Sigil OTP Code (Try "7890")'),
backgroundColor: Color(0xFFA855F7),
),
);
},
),
DemoStyle.liquid => LiquidOtpVerificationView(
key: ValueKey('liquid_$_isEffectiveDark'),
length: 4,
isDark: _isEffectiveDark,
controller: _liquidController,
focusNode: _liquidFocusNode,
isSuccess: _isSuccess,
isError: _isError,
isVerifying: _isVerifying,
onCompleted: (val) => _simulateCompletion(val, '1234'),
onResend: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('π Resent Liquid OTP Code (Try "1234")'),
backgroundColor: Color(0xFF3B82F6),
),
);
},
),
DemoStyle.lightning => LightningOtpVerificationView(
key: ValueKey('lightning_$_isEffectiveDark'),
length: 4,
isDark: _isEffectiveDark,
controller: _lightningController,
focusNode: _lightningFocusNode,
isSuccess: _isSuccess,
isError: _isError,
isVerifying: _isVerifying,
onCompleted: (val) => _simulateCompletion(val, '4565'),
onResend: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('β‘ Resent Lightning OTP Code (Try "4565")'),
backgroundColor: Color(0xFFF59E0B),
),
);
},
),
DemoStyle.motion => MotionOtpVerificationView(
key: ValueKey('motion_$_isEffectiveDark'),
length: 4,
isDark: _isEffectiveDark,
controller: _motionController,
focusNode: _motionFocusNode,
isSuccess: _isSuccess,
isError: _isError,
isVerifying: _isVerifying,
onCompleted: (val) => _simulateCompletion(val, '1234'),
onResend: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('π Resent Motion OTP Code (Try "1234")'),
backgroundColor: Color(0xFF6366F1),
),
);
},
),
DemoStyle.nexus => NexusOtpVerificationView(
key: ValueKey('nexus_$_isEffectiveDark'),
length: 4,
isDark: _isEffectiveDark,
controller: _nexusController,
focusNode: _nexusFocusNode,
isSuccess: _isSuccess,
isError: _isError,
isVerifying: _isVerifying,
onCompleted: (val) => _simulateCompletion(val, '4545'),
onResend: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('πΈοΈ Resent Nexus OTP Code (Try "4545")'),
backgroundColor: Color(0xFFFF453A),
),
);
},
),
DemoStyle.standard => Container(
padding: const EdgeInsets.all(28),
decoration: BoxDecoration(
color: _isEffectiveDark ? const Color(0xFF1E293B) : Colors.white,
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: _isEffectiveDark
? Colors.white.withValues(alpha: 0.12)
: const Color(0xFFE2E8F0),
),
),
child: Column(
children: [
Text(
'Standard Premium OTP',
style: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.bold,
color: _isEffectiveDark ? Colors.white : const Color(0xFF0F172A),
),
),
const SizedBox(height: 6),
Text(
'Fully configurable standard input widget',
style: GoogleFonts.outfit(
fontSize: 13,
color: _isEffectiveDark
? const Color(0xFF94A3B8)
: const Color(0xFF64748B),
),
),
const SizedBox(height: 28),
PremiumOtpInput(
length: 6,
controller: _standardController,
focusNode: _standardFocusNode,
isSuccess: _isSuccess,
isError: _isError,
isVerifying: _isVerifying,
onCompleted: (val) => _simulateCompletion(val, '123456'),
obscureText: _obscureText,
obscuringCharacter: _obscuringCharacter,
entryAnimationStyle: _entryAnimationStyle,
successAnimationStyle: _successAnimationStyle,
animateActiveBorder: true,
boxHeight: _boxHeight,
spacing: 10.0,
borderRadius: _borderRadius,
activeBorderColor: const Color(0xFF4F6EF7),
defaultBorderColor: _isEffectiveDark
? Colors.white.withValues(alpha: 0.2)
: const Color(0xFFCBD5E1),
boxBackgroundColor: _isEffectiveDark
? const Color(0xFF0F172A)
: const Color(0xFFF8FAFC),
textStyle: GoogleFonts.outfit(
fontSize: 24,
fontWeight: FontWeight.bold,
color: _isEffectiveDark ? Colors.white : const Color(0xFF0F172A),
),
),
],
),
),
},
);
}
Widget _buildStandardCustomizer(
bool isDark,
Color cardBg,
Color textPrimary,
Color textSecondary,
) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cardBg,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.08)
: const Color(0xFFE2E8F0),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.tune_rounded, size: 20, color: Color(0xFF4F6EF7)),
const SizedBox(width: 10),
Text(
'Customize Standard Properties',
style: GoogleFonts.outfit(
fontSize: 16,
fontWeight: FontWeight.bold,
color: textPrimary,
),
),
],
),
const Divider(height: 24),
// Entry animation style
Text(
'Digit Entry Animation:',
style: GoogleFonts.outfit(
fontSize: 13, fontWeight: FontWeight.w600, color: textSecondary),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: OtpEntryAnimationStyle.values.map((s) {
return ChoiceChip(
label: Text(s.name),
selected: _entryAnimationStyle == s,
onSelected: (sel) {
if (sel) setState(() => _entryAnimationStyle = s);
},
);
}).toList(),
),
const SizedBox(height: 16),
// Success animation style
Text(
'Success Animation Style:',
style: GoogleFonts.outfit(
fontSize: 13, fontWeight: FontWeight.w600, color: textSecondary),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: OtpSuccessAnimationStyle.values.map((s) {
return ChoiceChip(
label: Text(s.name),
selected: _successAnimationStyle == s,
onSelected: (sel) {
if (sel) setState(() => _successAnimationStyle = s);
},
);
}).toList(),
),
const SizedBox(height: 16),
// Box Height Slider
Row(
children: [
Text(
'Box Height (${_boxHeight.toInt()}px):',
style: GoogleFonts.outfit(
fontSize: 13, fontWeight: FontWeight.w600, color: textSecondary),
),
Expanded(
child: Slider(
value: _boxHeight,
min: 50,
max: 84,
onChanged: (val) => setState(() => _boxHeight = val),
),
),
],
),
// Border Radius Slider
Row(
children: [
Text(
'Border Radius (${_borderRadius.toInt()}px):',
style: GoogleFonts.outfit(
fontSize: 13, fontWeight: FontWeight.w600, color: textSecondary),
),
Expanded(
child: Slider(
value: _borderRadius,
min: 4,
max: 32,
onChanged: (val) => setState(() => _borderRadius = val),
),
),
],
),
const SizedBox(height: 12),
// Obscure toggle & character selection
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Obscure Text (PIN Mode):',
style: GoogleFonts.outfit(
fontSize: 13, fontWeight: FontWeight.w600, color: textSecondary),
),
Switch(
value: _obscureText,
onChanged: (val) => setState(() => _obscureText = val),
),
],
),
if (_obscureText) ...[
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: ['β', '*', 'β
', 'β₯'].map((char) {
return ChoiceChip(
label: Text(char),
selected: _obscuringCharacter == char,
onSelected: (sel) {
if (sel) setState(() => _obscuringCharacter = char);
},
);
}).toList(),
),
],
],
),
);
}
_StyleMeta _getStyleInfo(DemoStyle style) {
return switch (style) {
DemoStyle.nexus => _StyleMeta(
name: 'Nexus Lattice Mesh',
shortName: 'Nexus',
tag: 'NEURAL',
description: 'Neural lattice network with interconnected node physics and orbital electron arcs.',
correctCode: '4545',
icon: Icons.hub_outlined,
accentColor: const Color(0xFFFF453A),
),
DemoStyle.sigil => _StyleMeta(
name: 'Sigil Arc Flow',
shortName: 'Sigil',
tag: 'COSMIC',
description: 'Dark-glowing cosmic slots with interactive autofill toast and smooth arc animations.',
correctCode: '7890',
icon: Icons.auto_awesome,
accentColor: const Color(0xFFA855F7),
),
DemoStyle.liquid => _StyleMeta(
name: 'Liquid Motion',
shortName: 'Liquid',
tag: 'AQUATIC',
description: 'Glass containers filled with dynamic wavy liquid, floating bubbles & splash level transitions.',
correctCode: '1234',
icon: Icons.water_drop_outlined,
accentColor: const Color(0xFF3B82F6),
),
DemoStyle.lightning => _StyleMeta(
name: 'Lightning Discharge',
shortName: 'Lightning',
tag: 'ELECTRIC',
description: 'Electric discharge sparks running around outline paths with energy flare feedback.',
correctCode: '4565',
icon: Icons.bolt_rounded,
accentColor: const Color(0xFFF59E0B),
),
DemoStyle.motion => _StyleMeta(
name: 'Motion Elastic',
shortName: 'Motion',
tag: 'PHYSICS',
description: 'Fluid spring physics with elastic pop-in and celebration checkmark transitions.',
correctCode: '1234',
icon: Icons.motion_photos_on_rounded,
accentColor: const Color(0xFF6366F1),
),
DemoStyle.standard => _StyleMeta(
name: 'Standard Premium',
shortName: 'Standard',
tag: 'CLASSIC',
description: 'Highly customizable digit boxes with scaling entry animations, PIN masking & loaders.',
correctCode: '123456',
icon: Icons.pin_outlined,
accentColor: const Color(0xFF4F6EF7),
),
};
}
}
class _StyleMeta {
final String name;
final String shortName;
final String tag;
final String description;
final String correctCode;
final IconData icon;
final Color accentColor;
_StyleMeta({
required this.name,
required this.shortName,
required this.tag,
required this.description,
required this.correctCode,
required this.icon,
required this.accentColor,
});
}