pin_plus_keyboard 3.0.2
pin_plus_keyboard: ^3.0.2 copied to clipboard
A flutter package that gives you custom input fields and a custom keyboard for one time password widgets, transaction pin widgets and simple login widgets.
pin_plus_keyboard #
A highly customizable Flutter package that provides custom input fields and a custom numeric keyboard for PIN entry, OTP (One-Time Password) widgets, transaction PIN widgets, and simple login widgets.
Features #
✨ Highly Customizable
- Customizable input field shapes (circular, rounded, or default)
- Customizable keyboard button shapes and styles
- Full control over colors, sizes, borders, and shadows
- Support for hidden input (password-style)
- Custom fonts and text styles
- Gradient support for buttons and inputs
- Custom builders for complete control
- Theme presets (iOS-style, Android-style, Material 3)
🎨 Beautiful UI
- Modern and clean design
- Smooth animations (fill, shake, success)
- Responsive layout
- Support for elevation and shadows
- Multiple input styles (box or dash)
- Material 3 design support
- Dynamic color theming
♿ Accessibility
- Full screen reader support
- Keyboard navigation
- Semantic labels and hints
- High contrast mode support
- ARIA-like attributes
🔒 Security
- Auto-clear after inactivity timeout
- Screenshot blocking (Android)
- Rate limiting for failed attempts
- Secure input handling
- Random keyboard layout (shoulder surfing protection)
🔐 Biometric Integration
- Face ID / Touch ID / Fingerprint support
- Optional biometric authentication button
- Fallback to PIN when biometrics fail
- Platform-agnostic interface
⚡ Easy to Use
- Simple API
- Well-documented
- Comprehensive examples
- Type-safe with null safety
- Platform-specific optimizations
Getting Started #
Installation #
Add this package to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
pin_plus_keyboard: ^3.0.0
For biometric features, also add local_auth:
dependencies:
local_auth: ^2.0.0 # Optional, for biometric authentication
Then run:
flutter pub get
Import #
import 'package:pin_plus_keyboard/pin_plus_keyboard.dart';
Usage #
Basic Example #
The most basic usage requires three things:
- A
PinInputControllerwith the desired PIN length - A
spacingvalue for layout - An
onSubmitcallback function
import 'package:flutter/material.dart';
import 'package:pin_plus_keyboard/pin_plus_keyboard.dart';
class PinEntryScreen extends StatefulWidget {
const PinEntryScreen({super.key});
@override
State<PinEntryScreen> createState() => _PinEntryScreenState();
}
class _PinEntryScreenState extends State<PinEntryScreen> {
// Initialize the controller with the desired PIN length
final PinInputController _pinController = PinInputController(length: 6);
@override
void dispose() {
// Don't forget to dispose the controller
_pinController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Enter Your PIN',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 40),
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// This is called when all fields are filled or done button is pressed
print('PIN entered: ${_pinController.text}');
// Add your validation/submission logic here
},
),
],
),
),
);
}
}
Advanced Example with Customization #
PinPlusKeyBoardPackage(
// Required parameters
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
print('PIN: ${_pinController.text}');
},
// Input field customization
inputShape: InputShape.circular,
inputType: InputType.box,
inputFillColor: Colors.grey[200],
inputBorderColor: Colors.blue,
inputTextColor: Colors.black,
inputHasBorder: true,
inputBorderThickness: 2,
inputElevation: 5,
isInputHidden: false, // Set to true for password-style input
// Keyboard customization
keyboardButtonShape: KeyboardButtonShape.circular,
buttonFillColor: Colors.blue,
btnTextColor: Colors.white,
btnHasBorder: false,
btnElevation: 3,
keyboardMaxWidth: 80, // Percentage of screen width
// Error handling
errorColor: Colors.red,
)
Example with Material 3 #
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
},
// Enable Material 3 design tokens
useMaterial3: true,
// Colors will be automatically derived from theme
)
Example with Animations #
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
},
// Enable all animations
enableAnimations: true,
enableShakeAnimation: true,
enableSuccessAnimation: true,
animationCurve: Curves.easeInOut,
animationDuration: Duration(milliseconds: 300),
)
Example with Biometric Authentication #
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
},
// Enable biometric authentication
enableBiometric: true,
biometricReason: 'Please authenticate to access your account',
onBiometricSuccess: () {
print('Biometric authentication successful');
},
onBiometricFailure: () {
print('Biometric authentication failed');
},
)
Note: For biometric features to work, you need to add local_auth to your pubspec.yaml:
dependencies:
local_auth: ^2.0.0
Example with Security Features #
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
},
// Auto-clear PIN after 30 seconds of inactivity
autoClearTimeout: Duration(seconds: 30),
// Enable screenshot blocking (Android only)
enableScreenshotBlocking: true,
// Rate limiting
maxFailedAttempts: 5,
rateLimitWindow: Duration(minutes: 15),
)
Example with Random Keyboard Layout #
The random keyboard feature shuffles the positions of numbers 1-9 on the keyboard, making it harder for observers to see which numbers are being pressed (shoulder surfing protection). The zero button always remains in the center position.
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
},
// Enable random keyboard layout
enableRandomKeyboard: true,
// Reshuffle when PIN is cleared (optional)
reshuffleOnClear: true,
// Reshuffle when app comes to foreground (optional)
reshuffleOnResume: true,
)
Security Benefits:
- Shoulder Surfing Protection: Random number positions make it difficult for observers to determine which numbers are being pressed
- Enhanced Privacy: Even if someone watches you enter your PIN, they can't easily replicate it on a standard keyboard layout
- Flexible Reshuffling: Choose when to reshuffle - on initialization, on clear, or when app resumes
Note: The random layout only affects numbers 1-9. The zero button (0) always remains in the center position of the bottom row for consistency.
Example with Theme Presets #
import 'package:pin_plus_keyboard/pin_plus_keyboard.dart';
// Use iOS-style preset
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {},
...PinThemePresets.iosStyle(),
)
// Use Material 3 preset
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {},
...PinThemePresets.material3Style(
colorScheme: Theme.of(context).colorScheme,
),
)
Example with Hidden Input (Password Style) #
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
},
isInputHidden: true, // Hide the input as user types
inputHiddenColor: Colors.black, // Color to hide the input
)
Example with Dash Style Input #
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
},
inputType: InputType.dash, // Underline style instead of box
inputBorderColor: Colors.blue,
)
Example with Haptic Feedback and Validation #
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
print('PIN submitted: ${_pinController.text}');
},
// Enable haptic feedback for better user experience
enableHapticFeedback: true,
// Custom validation - return error message if invalid
validator: (String pin) {
// Example: Prevent PINs with all same digits
if (pin.split('').every((digit) => digit == pin[0])) {
return 'PIN cannot be all the same digit';
}
// Example: Prevent sequential PINs
if (pin == '123456' || pin == '654321') {
return 'PIN cannot be sequential';
}
return ''; // Empty string means validation passed
},
// Track each digit entry (useful for analytics)
onDigitEntered: (String digit, int position) {
print('Digit $digit entered at position $position');
},
)
Example: Clearing PIN Input #
To clear the PIN input (e.g., after validation error or successful submission), use the clear() method on the controller:
// ❌ INCORRECT - This will NOT work:
// pinInputController.text = ''; // Don't do this!
// ✅ CORRECT - Use the clear() method:
pinInputController.clear();
// Alternative - Use changeText() with empty string:
// pinInputController.changeText('');
Complete example with error handling:
class PinEntryScreen extends StatefulWidget {
const PinEntryScreen({super.key});
@override
State<PinEntryScreen> createState() => _PinEntryScreenState();
}
class _PinEntryScreenState extends State<PinEntryScreen> {
final PinInputController _pinController = PinInputController(length: 6);
void _handlePinSubmission() {
// Validate PIN (e.g., check with backend)
if (_isValidPin(_pinController.text)) {
// Success - clear PIN and navigate
_pinController.clear();
Navigator.push(context, MaterialPageRoute(builder: (_) => HomeScreen()));
} else {
// Error - show error message and clear PIN
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid PIN. Please try again.')),
);
// Clear the PIN input so user can try again
_pinController.clear();
}
}
bool _isValidPin(String pin) {
// Your validation logic here
return pin == '123456'; // Example
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: _handlePinSubmission,
validator: (String pin) {
if (pin.length < 6) {
return 'PIN must be 6 digits';
}
return ''; // No error
},
),
);
}
@override
void dispose() {
_pinController.dispose();
super.dispose();
}
}
Important Notes:
- Always use
pinInputController.clear()to clear the PIN input - Do NOT directly assign to
pinInputController.textas it won't trigger UI updates - The
clear()method automatically notifies all listeners and resets animations - After clearing, the input fields will be empty and ready for new input
Example with Gradients #
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
},
// Gradient for keyboard buttons
buttonGradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
// Gradient for input fields
inputGradient: LinearGradient(
colors: [Colors.grey.shade100, Colors.grey.shade200],
),
)
Example with Custom Builders #
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {
// Handle PIN submission
},
// Custom input field builder
inputFieldBuilder: (context, position, hasCharacter, character) {
return Container(
// Your custom input field design
child: Text(character ?? ''),
);
},
// Custom keyboard button builder
keyboardButtonBuilder: (context, number) {
return ElevatedButton(
onPressed: () {
// Handle button press
},
child: Text(number),
);
},
)
Accessibility #
The package includes comprehensive accessibility support:
- Screen Reader Support: All interactive elements have semantic labels
- Keyboard Navigation: Users can navigate using keyboard arrows (when enabled)
- High Contrast Mode: Automatically adapts to system accessibility settings
- Semantic Labels: Proper labels for all buttons and inputs
PinPlusKeyBoardPackage(
pinInputController: _pinController,
spacing: 40,
onSubmit: () {},
// Custom semantic label
semanticLabel: 'Enter your 6-digit PIN code',
// Enable keyboard navigation
enableKeyboardNavigation: true,
)
API Reference #
PinInputController #
The controller manages the state of the PIN input.
Properties
length(int): The number of input fields (required)text(String): The current PIN value
Methods
changeText(String text): Updates the PIN value and notifies all listenersclear(): Clears the PIN value and notifies all listeners. This is the recommended way to clear the PIN input.addListener(VoidCallback listener): Listen to changesremoveListener(VoidCallback listener): Remove listener
Important: Do not directly assign to the text property. Always use clear() or changeText() methods to ensure the UI updates correctly.
Getters
isComplete(bool): Returns true if all fields are filledisEmpty(bool): Returns true if no input has been entered
PinPlusKeyBoardPackage #
The main widget that displays the PIN input fields and keyboard.
Required Parameters
| Parameter | Type | Description |
|---|---|---|
pinInputController |
PinInputController |
Controller that manages the PIN state |
spacing |
double |
Space between input fields and keyboard |
onSubmit |
VoidCallback |
Callback called when PIN is submitted |
Optional Parameters
Input Field Customization
| Parameter | Type | Default | Description |
|---|---|---|---|
inputShape |
InputShape |
InputShape.defaultShape |
Shape of input fields (circular, rounded, defaultShape) |
inputType |
InputType |
InputType.box |
Style of input (box or dash) |
inputWidth |
double? |
null |
Width of individual input fields |
inputHeight |
double? |
null |
Height of individual input fields |
inputFillColor |
Color? |
null |
Fill color for empty input fields |
inputBorderColor |
Color? |
null |
Border color for input fields |
inputTextColor |
Color? |
null |
Text color for input numbers |
inputHasBorder |
bool |
true |
Whether input fields have borders |
inputBorderThickness |
double? |
null |
Thickness of input borders |
inputBorderRadius |
BorderRadius? |
null |
Custom border radius for inputs |
inputElevation |
double? |
null |
Shadow elevation for inputs |
inputShadowColor |
Color? |
null |
Shadow color for inputs |
inputTextStyle |
TextStyle? |
null |
Custom text style for inputs |
isInputHidden |
bool |
false |
Hide input as user types (password-style) |
inputHiddenColor |
Color |
Colors.black |
Color used to hide input |
inputsMaxWidth |
double |
70 |
Max width of input container (% of screen) |
focusColor |
Color? |
null |
Color for focused/active inputs |
inputGradient |
Gradient? |
null |
Gradient for input fields (overrides fillColor) |
inputFieldBuilder |
Widget Function(...)? |
null |
Custom builder for input fields |
Keyboard Customization
| Parameter | Type | Default | Description |
|---|---|---|---|
keyboardButtonShape |
KeyboardButtonShape |
defaultShape |
Shape of keyboard buttons |
keyboardMaxWidth |
double |
80 |
Max width of keyboard (% of screen) |
keyboardVerticalSpacing |
double |
8 |
Vertical spacing between button rows |
keyboardBtnSize |
double? |
null |
Custom size for keyboard buttons |
buttonFillColor |
Color? |
null |
Fill color for keyboard buttons |
buttonBorderColor |
Color? |
null |
Border color for keyboard buttons |
btnTextColor |
Color? |
null |
Text color for keyboard buttons |
btnHasBorder |
bool |
true |
Whether buttons have borders |
btnBorderThickness |
double? |
null |
Thickness of button borders |
btnElevation |
double? |
null |
Shadow elevation for buttons |
btnShadowColor |
Color? |
null |
Shadow color for buttons |
keyboardFontSize |
double? |
null |
Font size for keyboard button text |
keyboardFontFamily |
String? |
null |
Font family for keyboard buttons |
keyoardBtnBorderRadius |
BorderRadius? |
null |
Custom border radius for buttons |
buttonGradient |
Gradient? |
null |
Gradient for buttons (overrides fillColor) |
keyboardButtonBuilder |
Widget Function(...)? |
null |
Custom builder for keyboard buttons |
Action Buttons
| Parameter | Type | Default | Description |
|---|---|---|---|
backButton |
Icon? |
null |
Custom icon for backspace button |
doneButton |
Icon? |
null |
Custom icon for done/submit button |
cancelColor |
Color? |
null |
Color for backspace button |
extraInput |
String? |
null |
Extra character to display (e.g., "*", "#") |
leftExtraInputWidget |
Widget? |
null |
Custom widget for left side of bottom row |
Error Handling & Validation
| Parameter | Type | Default | Description |
|---|---|---|---|
errorColor |
Color |
Colors.red |
Color for error messages |
enableHapticFeedback |
bool |
false |
Enable haptic feedback on button presses |
validator |
String Function(String pin)? |
null |
Custom validation function (returns error message if invalid) |
onDigitEntered |
void Function(String digit, int position)? |
null |
Callback called when each digit is entered |
Accessibility
| Parameter | Type | Default | Description |
|---|---|---|---|
semanticLabel |
String? |
null |
Semantic label for screen readers |
enableKeyboardNavigation |
bool |
true |
Enable keyboard navigation |
Animations
| Parameter | Type | Default | Description |
|---|---|---|---|
enableAnimations |
bool |
true |
Enable input fill animations |
animationCurve |
Curve |
Curves.easeInOut |
Animation curve for fill animations |
animationDuration |
Duration |
200ms |
Duration for fill animations |
enableShakeAnimation |
bool |
true |
Enable shake animation on validation errors |
enableSuccessAnimation |
bool |
true |
Enable success animation on completion |
showLoadingState |
bool |
false |
Show loading indicator during submission |
Material 3
| Parameter | Type | Default | Description |
|---|---|---|---|
useMaterial3 |
true |
bool |
Use Material 3 design tokens |
colorScheme |
ColorScheme? |
null |
Custom ColorScheme (overrides theme) |
Platform-Specific
| Parameter | Type | Default | Description |
|---|---|---|---|
usePlatformSpecificStyling |
bool |
true |
Use platform-specific styling and behaviors |
Biometric
| Parameter | Type | Default | Description |
|---|---|---|---|
enableBiometric |
bool |
false |
Enable biometric authentication button |
biometricService |
BiometricService? |
null |
Custom biometric service instance |
onBiometricSuccess |
VoidCallback? |
null |
Callback when biometric succeeds |
onBiometricFailure |
VoidCallback? |
null |
Callback when biometric fails |
biometricReason |
String? |
null |
Reason shown to user during authentication |
Security
| Parameter | Type | Default | Description |
|---|---|---|---|
securityService |
SecurityService? |
null |
Custom security service instance |
autoClearTimeout |
Duration? |
null |
Auto-clear PIN after inactivity (null = disabled) |
enableScreenshotBlocking |
bool |
false |
Enable screenshot blocking (Android only) |
maxFailedAttempts |
int |
5 |
Max failed attempts before rate limiting |
rateLimitWindow |
Duration |
15 minutes |
Time window for rate limiting |
Random Keyboard Layout
| Parameter | Type | Default | Description |
|---|---|---|---|
enableRandomKeyboard |
bool |
false |
Enable random keyboard layout (shuffles numbers 1-9) |
reshuffleOnClear |
bool |
false |
Reshuffle keyboard when PIN is cleared |
reshuffleOnResume |
bool |
false |
Reshuffle keyboard when app comes to foreground |
PinThemePresets #
Predefined theme presets for quick setup:
PinThemePresets.iosStyle()- iOS-style themePinThemePresets.androidStyle()- Android Material Design themePinThemePresets.material3Style(colorScheme?)- Material 3 themePinThemePresets.minimalStyle()- Minimal themePinThemePresets.darkStyle()- Dark theme
Screenshots #

Requirements #
- Flutter SDK: >=3.24.0
- Dart SDK: >=3.4.0
For biometric features:
local_auth: ^2.0.0(optional dependency)
Contributing #
Contributions are welcome! Please read our Contributing Guidelines for details on our code of conduct and the process for submitting pull requests.
License #
This project is licensed under the terms specified in the LICENSE file.
Support #
If you encounter any issues or have questions, please:
Changelog #
See CHANGELOG.md for a list of changes and version history.