premium_otp_input 0.1.0
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(),
),
],
],
),
),
),
],
),
),
),
);
}
}