premium_otp_input 0.2.0
premium_otp_input: ^0.2.0 copied to clipboard
A highly customizable, beautiful, and interactive OTP and PIN entry widget for Flutter featuring Lightning, Liquid, and Motion animations.
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, lightning }
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();
final TextEditingController _lightningOtpController = TextEditingController();
final FocusNode _lightningOtpFocusNode = FocusNode();
DemoStyle _currentStyle = DemoStyle.lightning;
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 _handleLightningOtpCompleted(String value) async {
setState(() {
_isVerifying = true;
_isError = false;
});
// Simulated network request while the spark runs around the boxes.
await Future.delayed(const Duration(milliseconds: 1500));
if (!mounted) return;
if (value == "4565") {
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;
_lightningOtpController.clear();
_lightningOtpFocusNode.requestFocus();
});
}
}
void _reset() {
setState(() {
_isVerifying = false;
_isSuccess = false;
_isError = false;
_otpController.clear();
_liquidOtpController.clear();
_motionOtpController.clear();
_lightningOtpController.clear();
switch (_currentStyle) {
case DemoStyle.liquid:
_liquidOtpFocusNode.requestFocus();
case DemoStyle.motion:
_motionOtpFocusNode.requestFocus();
case DemoStyle.lightning:
_lightningOtpFocusNode.requestFocus();
case DemoStyle.standard:
_otpFocusNode.requestFocus();
}
});
}
Widget _styleChip(DemoStyle style, String label) {
final selected = _currentStyle == style;
return GestureDetector(
onTap: () {
setState(() {
_currentStyle = style;
_reset();
});
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: selected ? const Color(0xFF4F6EF7) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.bold,
color: selected ? Colors.white : const Color(0xFF475569),
),
),
),
);
}
@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 (Standard / Liquid / Motion / Lightning)
Center(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: const Color(0xFFE2E8F0),
borderRadius: BorderRadius.circular(12),
),
child: Wrap(
spacing: 4,
runSpacing: 4,
alignment: WrapAlignment.center,
children: [
_styleChip(DemoStyle.standard, 'Standard'),
_styleChip(DemoStyle.liquid, 'Liquid'),
_styleChip(DemoStyle.motion, 'Motion'),
_styleChip(DemoStyle.lightning, 'Lightning'),
],
),
),
),
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(
switch (_currentStyle) {
DemoStyle.motion => 'Premium Motion Animation',
DemoStyle.liquid => 'Liquid Motion Verification',
DemoStyle.lightning => 'Lightning Electric Flow',
DemoStyle.standard => 'Interactive Demo App',
},
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _currentStyle == DemoStyle.standard
? const Color(0xFFF97316)
: _currentStyle == DemoStyle.lightning
? const Color(0xFFF59E0B)
: 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.'
: _currentStyle == DemoStyle.lightning
? 'Enter "4565" 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.lightning)
LightningOtpVerificationView(
length: 4,
controller: _lightningOtpController,
focusNode: _lightningOtpFocusNode,
isSuccess: _isSuccess,
isError: _isError,
isVerifying: _isVerifying,
onCompleted: _handleLightningOtpCompleted,
onResend: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('OTP Resent successfully!'),
backgroundColor: Color(0xFFF59E0B),
),
);
},
)
else 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(),
),
],
],
),
),
),
],
),
),
),
);
}
}